use std::collections::HashSet;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

use bevy::math::Affine3A;
use bevy::prelude::*;
use bevy_egui::egui;
use egui_phosphor_icons::icons;
use scene::navigation::{
    bake_navigation, navigation_source_fingerprint, read_navigation_artifact,
    write_navigation_artifact, NavigationBakeArtifact, NavigationBakeInput,
};
use shared::{
    ActorId, NavigationArea, NavigationBounds, NavigationLink, NavigationObstacle,
    NavigationPathSample, COMPONENT_NAVIGATION_AREA, COMPONENT_NAVIGATION_BOUNDS,
    COMPONENT_NAVIGATION_LINK, COMPONENT_NAVIGATION_OBSTACLE,
};

use crate::history::{set_navigation_with_history, NavigationComponentState};
use crate::project_io::ProjectWorkspace;
use crate::scene_io::SceneIo;
use crate::ui::inspector::{
    apply_component_card_response, component_card, component_card_context, property_row,
    ComponentCardOptions,
};
use crate::ui::theme::{ERROR, SUCCESS, TEXT_DIM, WARNING};
use crate::viewport::{editor_spawn_ground_position, snap_translation, ViewportSettings};

#[derive(Resource, Debug, Clone)]
pub struct NavigationEditorState {
    pub bounds_entity: Option<Entity>,
    pub artifact: Option<NavigationBakeArtifact>,
    pub preview_start: Vec3,
    pub preview_end: Vec3,
    pub preview_path: Vec<Vec3>,
    pub status: String,
    pub error: bool,
    pub show_mesh: bool,
    artifact_authored_path: Option<String>,
    artifact_resolved_path: Option<PathBuf>,
    artifact_disk_revision: Option<ArtifactDiskRevision>,
    artifact_poll_elapsed: f32,
    input_bounds_entity: Option<Entity>,
    input_scene_revision: u64,
    input_refresh_elapsed: f32,
    current_input: Option<NavigationBakeInput>,
    current_input_error: Option<String>,
    observed_fingerprint: Option<String>,
    auto_bake_attempt: Option<String>,
    debounce_elapsed: f32,
}

impl Default for NavigationEditorState {
    fn default() -> Self {
        Self {
            bounds_entity: None,
            artifact: None,
            preview_start: Vec3::new(-4.0, 0.0, 0.0),
            preview_end: Vec3::new(4.0, 0.0, 0.0),
            preview_path: Vec::new(),
            status: "Not baked".into(),
            error: false,
            show_mesh: true,
            artifact_authored_path: None,
            artifact_resolved_path: None,
            artifact_disk_revision: None,
            artifact_poll_elapsed: 0.0,
            input_bounds_entity: None,
            input_scene_revision: 0,
            input_refresh_elapsed: 0.0,
            current_input: None,
            current_input_error: None,
            observed_fingerprint: None,
            auto_bake_attempt: None,
            debounce_elapsed: 0.0,
        }
    }
}

