//! Editor-only Project Settings panel (egui). Data lives in the `settings` crate.

use std::path::Path;

use bevy::prelude::*;
use bevy_egui::egui;
use game_hot::sync_project_sun_from_settings;
use settings::{
    save_project_settings_to_string, sync_sim_tuning, ExposureMode, ProjectSettings,
    ProjectSettingsChanged, ProjectSettingsIo,
};

use crate::project::collaboration::{
    file_status_indicator_ui, publish_authored_file, CollaborationState, FileSnapshot,
    FileWriteIntent,
};
use crate::scene_io::SceneIo;

/// Whether the Project Settings window is visible (editor UI state only).
#[derive(Resource, Debug, Default)]
pub struct ProjectSettingsPanel {
    pub open: bool,
    pub draft: Option<ProjectSettings>,
    disk_snapshot: Option<FileSnapshot>,
    save_error: Option<String>,
}

pub struct SettingsUiPlugin;

impl Plugin for SettingsUiPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<ProjectSettingsPanel>().add_systems(
            PostUpdate,
            apply_project_settings_changes
                .after(crate::scene_view::sync_editor_camera_viewport_target),
        );
    }
}

pub fn open_project_settings_panel(panel: &mut ProjectSettingsPanel) {
    panel.open = true;
    panel.draft = None;
}

pub fn project_settings_window(
    world: &mut World,
    ctx: &egui::Context,
    panel: &mut ProjectSettingsPanel,
) {
    if !panel.open {
        panel.draft = None;
        panel.disk_snapshot = None;
        panel.save_error = None;
        return;
    }

    if panel.draft.is_none() {
        panel.draft = Some(world.resource::<ProjectSettings>().clone());
        let io = world.resource::<ProjectSettingsIo>();
        panel.disk_snapshot = Some(
            io.loaded_source
                .as_deref()
                .map(|source| {
                    FileSnapshot::from_loaded_bytes(Path::new(&io.path), source.as_bytes())
                })
                .unwrap_or_else(FileSnapshot::missing),
        );
        panel.save_error = None;
    }

    let mut open = panel.open;
    egui::Window::new("Project Settings")
        .open(&mut open)
        .default_width(420.0)
        .show(ctx, |ui| {
            let ProjectSettingsPanel {
                draft: Some(draft),
                disk_snapshot,
                save_error,
                ..
            } = panel
            else {
                return;
            };
            let io_path = world.resource::<ProjectSettingsIo>().path.clone();
            let mut io_dirty = world.resource::<ProjectSettingsIo>().dirty;
            draw_project_settings_form(
                world,
                ui,
                draft,
                &io_path,
                &mut io_dirty,
                disk_snapshot,
                save_error,
            );
            world.resource_mut::<ProjectSettingsIo>().dirty = io_dirty;
        });
    panel.open = open;
    if !open {
        panel.draft = None;
        panel.disk_snapshot = None;
        panel.save_error = None;
    }
}

