use std::path::{Path, PathBuf};

use bevy::prelude::Vec3;
use scene::navigation::{NavigationBakeArtifact, NavigationQueryPath, NavigationQueryRuntime};

/// Loaded navigation artifact and its shared Polyanya query runtime.
#[derive(Debug, Clone)]
pub struct NavigationRuntime {
    query: NavigationQueryRuntime,
}

#[derive(Debug, Clone, PartialEq)]
pub struct NavigationPath {
    pub points: Vec<Vec3>,
    pub length: f32,
    pub used_link_actor_ids: Vec<String>,
}

impl NavigationPath {
    pub fn used_link_actor_id(&self) -> Option<&str> {
        self.used_link_actor_ids.first().map(String::as_str)
    }
}

impl From<NavigationQueryPath> for NavigationPath {
    fn from(path: NavigationQueryPath) -> Self {
        Self {
            points: path.points.into_iter().map(Vec3::from_array).collect(),
            length: path.length,
            used_link_actor_ids: path.used_link_actor_ids,
        }
    }
}

impl NavigationRuntime {
    pub fn from_artifact(artifact: NavigationBakeArtifact) -> Result<Self, String> {
        Ok(Self {
            query: NavigationQueryRuntime::from_artifact(artifact)?,
        })
    }

    pub fn load(path: &Path) -> Result<Self, String> {
        Ok(Self {
            query: NavigationQueryRuntime::load(path)?,
        })
    }

    pub fn artifact(&self) -> &NavigationBakeArtifact {
        self.query.artifact()
    }

    pub fn query(&self, start: Vec3, end: Vec3) -> Result<NavigationPath, String> {
        self.query
            .query(start.to_array(), end.to_array())
            .map(NavigationPath::from)
    }

    pub fn validate_links(&self) -> Vec<String> {
        self.query.validate_links()
    }
}