pub fn navigation_bounds_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut bounds) = world.get::<NavigationBounds>(entity).cloned() else {
        return;
    };
    let original = bounds.clone();
    let mut preview_start = world.resource::<NavigationEditorState>().preview_start;
    let mut preview_end = world.resource::<NavigationEditorState>().preview_end;
    let mut show_mesh = world.resource::<NavigationEditorState>().show_mesh;
    let status = world.resource::<NavigationEditorState>().status.clone();
    let status_error = world.resource::<NavigationEditorState>().error;
    let (current, current_error) = {
        let state = world.resource::<NavigationEditorState>();
        if state.input_bounds_entity == Some(entity) {
            (
                state.current_input.clone(),
                state.current_input_error.clone(),
            )
        } else {
            (None, None)
        }
    };
    let stale = current.as_ref().is_none_or(|input| {
        let state = world.resource::<NavigationEditorState>();
        !artifact_source_matches(world, state, entity, &bounds)
            || state
                .artifact
                .as_ref()
                .is_none_or(|artifact| artifact.is_stale_for(input))
    });
    let mut bake = false;
    let mut preview = false;
    let mut pin_sample = false;
    let mut load_sample = None;
    let mut remove_sample = None;

    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_NAVIGATION_BOUNDS,
            "Navigation Bounds",
            icons::PATH,
        ),
    );
    let response = component_card(ui, &card, |ui| {
        property_row(ui, "Half extents", |ui| {
            vec3_editor(ui, &mut bounds.half_extents, 0.1)
        });
        property_row(ui, "Agent ID", |ui| {
            ui.text_edit_singleline(&mut bounds.agent.id)
        });
        property_row(ui, "Radius / height", |ui| {
            ui.horizontal(|ui| {
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.radius)
                        .speed(0.01)
                        .range(0.01..=100.0),
                );
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.height)
                        .speed(0.01)
                        .range(0.01..=100.0),
                )
            })
            .response
        });
        let maximum_climb = (bounds.agent.height - f32::EPSILON).max(0.0);
        property_row(ui, "Climb / slope", |ui| {
            ui.horizontal(|ui| {
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.max_climb)
                        .speed(0.01)
                        .range(0.0..=maximum_climb),
                );
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.max_slope_deg)
                        .speed(0.25)
                        .range(0.0..=89.9)
                        .suffix(" deg"),
                )
            })
            .response
        });
        property_row(ui, "Voxel detail", |ui| {
            ui.horizontal(|ui| {
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.cell_size_fraction)
                        .speed(0.1)
                        .range(1.0..=32.0),
                );
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.cell_height_fraction)
                        .speed(0.1)
                        .range(1.0..=64.0),
                )
            })
            .response
        });
        property_row(ui, "Regions min / merge", |ui| {
            ui.horizontal(|ui| {
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.min_region_size)
                        .speed(1.0)
                        .range(0..=255),
                );
                ui.add(
                    egui::DragValue::new(&mut bounds.agent.merge_region_size)
                        .speed(1.0)
                        .range(0..=255),
                )
            })
            .response
        });
        property_row(ui, "Artifact", |ui| {
            ui.text_edit_singleline(&mut bounds.artifact_path)
        });
        property_row(ui, "Auto bake", |ui| {
            ui.checkbox(&mut bounds.auto_bake, "Debounced")
        });

        ui.separator();
        ui.horizontal(|ui| {
            bake = ui
                .button(format!("{} Bake", icons::HAMMER.as_str()))
                .on_hover_text("Bake deterministic navigation artifact")
                .clicked();
            preview = ui
                .add_enabled(
                    !stale,
                    egui::Button::new(format!("{} Test Path", icons::PATH.as_str())),
                )
                .on_hover_text("Query the baked artifact using the game runtime API")
                .clicked();
            ui.checkbox(&mut show_mesh, "Overlay");
        });
        let status_color = if status_error || current_error.is_some() {
            ERROR
        } else if stale {
            WARNING
        } else {
            SUCCESS
        };
        ui.colored_label(
            status_color,
            match current_error.as_deref() {
                Some(error) => format!("Bake blocked: {error}"),
                None if stale => format!("Stale: {status}"),
                None => status.clone(),
            },
        );

        property_row(ui, "Path start", |ui| {
            vec3_editor(ui, &mut preview_start, 0.1)
        });
        property_row(ui, "Path end", |ui| vec3_editor(ui, &mut preview_end, 0.1));
        ui.separator();
        ui.strong(format!(
            "Validation samples ({})",
            bounds.validation_samples.len()
        ));
        if ui
            .add_enabled(
                preview_start.is_finite()
                    && preview_end.is_finite()
                    && preview_start != preview_end,
                egui::Button::new(format!("{} Pin Current Path", icons::PLUS.as_str())),
            )
            .on_hover_text("Persist the current path endpoints as a build validation sample")
            .clicked()
        {
            pin_sample = true;
        }
        for (index, sample) in bounds.validation_samples.iter_mut().enumerate() {
            ui.push_id(("navigation-sample", index), |ui| {
                ui.horizontal(|ui| {
                    ui.checkbox(&mut sample.enabled, "")
                        .on_hover_text("Include this path in build validation");
                    ui.add(
                        egui::TextEdit::singleline(&mut sample.id)
                            .desired_width((ui.available_width() - 62.0).max(72.0)),
                    );
                    if ui
                        .button(icons::PLAY.as_str())
                        .on_hover_text("Load this sample into the path preview")
                        .clicked()
                    {
                        load_sample = Some(index);
                    }
                    if ui
                        .button(icons::TRASH.as_str())
                        .on_hover_text("Remove validation sample")
                        .clicked()
                    {
                        remove_sample = Some(index);
                    }
                });
                property_row(ui, "Start", |ui| vec3_editor(ui, &mut sample.start, 0.1));
                property_row(ui, "End", |ui| vec3_editor(ui, &mut sample.end, 0.1));
            });
        }
        if let Some(input) = current.as_ref() {
            ui.label(
                egui::RichText::new(format!(
                    "{} obstacles  {} areas  {} links  {} samples",
                    input.obstacles.len(),
                    input.areas.len(),
                    input.links.len(),
                    input.samples.len()
                ))
                .small()
                .color(TEXT_DIM),
            );
        }
    });
    apply_component_card_response(world, entity, response);

    if let Some(index) = remove_sample {
        bounds.validation_samples.remove(index);
    }
    let bounds_transform = world_affine(world, entity);
    if pin_sample {
        let id = next_sample_id(&bounds.validation_samples);
        let inverse = bounds_transform.inverse();
        bounds.validation_samples.push(NavigationPathSample {
            id,
            start: inverse.transform_point3(preview_start),
            end: inverse.transform_point3(preview_end),
            enabled: true,
        });
    }
    if let Some(sample) = load_sample.and_then(|index| bounds.validation_samples.get(index)) {
        preview_start = bounds_transform.transform_point3(sample.start);
        preview_end = bounds_transform.transform_point3(sample.end);
    }

    if bounds != original {
        set_navigation_with_history(
            world,
            entity,
            NavigationComponentState {
                bounds: Some(bounds.clone()),
                ..Default::default()
            },
        );
    }
    {
        let mut state = world.resource_mut::<NavigationEditorState>();
        state.preview_start = preview_start;
        state.preview_end = preview_end;
        state.show_mesh = show_mesh;
    }
    if bake {
        bake_bounds(world, entity);
    }
    if preview {
        query_preview(world, entity);
    }
}