fn draw_project_settings_form(
    world: &mut World,
    ui: &mut egui::Ui,
    draft: &mut ProjectSettings,
    io_path: &str,
    io_dirty: &mut bool,
    disk_snapshot: &mut Option<FileSnapshot>,
    save_error: &mut Option<String>,
) {
    let caps = world
        .get_resource::<settings::RenderingCapabilities>()
        .cloned()
        .unwrap_or_default();

    ui.horizontal_wrapped(|ui| {
        ui.small(egui::RichText::new(io_path).monospace());
        if let Some(state) = world.get_resource::<CollaborationState>() {
            let status = state.file_status(Path::new(io_path));
            let _ = file_status_indicator_ui(ui, &status, Path::new(io_path));
        }
    });
    if let Some(error) = save_error.as_deref() {
        ui.colored_label(crate::ui::theme::ERROR, error);
    }
    ui.separator();

    ui.collapsing("Global Illumination", |ui| {
        egui::ComboBox::from_label("GiMode")
            .selected_text(format!("{:?}", draft.rendering.gi_mode))
            .show_ui(ui, |ui| {
                for mode in [
                    settings::GiMode::Auto,
                    settings::GiMode::Forward,
                    settings::GiMode::Solari,
                ] {
                    if ui
                        .selectable_value(&mut draft.rendering.gi_mode, mode, format!("{mode:?}"))
                        .clicked()
                    {}
                }
            });
        ui.small("Auto uses Solari when RT GPU support is available; otherwise Forward PBR.");
        let status = match (draft.rendering.gi_mode, caps.rt_supported) {
            (settings::GiMode::Forward, _) => "Forward PBR",
            (settings::GiMode::Solari, true) => "Solari active",
            (settings::GiMode::Solari, false) => "Solari requested — will fall back (no RT)",
            (settings::GiMode::Auto, true) => "Auto → Solari when playing",
            (settings::GiMode::Auto, false) => "Auto → Forward fallback (no RT)",
        };
        ui.label(format!("Status: {status}"));
        ui.small("Open Window → Rendering for resolved active-camera stack.");
    });

    ui.collapsing("Lighting", |ui| {
        ui.label("Ambient");
        ui.horizontal(|ui| {
            let mut rgb = [
                draft.rendering.ambient_color[0],
                draft.rendering.ambient_color[1],
                draft.rendering.ambient_color[2],
            ];
            if ui.color_edit_button_rgb(&mut rgb).changed() {
                draft.rendering.ambient_color = rgb;
            }
        });
        ui.add(
            egui::Slider::new(&mut draft.rendering.ambient_brightness, 0.0..=200.0)
                .text("Ambient brightness"),
        );
        ui.add(
            egui::Slider::new(&mut draft.rendering.sun_illuminance, 1_000.0..=150_000.0)
                .text("Sun illuminance (lux)"),
        );
        ui.add(
            egui::Slider::new(&mut draft.rendering.shadow_cascades, 1..=4).text("Shadow cascades"),
        );
        ui.add(
            egui::Slider::new(&mut draft.rendering.shadow_first_cascade, 1.0..=20.0)
                .text("First cascade (m)"),
        );
        ui.add(
            egui::Slider::new(&mut draft.rendering.shadow_max_distance, 50.0..=500.0)
                .text("Shadow max distance"),
        );
    });

    ui.collapsing("Post Process", |ui| {
        ui.checkbox(&mut draft.rendering.hdr, "HDR");
        ui.small("The viewport uses HDR when Lit mode is active.");
        ui.horizontal(|ui| {
            ui.label("Exposure");
            if ui
                .selectable_label(
                    draft.rendering.exposure_mode == ExposureMode::Manual,
                    "Manual",
                )
                .clicked()
            {
                draft.rendering.exposure_mode = ExposureMode::Manual;
            }
            if ui
                .selectable_label(draft.rendering.exposure_mode == ExposureMode::Auto, "Auto")
                .clicked()
            {
                draft.rendering.exposure_mode = ExposureMode::Auto;
            }
        });
        let manual = draft.rendering.exposure_mode == ExposureMode::Manual;
        if manual {
            ui.add(
                egui::Slider::new(&mut draft.rendering.exposure_ev100, 0.0..=20.0)
                    .text("Exposure EV100"),
            );
        } else {
            ui.add_enabled(
                false,
                egui::Slider::new(&mut draft.rendering.exposure_ev100, 0.0..=20.0)
                    .text("Exposure EV100"),
            );
            ui.small("Scene brightness adapts automatically (histogram metering).");
            ui.add(
                egui::Slider::new(
                    &mut draft.rendering.auto_exposure_speed_brighten,
                    0.1..=10.0,
                )
                .text("Auto brighten (f-stops/s)"),
            );
            ui.add(
                egui::Slider::new(&mut draft.rendering.auto_exposure_speed_darken, 0.1..=10.0)
                    .text("Auto darken (f-stops/s)"),
            );
            if !draft.rendering.hdr {
                ui.colored_label(
                    egui::Color32::from_rgb(255, 180, 100),
                    "Auto exposure requires HDR — enable HDR or use Manual.",
                );
            } else {
                ui.small("Requires GPU compute; see Window → Rendering if unavailable.");
            }
        }
        ui.checkbox(&mut draft.rendering.tonemapping_aces, "Tonemapping ACES");
        ui.checkbox(&mut draft.rendering.bloom, "Bloom");
        ui.checkbox(&mut draft.rendering.taa, "TAA");
        ui.checkbox(&mut draft.rendering.ssao, "SSAO");
        ui.checkbox(&mut draft.rendering.atmosphere, "Atmosphere");
        ui.label("Fog");
        ui.horizontal(|ui| {
            let mut fog_rgb = draft.rendering.fog_color;
            if ui.color_edit_button_rgb(&mut fog_rgb).changed() {
                draft.rendering.fog_color = fog_rgb;
            }
        });
        ui.add(
            egui::Slider::new(&mut draft.rendering.fog_density, 0.0..=0.01)
                .logarithmic(true)
                .text("Fog density"),
        );
    });

    ui.collapsing("Audio Buses", |ui| {
        let bus_ids: Vec<String> = draft.audio.buses.iter().map(|bus| bus.id.clone()).collect();
        let mut remove_bus = None;
        for (index, bus) in draft.audio.buses.iter_mut().enumerate() {
            egui::Frame::new()
                .fill(egui::Color32::from_rgb(24, 26, 29))
                .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(53, 57, 63)))
                .corner_radius(4.0)
                .inner_margin(8.0)
                .show(ui, |ui| {
                    ui.horizontal(|ui| {
                        ui.strong(&bus.label);
                        ui.small(
                            egui::RichText::new(format!("[{}]", bus.id))
                                .color(egui::Color32::from_rgb(139, 146, 156)),
                        );
                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                            let required = matches!(
                                bus.id.as_str(),
                                settings::AUDIO_BUS_MASTER_ID
                                    | settings::AUDIO_BUS_MUSIC_ID
                                    | settings::AUDIO_BUS_SFX_ID
                            );
                            if ui
                                .add_enabled(!required, egui::Button::new("Remove"))
                                .clicked()
                            {
                                remove_bus = Some(index);
                            }
                        });
                    });
                    ui.horizontal_wrapped(|ui| {
                        ui.label("Name");
                        ui.add_sized([140.0, 20.0], egui::TextEdit::singleline(&mut bus.label));
                        ui.checkbox(&mut bus.muted, "Mute");
                    });
                    ui.add(
                        egui::Slider::new(
                            &mut bus.gain_db,
                            settings::AUDIO_GAIN_DB_MIN..=settings::AUDIO_GAIN_DB_MAX,
                        )
                        .text("Gain")
                        .suffix(" dB"),
                    );
                    if bus.id != settings::AUDIO_BUS_MASTER_ID {
                        let parent_label = bus
                            .parent
                            .as_deref()
                            .and_then(|parent| bus_ids.iter().find(|id| id.as_str() == parent))
                            .map(String::as_str)
                            .unwrap_or("None");
                        egui::ComboBox::from_id_salt(("audio_bus_parent", index))
                            .selected_text(parent_label)
                            .show_ui(ui, |ui| {
                                for candidate in &bus_ids {
                                    if candidate != &bus.id {
                                        ui.selectable_value(
                                            &mut bus.parent,
                                            Some(candidate.clone()),
                                            candidate,
                                        );
                                    }
                                }
                            });
                    }
                });
            ui.add_space(4.0);
        }
        if let Some(index) = remove_bus {
            draft.audio.buses.remove(index);
        }
        if ui.button("Add Bus").clicked() {
            let mut suffix = 1usize;
            let id = loop {
                let candidate = format!("bus_{suffix}");
                if draft.audio.bus(&candidate).is_none() {
                    break candidate;
                }
                suffix += 1;
            };
            draft.audio.buses.push(settings::AudioBusSettings::new(
                id,
                "New Bus",
                Some(settings::AUDIO_BUS_MASTER_ID),
            ));
        }
        if let Err(error) = draft.audio.validate() {
            ui.colored_label(egui::Color32::from_rgb(235, 105, 105), error.to_string());
        }
    });

    ui.separator();
    ui.heading("Physics");
    ui.add(egui::Slider::new(&mut draft.physics.walk_speed, 1.0..=15.0).text("Walk speed"));
    ui.add(egui::Slider::new(&mut draft.physics.run_speed, 1.0..=20.0).text("Run speed"));
    ui.add(egui::Slider::new(&mut draft.physics.gravity, 5.0..=40.0).text("Gravity"));
    ui.add(egui::Slider::new(&mut draft.physics.jump_speed, 3.0..=15.0).text("Jump speed"));

    ui.separator();
    ui.heading("Input");
    ui.add(
        egui::Slider::new(&mut draft.input.yaw_sensitivity, 0.0005..=0.01).text("Yaw sensitivity"),
    );
    ui.add(
        egui::Slider::new(&mut draft.input.pitch_sensitivity, 0.0005..=0.01)
            .text("Pitch sensitivity"),
    );
    ui.add(
        egui::Slider::new(&mut draft.input.editor_fly_speed, 2.0..=40.0).text("Editor fly speed"),
    );

    ui.separator();
    let settings_valid = draft.audio.validate().is_ok();
    if ui
        .add_enabled(settings_valid, egui::Button::new("Apply"))
        .clicked()
    {
        commit_project_settings(world, draft);
        *io_dirty = true;
    }
    if ui
        .add_enabled(settings_valid, egui::Button::new("Save to disk"))
        .clicked()
    {
        commit_project_settings(world, draft);
        *io_dirty = true;
        let settings = world.resource::<ProjectSettings>().clone();
        let Some(expected) = disk_snapshot.clone() else {
            let error = "Project settings disk baseline is unavailable; reload before saving";
            *save_error = Some(error.into());
            world.resource_mut::<SceneIo>().set_status(error);
            return;
        };
        match persist_project_settings(world, &settings, &expected) {
            Ok((snapshot, source)) => {
                let mut io = world.resource_mut::<ProjectSettingsIo>();
                io.dirty = false;
                io.loaded_source = Some(source);
                *disk_snapshot = Some(snapshot);
                *save_error = None;
                *io_dirty = false;
                world
                    .resource_mut::<SceneIo>()
                    .set_status(format!("Saved project settings to {io_path}"));
            }
            Err(error) => {
                warn!("Save project settings failed: {error}");
                *save_error = Some(error.clone());
                world
                    .resource_mut::<SceneIo>()
                    .set_status(format!("Save project settings failed: {error}"));
            }
        }
    }
    if ui.button("Revert from disk").clicked() {
        match read_project_settings_document(Path::new(io_path)) {
            Ok((settings, source, snapshot)) => {
                *draft = settings;
                commit_project_settings(world, draft);
                let mut io = world.resource_mut::<ProjectSettingsIo>();
                io.dirty = false;
                io.loaded_source = Some(source);
                *disk_snapshot = Some(snapshot);
                *save_error = None;
                *io_dirty = false;
            }
            Err(error) => {
                *save_error = Some(error.clone());
                world
                    .resource_mut::<SceneIo>()
                    .set_status(format!("Revert project settings failed: {error}"));
            }
        }
    }
    if *io_dirty {
        ui.label(egui::RichText::new("Unsaved changes").color(egui::Color32::YELLOW));
    }
}