pub fn query_navigation_path(
    artifact_path: &Path,
    start: Vec3,
    end: Vec3,
) -> Result<NavigationPath, String> {
    NavigationRuntime::load(artifact_path)?.query(start, end)
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NavigationValidationSummary {
    pub artifact_path: PathBuf,
    pub enabled_link_count: usize,
    pub enabled_sample_count: usize,
}

pub fn validate_navigation_artifact(
    artifact_path: &Path,
) -> Result<NavigationValidationSummary, String> {
    let runtime = NavigationQueryRuntime::load(artifact_path)?;
    let mut findings = runtime.validate_links();
    findings.extend(runtime.validate_samples());
    if !findings.is_empty() {
        return Err(findings.join("\n"));
    }
    Ok(NavigationValidationSummary {
        artifact_path: artifact_path.to_path_buf(),
        enabled_link_count: runtime
            .artifact()
            .links
            .iter()
            .filter(|link| link.enabled)
            .count(),
        enabled_sample_count: runtime
            .artifact()
            .samples
            .iter()
            .filter(|sample| sample.enabled)
            .count(),
    })
}

pub fn navigation_validation_argument(args: &[String]) -> Result<Option<PathBuf>, String> {
    let Some(index) = args
        .iter()
        .position(|argument| argument == "--validate-navigation")
    else {
        return Ok(None);
    };
    if args.len() != 2 || index != 0 {
        return Err(
            "usage: game --validate-navigation <project-relative-or-absolute-artifact-path>".into(),
        );
    }
    let path = args[1].trim();
    if path.is_empty() {
        return Err("navigation validation artifact path must not be empty".into());
    }
    Ok(Some(PathBuf::from(path)))
}

#[cfg(test)]
mod tests {
    use scene::navigation::{
        bake_navigation, NavigationBakeInput, NavigationGeometryInput, NavigationLinkInput,
        NavigationVolumeInput,
    };
    use shared::NavigationAgentProfile;

    use super::*;

    fn floor_geometry(
        actor_id: &str,
        min_x: f32,
        max_x: f32,
        min_z: f32,
        max_z: f32,
        y: f32,
    ) -> NavigationGeometryInput {
        NavigationGeometryInput {
            actor_id: actor_id.into(),
            triangles: vec![
                [[min_x, y, min_z], [max_x, y, max_z], [max_x, y, min_z]],
                [[min_x, y, min_z], [min_x, y, max_z], [max_x, y, max_z]],
            ],
        }
    }

    fn input() -> NavigationBakeInput {
        NavigationBakeInput {
            source_scene: "assets/levels/navigation_showcase.scn.ron".into(),
            bounds_actor_id: "bounds".into(),
            center: [0.0, 0.0, 0.0],
            half_extents: [6.0, 2.0, 6.0],
            agent: NavigationAgentProfile {
                min_region_size: 1,
                merge_region_size: 2,
                ..Default::default()
            },
            geometry: vec![floor_geometry("floor", -6.0, 6.0, -6.0, 6.0, 0.0)],
            obstacles: vec![NavigationVolumeInput {
                actor_id: "wall".into(),
                center: [0.0, 0.5, 0.0],
                half_extents: [0.5, 1.0, 4.5],
            }],
            areas: Vec::new(),
            links: Vec::new(),
            samples: Vec::new(),
        }
    }

    #[test]
    fn runtime_query_routes_on_the_baked_mesh() {
        let runtime = NavigationRuntime::from_artifact(bake_navigation(&input()).unwrap()).unwrap();
        let path = runtime
            .query(Vec3::new(-4.0, 0.0, -5.0), Vec3::new(4.0, 0.0, -5.0))
            .unwrap();
        assert!(path.length >= 8.0);
        assert!(path.points.len() >= 2);
    }

    #[test]
    fn explicit_link_can_connect_isolated_regions() {
        let mut input = input();
        input.obstacles[0].half_extents[2] = 6.0;
        input.links.push(NavigationLinkInput {
            actor_id: "door-link".into(),
            start: [-1.0, 0.0, 0.0],
            end: [1.0, 0.0, 0.0],
            bidirectional: true,
            cost: 1.0,
            enabled: true,
        });
        let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
        let path = runtime
            .query(Vec3::new(-4.0, 0.0, 0.0), Vec3::new(4.0, 0.0, 0.0))
            .unwrap();
        assert_eq!(path.used_link_actor_id(), Some("door-link"));
        assert_eq!(path.used_link_actor_ids, ["door-link"]);
    }

    #[test]
    fn route_can_traverse_multiple_explicit_links() {
        let mut input = input();
        input.geometry = vec![
            floor_geometry("island-a", -6.0, -3.0, -2.0, 2.0, 0.0),
            floor_geometry("island-b", -1.5, 1.5, -2.0, 2.0, 0.0),
            floor_geometry("island-c", 3.0, 6.0, -2.0, 2.0, 0.0),
        ];
        input.obstacles.clear();
        input.links = vec![
            NavigationLinkInput {
                actor_id: "link-a-b".into(),
                start: [-3.6, 0.0, 0.0],
                end: [-1.0, 0.0, 0.0],
                bidirectional: true,
                cost: 1.0,
                enabled: true,
            },
            NavigationLinkInput {
                actor_id: "link-b-c".into(),
                start: [1.0, 0.0, 0.0],
                end: [3.6, 0.0, 0.0],
                bidirectional: true,
                cost: 1.0,
                enabled: true,
            },
        ];
        let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
        let path = runtime
            .query(Vec3::new(-5.0, 0.0, 0.0), Vec3::new(5.0, 0.0, 0.0))
            .unwrap();
        assert_eq!(path.used_link_actor_ids, ["link-a-b", "link-b-c"]);
        assert!(path.length >= 10.0);
    }

    #[test]
    fn query_selects_vertically_nearest_walkable_surface() {
        let mut input = input();
        input.center = [0.0, 2.0, 0.0];
        input.half_extents = [6.0, 3.0, 6.0];
        input.geometry = vec![
            floor_geometry("lower-floor", -5.0, 5.0, -5.0, 5.0, 0.0),
            floor_geometry("upper-floor", -5.0, 5.0, -5.0, 5.0, 4.0),
        ];
        input.obstacles.clear();
        let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();

        let lower = runtime
            .query(Vec3::new(-3.0, 0.1, 0.0), Vec3::new(3.0, 0.1, 0.0))
            .unwrap();
        let upper = runtime
            .query(Vec3::new(-3.0, 3.9, 0.0), Vec3::new(3.0, 3.9, 0.0))
            .unwrap();
        assert!(lower.points.iter().all(|point| point.y < 1.0));
        assert!(upper.points.iter().all(|point| point.y > 3.0));
    }

    #[test]
    fn invalid_link_endpoint_is_reported_with_actor_identity() {
        let mut input = input();
        input.links.push(NavigationLinkInput {
            actor_id: "bad-link".into(),
            start: [100.0, 0.0, 100.0],
            end: [0.0, 0.0, 0.0],
            bidirectional: false,
            cost: 1.0,
            enabled: true,
        });
        let runtime = NavigationRuntime::from_artifact(bake_navigation(&input).unwrap()).unwrap();
        assert!(runtime
            .validate_links()
            .iter()
            .any(|finding| finding.contains("bad-link start")));
    }

    #[test]
    fn validation_cli_argument_is_strict_and_window_free() {
        assert_eq!(
            navigation_validation_argument(&[
                "--validate-navigation".into(),
                "assets/navigation/generated/main.nav.ron".into(),
            ])
            .unwrap(),
            Some(PathBuf::from("assets/navigation/generated/main.nav.ron"))
        );
        assert!(navigation_validation_argument(&["--validate-navigation".into()]).is_err());
        assert_eq!(
            navigation_validation_argument(&["--project".into(), ".".into()]).unwrap(),
            None
        );
    }
}