fn next_sample_id(samples: &[NavigationPathSample]) -> String {
    (1..)
        .map(|index| format!("sample-{index}"))
        .find(|candidate| samples.iter().all(|sample| sample.id != *candidate))
        .expect("navigation sample IDs have an unbounded numeric suffix")
}

pub fn navigation_obstacle_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut value) = world.get::<NavigationObstacle>(entity).cloned() else {
        return;
    };
    let original = value.clone();
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_NAVIGATION_OBSTACLE,
            "Navigation Obstacle",
            icons::SELECTION_SLASH,
        ),
    );
    let response = component_card(ui, &card, |ui| {
        property_row(ui, "Half extents", |ui| {
            vec3_editor(ui, &mut value.half_extents, 0.05)
        });
        property_row(ui, "Runtime", |ui| {
            ui.checkbox(&mut value.dynamic, "Dynamic obstacle")
        });
        if value.dynamic {
            ui.colored_label(
                WARNING,
                "Dynamic carving is not baked in navigation v1; this volume remains a static bake input.",
            );
        }
    });
    apply_component_card_response(world, entity, response);
    if value != original {
        set_navigation_with_history(
            world,
            entity,
            NavigationComponentState {
                obstacle: Some(value),
                ..Default::default()
            },
        );
    }
}

pub fn navigation_area_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut value) = world.get::<NavigationArea>(entity).cloned() else {
        return;
    };
    let original = value.clone();
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_NAVIGATION_AREA,
            "Navigation Area",
            icons::POLYGON,
        ),
    );
    let response = component_card(ui, &card, |ui| {
        property_row(ui, "Area ID", |ui| ui.text_edit_singleline(&mut value.id));
        property_row(ui, "Half extents", |ui| {
            vec3_editor(ui, &mut value.half_extents, 0.05)
        });
        property_row(ui, "Cost", |ui| {
            ui.add(
                egui::DragValue::new(&mut value.cost)
                    .speed(0.05)
                    .range(0.05..=100.0),
            )
        });
        property_row(ui, "Walkable", |ui| {
            ui.checkbox(&mut value.walkable, "Enabled")
        });
    });
    apply_component_card_response(world, entity, response);
    if value != original {
        set_navigation_with_history(
            world,
            entity,
            NavigationComponentState {
                area: Some(value),
                ..Default::default()
            },
        );
    }
}

pub fn navigation_link_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut value) = world.get::<NavigationLink>(entity).cloned() else {
        return;
    };
    let original = value.clone();
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_NAVIGATION_LINK, "Navigation Link", icons::LINK),
    );
    let response = component_card(ui, &card, |ui| {
        property_row(ui, "Start", |ui| vec3_editor(ui, &mut value.start, 0.05));
        property_row(ui, "End", |ui| vec3_editor(ui, &mut value.end, 0.05));
        property_row(ui, "Direction", |ui| {
            ui.checkbox(&mut value.bidirectional, "Bidirectional")
        });
        property_row(ui, "Cost", |ui| {
            ui.add(
                egui::DragValue::new(&mut value.cost)
                    .speed(0.05)
                    .range(0.05..=100.0),
            )
        });
        property_row(ui, "Enabled", |ui| {
            ui.checkbox(&mut value.enabled, "Active")
        });
    });
    apply_component_card_response(world, entity, response);
    if value != original {
        set_navigation_with_history(
            world,
            entity,
            NavigationComponentState {
                link: Some(value),
                ..Default::default()
            },
        );
    }
}