fn persist_project_settings(
    world: &mut World,
    settings: &ProjectSettings,
    expected: &FileSnapshot,
) -> Result<(FileSnapshot, String), String> {
    let path = world.resource::<ProjectSettingsIo>().path.clone();
    let source = save_project_settings_to_string(settings).map_err(|error| error.to_string())?;
    let snapshot = publish_authored_file(
        world,
        Path::new(&path),
        source.as_bytes(),
        expected,
        FileWriteIntent::ProjectSettings,
    )?;
    Ok((snapshot, source))
}

fn read_project_settings_document(
    path: &Path,
) -> Result<(ProjectSettings, String, FileSnapshot), String> {
    let source = std::fs::read_to_string(path)
        .map_err(|error| format!("could not read {}: {error}", path.display()))?;
    let settings: ProjectSettings = ron::from_str(&source)
        .map_err(|error| format!("invalid project settings in {}: {error}", path.display()))?;
    let snapshot = FileSnapshot::from_loaded_bytes(path, source.as_bytes());
    Ok((settings, source, snapshot))
}

pub(crate) fn reload_project_settings_after_file_conflict(
    world: &mut World,
    path: &Path,
) -> Result<String, String> {
    let active_path = world.resource::<ProjectSettingsIo>().path.clone();
    if Path::new(&active_path) != path {
        return Err("the project settings conflict no longer matches the active manifest".into());
    }
    let (settings, source, snapshot) = read_project_settings_document(path)?;
    commit_project_settings(world, &settings);
    {
        let mut io = world.resource_mut::<ProjectSettingsIo>();
        io.dirty = false;
        io.loaded_source = Some(source);
    }
    {
        let mut panel = world.resource_mut::<ProjectSettingsPanel>();
        panel.draft = Some(settings);
        panel.disk_snapshot = Some(snapshot);
        panel.save_error = None;
    }
    Ok(format!("Reloaded project settings from {}", path.display()))
}