pub fn bake_bounds(world: &mut World, entity: Entity) {
    let Some(bounds) = world.get::<NavigationBounds>(entity).cloned() else {
        set_status(
            world,
            "Bake failed: selected actor has no Navigation Bounds",
            true,
        );
        return;
    };
    let input = match build_input(world, entity, &bounds) {
        Ok(input) => input,
        Err(error) => {
            set_status(world, format!("Bake failed: {error}"), true);
            return;
        }
    };
    let artifact = match bake_navigation(&input) {
        Ok(artifact) => artifact,
        Err(error) => {
            set_status(world, format!("Bake failed: {error}"), true);
            return;
        }
    };
    let path = match artifact_path(world, &bounds.artifact_path) {
        Ok(path) => path,
        Err(error) => {
            set_status(world, format!("Bake failed: {error}"), true);
            return;
        }
    };
    if let Err(error) = write_navigation_artifact(&path, &artifact) {
        set_status(world, format!("Bake failed: {error}"), true);
        return;
    }
    let polygon_count: usize = artifact
        .mesh
        .layers
        .iter()
        .map(|layer| layer.polygons.len())
        .sum();
    refresh_artifact_source(world, entity, &bounds, true);
    let loaded_current = world
        .resource::<NavigationEditorState>()
        .artifact
        .as_ref()
        .is_some_and(|loaded| !loaded.is_stale_for(&input));
    if !loaded_current {
        set_status(
            world,
            format!(
                "Bake failed: could not reload the generated artifact {}",
                bounds.artifact_path
            ),
            true,
        );
        return;
    }
    let mut state = world.resource_mut::<NavigationEditorState>();
    state.preview_path.clear();
    state.error = false;
    state.status = format!("Baked {polygon_count} polygons to {}", bounds.artifact_path);
}

pub fn query_preview(world: &mut World, entity: Entity) {
    let Some(bounds) = world.get::<NavigationBounds>(entity).cloned() else {
        set_status(
            world,
            "Path test failed: selected actor has no Navigation Bounds",
            true,
        );
        return;
    };
    refresh_artifact_source(world, entity, &bounds, true);
    let input = match build_input(world, entity, &bounds) {
        Ok(input) => input,
        Err(error) => {
            set_status(world, format!("Path test failed: {error}"), true);
            return;
        }
    };
    let (artifact, start, end) = {
        let state = world.resource::<NavigationEditorState>();
        (
            state.artifact.clone(),
            state.preview_start,
            state.preview_end,
        )
    };
    let Some(artifact) = artifact else {
        set_status(world, "Path test failed: bake navigation first", true);
        return;
    };
    if artifact.is_stale_for(&input) {
        set_status(
            world,
            "Path test failed: navigation artifact is stale; rebake first",
            true,
        );
        return;
    }
    let runtime = match game::navigation::NavigationRuntime::from_artifact(artifact) {
        Ok(runtime) => runtime,
        Err(error) => {
            set_status(world, format!("Path test failed: {error}"), true);
            return;
        }
    };
    match runtime.query(start, end) {
        Ok(path) => {
            let mut state = world.resource_mut::<NavigationEditorState>();
            state.bounds_entity = Some(entity);
            state.preview_path = path.points;
            state.error = false;
            state.status = format!(
                "Path {:.2} m, {} waypoints{}",
                path.length,
                state.preview_path.len(),
                if path.used_link_actor_ids.is_empty() {
                    String::new()
                } else {
                    format!(", links {}", path.used_link_actor_ids.join(", "))
                }
            );
        }
        Err(error) => set_status(world, format!("Path test failed: {error}"), true),
    }
}

fn build_input(
    world: &mut World,
    bounds_entity: Entity,
    bounds: &NavigationBounds,
) -> Result<NavigationBakeInput, String> {
    let active_path = world
        .resource::<SceneIo>()
        .active_path
        .as_ref()
        .cloned()
        .ok_or_else(|| "save the scene before baking navigation".to_string())?;
    let project_root = PathBuf::from(&world.resource::<ProjectWorkspace>().root);
    let bounds_actor_id = actor_id(world, bounds_entity)?;
    let snapshot = crate::scene_io::serialize_active_scene(world)
        .map_err(|error| format!("could not snapshot authored scene for navigation: {error}"))?;
    let jobs = scene::navigation::resolve_navigation_bake_jobs(
        &project_root,
        &active_path,
        Some(&snapshot),
    )?;
    let selected_artifact = shared::navigation_generated_artifact_path(&bounds.artifact_path)
        .map_err(str::to_string)?;
    let duplicate_owners = jobs
        .iter()
        .filter(|job| {
            shared::navigation_generated_artifact_path(&job.artifact_path)
                .is_ok_and(|path| path == selected_artifact)
        })
        .map(|job| job.input.bounds_actor_id.as_str())
        .collect::<Vec<_>>();
    if duplicate_owners.len() > 1 {
        return Err(format!(
            "navigation artifact `{selected_artifact}` has duplicate bounds owners: {}. Assign a unique generated path to each bounds actor before baking",
            duplicate_owners.join(", ")
        ));
    }
    jobs.into_iter()
    .find(|job| job.input.bounds_actor_id == bounds_actor_id)
    .map(|job| job.input)
    .ok_or_else(|| {
        format!(
            "navigation bounds `{bounds_actor_id}` is not owned by the active scene; open its source scene to bake it"
        )
    })
}

fn actor_id(world: &World, entity: Entity) -> Result<String, String> {
    world
        .get::<ActorId>(entity)
        .map(|id| id.0.clone())
        .filter(|id| !id.trim().is_empty())
        .ok_or_else(|| format!("navigation actor {entity:?} requires a stable ActorId"))
}

fn world_affine(world: &World, entity: Entity) -> Affine3A {
    let mut chain = Vec::new();
    let mut current = entity;
    let mut visited = HashSet::new();
    while visited.insert(current) {
        chain.push(world.get::<Transform>(current).copied().unwrap_or_default());
        let Some(parent) = world.get::<ChildOf>(current).map(ChildOf::parent) else {
            break;
        };
        current = parent;
    }
    if world.get::<ChildOf>(current).is_some() {
        return Affine3A::IDENTITY;
    }
    let mut chain = chain.into_iter().rev();
    let root = chain.next().unwrap_or_default();
    chain
        .fold(GlobalTransform::from(root), |global, local| {
            global.mul_transform(local)
        })
        .affine()
}

pub(crate) fn world_aabb_half_extents(transform: Affine3A, local_half_extents: Vec3) -> Vec3 {
    let x = Vec3::from(transform.matrix3.x_axis).abs() * local_half_extents.x;
    let y = Vec3::from(transform.matrix3.y_axis).abs() * local_half_extents.y;
    let z = Vec3::from(transform.matrix3.z_axis).abs() * local_half_extents.z;
    x + y + z
}

fn artifact_path(world: &World, authored: &str) -> Result<PathBuf, String> {
    let relative = shared::navigation_generated_artifact_path(authored).map_err(str::to_string)?;
    Ok(Path::new(&world.resource::<ProjectWorkspace>().root).join(relative))
}

const ARTIFACT_POLL_INTERVAL_SECS: f32 = 0.5;
const INPUT_REFRESH_INTERVAL_SECS: f32 = 1.0;

#[derive(Debug, Clone, PartialEq, Eq)]
enum ArtifactDiskRevision {
    Missing,
    Present(blake3::Hash),
    Unreadable(ErrorKind),
}

fn artifact_disk_revision(path: &Path) -> ArtifactDiskRevision {
    match std::fs::read(path) {
        Ok(bytes) => ArtifactDiskRevision::Present(blake3::hash(&bytes)),
        Err(error) if error.kind() == ErrorKind::NotFound => ArtifactDiskRevision::Missing,
        Err(error) => ArtifactDiskRevision::Unreadable(error.kind()),
    }
}

fn artifact_source_matches(
    world: &World,
    state: &NavigationEditorState,
    entity: Entity,
    bounds: &NavigationBounds,
) -> bool {
    let resolved = artifact_path(world, &bounds.artifact_path).ok();
    state.bounds_entity == Some(entity)
        && state.artifact_authored_path.as_deref() == Some(bounds.artifact_path.as_str())
        && state.artifact_resolved_path == resolved
}

/// Refreshes the selected artifact from its authoritative disk path.
///
/// Identity changes are checked immediately. `poll_disk` enables the periodic content-hash check
/// used to notice external deletes and rebakes without hashing a potentially large artifact every
/// render frame.
fn refresh_artifact_source(
    world: &mut World,
    entity: Entity,
    bounds: &NavigationBounds,
    poll_disk: bool,
) -> bool {
    let resolved_result = artifact_path(world, &bounds.artifact_path);
    let resolved_path = resolved_result.as_ref().ok().cloned();
    let identity_changed = {
        let state = world.resource::<NavigationEditorState>();
        state.bounds_entity != Some(entity)
            || state.artifact_authored_path.as_deref() != Some(bounds.artifact_path.as_str())
            || state.artifact_resolved_path != resolved_path
    };
    if !identity_changed && !poll_disk {
        return false;
    }

    let disk_revision = resolved_path.as_deref().map(artifact_disk_revision);
    let source_changed = identity_changed
        || world
            .resource::<NavigationEditorState>()
            .artifact_disk_revision
            != disk_revision;
    {
        let mut state = world.resource_mut::<NavigationEditorState>();
        state.artifact_poll_elapsed = 0.0;
    }
    if !source_changed {
        return false;
    }

    let (artifact, status, error) = match (&resolved_result, &disk_revision) {
        (Err(error), _) => (
            None,
            format!("Navigation artifact unavailable: {error}"),
            true,
        ),
        (Ok(_), Some(ArtifactDiskRevision::Missing)) => (
            None,
            format!("Artifact missing: {}; bake required", bounds.artifact_path),
            false,
        ),
        (Ok(path), Some(ArtifactDiskRevision::Unreadable(kind))) => (
            None,
            format!("Navigation artifact unavailable: failed to read {path:?}: {kind:?}"),
            true,
        ),
        (Ok(path), Some(ArtifactDiskRevision::Present(_))) => {
            match read_navigation_artifact(path) {
                Ok(artifact) => (
                    Some(artifact),
                    format!("Loaded: {}", bounds.artifact_path),
                    false,
                ),
                Err(error) => (
                    None,
                    format!("Navigation artifact unavailable: {error}"),
                    true,
                ),
            }
        }
        (Ok(_), None) => unreachable!("resolved artifacts always have a disk revision"),
    };

    let mut state = world.resource_mut::<NavigationEditorState>();
    state.bounds_entity = Some(entity);
    state.artifact_authored_path = Some(bounds.artifact_path.clone());
    state.artifact_resolved_path = resolved_path;
    state.artifact_disk_revision = disk_revision;
    state.artifact = artifact;
    state.preview_path.clear();
    state.status = status;
    state.error = error;
    state.observed_fingerprint = None;
    state.auto_bake_attempt = None;
    state.debounce_elapsed = 0.0;
    true
}