pub(crate) fn project_settings_conflict_copy_saved(world: &mut World) {
    world.resource_mut::<ProjectSettingsPanel>().save_error = None;
}

fn commit_project_settings(world: &mut World, draft: &ProjectSettings) {
    let previous = world.resource::<ProjectSettings>().rendering.gi_mode;
    *world.resource_mut::<ProjectSettings>() = draft.clone();
    if previous != draft.rendering.gi_mode {
        info!(
            "Project settings GI mode changed: {:?} -> {:?}",
            previous, draft.rendering.gi_mode
        );
    } else {
        debug!(
            "Project settings applied: gi_mode={:?}",
            draft.rendering.gi_mode
        );
    }
    world.write_message(ProjectSettingsChanged);
}

fn apply_project_settings_changes(
    mut reader: MessageReader<ProjectSettingsChanged>,
    mut commands: Commands,
    settings: Res<ProjectSettings>,
    tuning: ResMut<settings::SimTuning>,
    scene_suns: Query<
        (
            &shared::LightDesc,
            Option<&shared::AuthoringComponentStates>,
            Option<&shared::InspectorOrder>,
        ),
        With<shared::LevelObject>,
    >,
    project_suns: Query<(&mut DirectionalLight, &mut Visibility), With<shared::ProjectSun>>,
) {
    if reader.read().next().is_none() {
        return;
    }
    sync_sim_tuning(&settings, tuning);
    {
        let ambient = settings.rendering.clone();
        commands.insert_resource(GlobalAmbientLight {
            color: Color::srgb(
                ambient.ambient_color[0],
                ambient.ambient_color[1],
                ambient.ambient_color[2],
            ),
            brightness: ambient.ambient_brightness,
            ..default()
        });
    }
    sync_project_sun_from_settings(settings, scene_suns, project_suns);
    // Full stack refresh runs next frame via `render_view::sync_project_render_view`
    // when `ProjectSettings` changes.
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn project_settings_save_refuses_an_external_revision() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-project-settings-external-write-{}",
            uuid::Uuid::new_v4()
        ));
        let path = root.join("assets/project.ron");
        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
        let loaded = save_project_settings_to_string(&ProjectSettings::default()).unwrap();
        std::fs::write(&path, &loaded).unwrap();
        let expected = FileSnapshot::from_loaded_bytes(&path, loaded.as_bytes());
        let mut world = World::new();
        world.insert_resource(ProjectSettingsIo {
            path: path.to_string_lossy().into_owned(),
            loaded_source: Some(loaded),
            ..Default::default()
        });
        world.insert_resource(SceneIo::default());
        std::fs::write(&path, b"external").unwrap();

        let result = persist_project_settings(&mut world, &ProjectSettings::default(), &expected);

        assert!(result.unwrap_err().contains("changed outside Blacksite"));
        assert_eq!(std::fs::read(&path).unwrap(), b"external");
        std::fs::remove_dir_all(root).unwrap();
    }
}