fn clear_artifact_selection(world: &mut World) {
    let should_clear = {
        let state = world.resource::<NavigationEditorState>();
        state.bounds_entity.is_some()
            || state.artifact.is_some()
            || state.artifact_authored_path.is_some()
            || state.input_bounds_entity.is_some()
    };
    if !should_clear {
        return;
    }
    let mut state = world.resource_mut::<NavigationEditorState>();
    state.bounds_entity = None;
    state.artifact = None;
    state.artifact_authored_path = None;
    state.artifact_resolved_path = None;
    state.artifact_disk_revision = None;
    state.artifact_poll_elapsed = 0.0;
    state.input_bounds_entity = None;
    state.input_scene_revision = 0;
    state.input_refresh_elapsed = 0.0;
    state.current_input = None;
    state.current_input_error = None;
    state.preview_path.clear();
    state.status = "Select Navigation Bounds to inspect a bake".into();
    state.error = false;
    state.observed_fingerprint = None;
    state.auto_bake_attempt = None;
    state.debounce_elapsed = 0.0;
}

fn set_status(world: &mut World, message: impl Into<String>, error: bool) {
    let message = message.into();
    let mut state = world.resource_mut::<NavigationEditorState>();
    state.status.clone_from(&message);
    state.error = error;
    if error {
        state.preview_path.clear();
    }
    world.resource_mut::<SceneIo>().set_status(message);
}

fn vec3_editor(ui: &mut egui::Ui, value: &mut Vec3, speed: f64) -> egui::Response {
    ui.horizontal(|ui| {
        ui.add(egui::DragValue::new(&mut value.x).speed(speed));
        ui.add(egui::DragValue::new(&mut value.y).speed(speed));
        ui.add(egui::DragValue::new(&mut value.z).speed(speed))
    })
    .response
}

#[derive(Debug, Clone, Copy)]
pub enum NavigationActorType {
    Bounds,
    Obstacle,
    Area,
    Link,
}

pub fn spawn_navigation_actor(world: &mut World, actor_type: NavigationActorType) -> Entity {
    let rect = world.resource::<crate::ui::UiState>().viewport_rect;
    let settings = world.resource::<ViewportSettings>().clone();
    let translation = snap_translation(editor_spawn_ground_position(world, rect), &settings);
    let (name, bounds, obstacle, area, link) = match actor_type {
        NavigationActorType::Bounds => (
            "Navigation Bounds",
            Some(NavigationBounds::default()),
            None,
            None,
            None,
        ),
        NavigationActorType::Obstacle => (
            "Navigation Obstacle",
            None,
            Some(NavigationObstacle::default()),
            None,
            None,
        ),
        NavigationActorType::Area => (
            "Navigation Area",
            None,
            None,
            Some(NavigationArea::default()),
            None,
        ),
        NavigationActorType::Link => (
            "Navigation Link",
            None,
            None,
            None,
            Some(NavigationLink::default()),
        ),
    };
    let mut snapshot =
        super::helpers::primitive_snapshot(name, shared::PrimitiveShape::Box, translation);
    snapshot.actor_kind = shared::ActorKind::Navigation;
    snapshot.primitive = None;
    snapshot.material = None;
    snapshot.physics = None;
    snapshot.collider = None;
    snapshot.rigid_body = None;
    snapshot.navigation_bounds = bounds;
    snapshot.navigation_obstacle = obstacle;
    snapshot.navigation_area = area;
    snapshot.navigation_link = link;
    crate::history::spawn_with_history(world, snapshot)
}

pub fn sync_navigation_state(world: &mut World) {
    let selected = world.resource::<crate::selection::SelectedEntity>().0;
    let Some(entity) = selected.filter(|entity| world.get::<NavigationBounds>(*entity).is_some())
    else {
        clear_artifact_selection(world);
        return;
    };
    let Some(bounds) = world.get::<NavigationBounds>(entity).cloned() else {
        return;
    };
    let delta = world.resource::<Time>().delta_secs();
    let poll_disk = {
        let mut state = world.resource_mut::<NavigationEditorState>();
        state.artifact_poll_elapsed += delta;
        state.artifact_poll_elapsed >= ARTIFACT_POLL_INTERVAL_SECS
    };
    let artifact_source_changed = refresh_artifact_source(world, entity, &bounds, poll_disk);
    let scene_revision = world.resource::<SceneIo>().change_revision();
    let refresh_input = {
        let mut state = world.resource_mut::<NavigationEditorState>();
        state.input_refresh_elapsed += delta;
        state.input_bounds_entity != Some(entity)
            || state.input_scene_revision != scene_revision
            || state.input_refresh_elapsed >= INPUT_REFRESH_INTERVAL_SECS
    };
    if refresh_input {
        let result = build_input(world, entity, &bounds);
        let mut state = world.resource_mut::<NavigationEditorState>();
        state.input_bounds_entity = Some(entity);
        state.input_scene_revision = scene_revision;
        state.input_refresh_elapsed = 0.0;
        match result {
            Ok(input) => {
                state.current_input = Some(input);
                state.current_input_error = None;
            }
            Err(error) => {
                state.current_input = None;
                state.current_input_error = Some(error);
            }
        }
    }
    let (input, input_error) = {
        let state = world.resource::<NavigationEditorState>();
        (
            state.current_input.clone(),
            state.current_input_error.clone(),
        )
    };
    let Some(input) = input else {
        let message = format!(
            "Navigation bake input is invalid: {}",
            input_error.unwrap_or_else(|| "resolver did not produce an input".into())
        );
        if world.resource::<NavigationEditorState>().status != message {
            set_status(world, message, true);
        }
        return;
    };
    {
        let mut state = world.resource_mut::<NavigationEditorState>();
        if state.error
            && state
                .status
                .starts_with("Navigation bake input is invalid:")
        {
            state.error = false;
            state.status = "Navigation input resolved".into();
        }
    }
    let fingerprint = navigation_source_fingerprint(&input);
    let mut should_bake = false;
    {
        let mut state = world.resource_mut::<NavigationEditorState>();
        if state.observed_fingerprint.as_deref() != Some(&fingerprint) {
            state.observed_fingerprint = Some(fingerprint.clone());
            state.auto_bake_attempt = None;
            state.debounce_elapsed = 0.0;
        }
        let stale = state
            .artifact
            .as_ref()
            .is_none_or(|artifact| artifact.is_stale_for(&input));
        if stale {
            state.debounce_elapsed += delta;
            if !state.error {
                state.status = "Authored navigation changed; rebake required".into();
            }
            should_bake = bounds.auto_bake
                && state.debounce_elapsed >= 0.75
                && state.auto_bake_attempt.as_deref() != Some(&fingerprint);
            if should_bake {
                state.auto_bake_attempt = Some(fingerprint.clone());
            }
        } else if artifact_source_changed
            || state.status == "Not baked"
            || state.status.starts_with("Loaded: ")
            || state.status == "Navigation input resolved"
            || state.status == "Authored navigation changed; rebake required"
        {
            state.status = format!("Current: {}", bounds.artifact_path);
            state.error = false;
        }
    }
    if should_bake {
        bake_bounds(world, entity);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::selection::SelectedEntity;
    use scene::navigation::NavigationGeometryInput;

    #[test]
    fn world_affine_uses_current_authored_parent_chain() {
        let mut world = World::new();
        let parent = world.spawn(Transform::from_xyz(4.0, 0.0, 0.0)).id();
        let child = world
            .spawn((Transform::from_xyz(0.0, 0.0, 2.0), ChildOf(parent)))
            .id();

        world
            .entity_mut(child)
            .insert(Transform::from_xyz(0.0, 0.0, 3.0));

        assert_eq!(
            Vec3::from(world_affine(&world, child).translation),
            Vec3::new(4.0, 0.0, 3.0)
        );
    }

    #[test]
    fn world_aabb_half_extents_include_rotation_and_nonuniform_scale() {
        let transform =
            Transform::from_rotation(Quat::from_rotation_y(std::f32::consts::FRAC_PI_2))
                .with_scale(Vec3::new(2.0, 3.0, 4.0));

        let half_extents = world_aabb_half_extents(
            GlobalTransform::from(transform).affine(),
            Vec3::new(1.0, 2.0, 3.0),
        );

        assert!(half_extents.abs_diff_eq(Vec3::new(12.0, 6.0, 2.0), 0.0001));
    }

    #[test]
    fn world_affine_preserves_parent_induced_shear() {
        let mut world = World::new();
        let parent = world
            .spawn(Transform::from_scale(Vec3::new(2.0, 1.0, 0.5)))
            .id();
        let child = world
            .spawn((
                Transform::from_rotation(Quat::from_rotation_y(0.7)),
                ChildOf(parent),
            ))
            .id();
        let affine = world_affine(&world, child);
        let point = Vec3::new(1.0, 0.0, 1.0);
        let exact = affine.transform_point3(point);
        let decomposed = GlobalTransform::from(Transform::from_scale(Vec3::new(2.0, 1.0, 0.5)))
            .mul_transform(Transform::from_rotation(Quat::from_rotation_y(0.7)))
            .compute_transform()
            .transform_point(point);

        assert!(exact.distance(decomposed) > 0.1);
    }

    #[test]
    fn artifact_source_refresh_invalidates_external_changes_and_selection() {
        let root = std::env::temp_dir().join(format!(
            "blacksite-navigation-artifact-watch-{}",
            uuid::Uuid::new_v4()
        ));
        let bounds = NavigationBounds {
            artifact_path: "assets/navigation/generated/watched.nav.ron".into(),
            ..Default::default()
        };
        let watched = root.join(&bounds.artifact_path);
        std::fs::create_dir_all(watched.parent().expect("artifact should have a parent"))
            .expect("temporary artifact directory should be created");
        let input = NavigationBakeInput {
            source_scene: "assets/levels/watch-test.scn.ron".into(),
            bounds_actor_id: "watch-bounds".into(),
            center: [0.0, 0.0, 0.0],
            half_extents: [3.0, 2.0, 3.0],
            agent: shared::NavigationAgentProfile {
                min_region_size: 1,
                merge_region_size: 2,
                ..Default::default()
            },
            geometry: vec![NavigationGeometryInput {
                actor_id: "floor".into(),
                triangles: vec![
                    [[-3.0, 0.0, -3.0], [3.0, 0.0, 3.0], [3.0, 0.0, -3.0]],
                    [[-3.0, 0.0, -3.0], [-3.0, 0.0, 3.0], [3.0, 0.0, 3.0]],
                ],
            }],
            obstacles: Vec::new(),
            areas: Vec::new(),
            links: Vec::new(),
            samples: Vec::new(),
        };
        let baked = bake_navigation(&input).expect("test navigation should bake");
        write_navigation_artifact(&watched, &baked).expect("test artifact should be written");

        let workspace = ProjectWorkspace {
            root: root.to_string_lossy().into_owned(),
            ..Default::default()
        };
        let mut world = World::new();
        world.insert_resource(workspace);
        world.insert_resource(NavigationEditorState::default());
        let first_entity = world.spawn_empty().id();

        assert!(refresh_artifact_source(
            &mut world,
            first_entity,
            &bounds,
            false
        ));
        let first_revision = world
            .resource::<NavigationEditorState>()
            .artifact_disk_revision
            .clone();
        assert!(world.resource::<NavigationEditorState>().artifact.is_some());

        world
            .resource_mut::<NavigationEditorState>()
            .preview_path
            .push(Vec3::ONE);
        std::fs::remove_file(&watched).expect("watched artifact should be removed");
        assert!(refresh_artifact_source(
            &mut world,
            first_entity,
            &bounds,
            true
        ));
        let state = world.resource::<NavigationEditorState>();
        assert!(state.artifact.is_none());
        assert!(state.preview_path.is_empty());
        assert!(state.status.contains("Artifact missing"));

        let mut external_input = input.clone();
        external_input.source_scene = "assets/levels/external-rebake.scn.ron".into();
        let externally_rebaked =
            bake_navigation(&external_input).expect("external navigation should rebake");
        write_navigation_artifact(&watched, &externally_rebaked)
            .expect("external rebake should be written");
        assert!(refresh_artifact_source(
            &mut world,
            first_entity,
            &bounds,
            true
        ));
        let state = world.resource::<NavigationEditorState>();
        assert_ne!(state.artifact_disk_revision, first_revision);
        assert_eq!(
            state
                .artifact
                .as_ref()
                .map(|artifact| artifact.source_scene.as_str()),
            Some("assets/levels/external-rebake.scn.ron")
        );

        let mut second_bounds = bounds.clone();
        second_bounds.artifact_path = "assets/navigation/generated/second.nav.ron".into();
        let second_path = root.join(&second_bounds.artifact_path);
        write_navigation_artifact(&second_path, &baked).expect("second artifact should be written");
        assert!(refresh_artifact_source(
            &mut world,
            first_entity,
            &second_bounds,
            false
        ));
        let state = world.resource::<NavigationEditorState>();
        assert_eq!(
            state.artifact_authored_path.as_deref(),
            Some(second_bounds.artifact_path.as_str())
        );
        assert_ne!(
            state
                .artifact
                .as_ref()
                .map(|artifact| artifact.source_scene.as_str()),
            Some("assets/levels/external-rebake.scn.ron")
        );

        let second_entity = world.spawn_empty().id();
        assert!(refresh_artifact_source(
            &mut world,
            second_entity,
            &second_bounds,
            false
        ));
        assert_eq!(
            world.resource::<NavigationEditorState>().bounds_entity,
            Some(second_entity)
        );

        world.insert_resource(SelectedEntity(None));
        sync_navigation_state(&mut world);
        let state = world.resource::<NavigationEditorState>();
        assert!(state.bounds_entity.is_none());
        assert!(state.artifact.is_none());

        std::fs::remove_dir_all(root).expect("temporary project should be removed");
    }
}
