use bevy::prelude::*;
use serde::{Deserialize, Serialize};

pub const NAVIGATION_GENERATED_ARTIFACT_DIRECTORY: &str = "assets/navigation/generated/";
pub const COMPONENT_NAVIGATION_BOUNDS: &str = "shared::navigation::NavigationBounds";
pub const COMPONENT_NAVIGATION_OBSTACLE: &str = "shared::navigation::NavigationObstacle";
pub const COMPONENT_NAVIGATION_AREA: &str = "shared::navigation::NavigationArea";
pub const COMPONENT_NAVIGATION_LINK: &str = "shared::navigation::NavigationLink";

/// Agent dimensions and deterministic Rerecast bake settings.
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationAgentProfile {
    pub id: String,
    pub radius: f32,
    pub height: f32,
    pub max_climb: f32,
    pub max_slope_deg: f32,
    pub cell_size_fraction: f32,
    pub cell_height_fraction: f32,
    pub min_region_size: u16,
    pub merge_region_size: u16,
}

impl Default for NavigationAgentProfile {
    fn default() -> Self {
        Self {
            id: "humanoid".into(),
            radius: 0.35,
            height: 1.8,
            max_climb: 0.45,
            max_slope_deg: 45.0,
            cell_size_fraction: 3.0,
            cell_height_fraction: 6.0,
            min_region_size: 4,
            merge_region_size: 12,
        }
    }
}

impl NavigationAgentProfile {
    pub fn validate(&self) -> Result<(), &'static str> {
        if self.id.trim().is_empty() {
            return Err("agent profile ID must not be empty");
        }
        if !self.radius.is_finite() || self.radius <= 0.0 {
            return Err("agent radius must be finite and greater than zero");
        }
        if !self.height.is_finite() || self.height <= self.radius * 2.0 {
            return Err("agent height must be finite and greater than twice its radius");
        }
        if !self.max_climb.is_finite() || self.max_climb < 0.0 || self.max_climb >= self.height {
            return Err("maximum climb must be finite, non-negative, and below agent height");
        }
        if !self.max_slope_deg.is_finite() || !(0.0..90.0).contains(&self.max_slope_deg) {
            return Err("maximum slope must be finite and in [0, 90) degrees");
        }
        if !self.cell_size_fraction.is_finite() || !(1.0..=32.0).contains(&self.cell_size_fraction)
        {
            return Err("cell size fraction must be finite and in [1, 32]");
        }
        if !self.cell_height_fraction.is_finite()
            || !(1.0..=64.0).contains(&self.cell_height_fraction)
        {
            return Err("cell height fraction must be finite and in [1, 64]");
        }
        let cell_height = self.radius / self.cell_height_fraction;
        if (self.height / cell_height).ceil() > f32::from(u16::MAX) {
            return Err("agent height requires more than 65535 vertical voxels");
        }
        if (self.max_climb / cell_height).floor() > f32::from(u16::MAX) {
            return Err("maximum climb requires more than 65535 vertical voxels");
        }
        if self.min_region_size > 255 || self.merge_region_size > 255 {
            return Err("navigation region sizes must not exceed 255 voxels");
        }
        Ok(())
    }
}

/// Axis-aligned world region baked for one agent profile.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationBounds {
    pub half_extents: Vec3,
    pub agent: NavigationAgentProfile,
    pub artifact_path: String,
    #[serde(default)]
    pub auto_bake: bool,
    #[serde(default)]
    pub validation_samples: Vec<NavigationPathSample>,
}

impl Default for NavigationBounds {
    fn default() -> Self {
        Self {
            half_extents: Vec3::new(20.0, 4.0, 20.0),
            agent: NavigationAgentProfile::default(),
            artifact_path: "assets/navigation/generated/main_humanoid.nav.ron".into(),
            auto_bake: false,
            validation_samples: Vec::new(),
        }
    }
}

/// Named local-space start/end pair persisted for bake and packaged-runtime validation.
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationPathSample {
    pub id: String,
    pub start: Vec3,
    pub end: Vec3,
    #[serde(default = "default_true")]
    pub enabled: bool,
}

impl Default for NavigationPathSample {
    fn default() -> Self {
        Self {
            id: "sample".into(),
            start: Vec3::new(-4.0, 0.0, 0.0),
            end: Vec3::new(4.0, 0.0, 0.0),
            enabled: true,
        }
    }
}

impl NavigationBounds {
    /// Creates bounds whose generated output is owned by one stable actor.
    pub fn for_actor(actor_id: &str) -> Self {
        Self {
            artifact_path: navigation_artifact_path_for_actor(actor_id),
            ..Self::default()
        }
    }
}

/// Returns a readable, collision-resistant artifact path derived from a persisted actor ID.
pub fn navigation_artifact_path_for_actor(actor_id: &str) -> String {
    let mut slug = String::with_capacity(32);
    let mut last_was_separator = false;
    for character in actor_id.trim().chars() {
        if slug.len() >= 32 {
            break;
        }
        let character = character.to_ascii_lowercase();
        if character.is_ascii_alphanumeric() || matches!(character, '-' | '_') {
            slug.push(character);
            last_was_separator = false;
        } else if !slug.is_empty() && !last_was_separator {
            slug.push('-');
            last_was_separator = true;
        }
    }
    while slug.ends_with('-') {
        slug.pop();
    }
    if slug.is_empty() {
        slug.push_str("actor");
    }

    let digest = blake3::hash(actor_id.as_bytes()).to_hex();
    format!(
        "{NAVIGATION_GENERATED_ARTIFACT_DIRECTORY}{slug}-{}.nav.ron",
        &digest[..24]
    )
}

/// Normalizes and validates a project-relative generated navigation artifact path.
pub fn navigation_generated_artifact_path(path: &str) -> Result<String, &'static str> {
    let normalized = path.replace('\\', "/");
    let relative = std::path::Path::new(&normalized);
    if normalized.trim().is_empty()
        || relative.is_absolute()
        || relative.components().any(|component| {
            matches!(
                component,
                std::path::Component::CurDir
                    | std::path::Component::ParentDir
                    | std::path::Component::RootDir
                    | std::path::Component::Prefix(_)
            )
        })
    {
        return Err("navigation artifacts must be project-relative files under assets/navigation/generated/");
    }
    let canonical = relative
        .components()
        .filter_map(|component| match component {
            std::path::Component::Normal(component) => component.to_str(),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/");
    if !canonical.starts_with(NAVIGATION_GENERATED_ARTIFACT_DIRECTORY)
        || canonical == NAVIGATION_GENERATED_ARTIFACT_DIRECTORY.trim_end_matches('/')
        || !canonical.ends_with(".nav.ron")
    {
        return Err(
            "navigation artifacts must be .nav.ron files under assets/navigation/generated/",
        );
    }
    Ok(canonical)
}

/// Axis-aligned volume carved from every overlapping navigation bake.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationObstacle {
    pub half_extents: Vec3,
    #[serde(default)]
    pub dynamic: bool,
}

impl Default for NavigationObstacle {
    fn default() -> Self {
        Self {
            half_extents: Vec3::splat(0.5),
            dynamic: false,
        }
    }
}

/// Named navigation area volume retained in the bake artifact.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationArea {
    pub id: String,
    pub half_extents: Vec3,
    pub cost: f32,
    #[serde(default = "default_true")]
    pub walkable: bool,
}

impl Default for NavigationArea {
    fn default() -> Self {
        Self {
            id: "default".into(),
            half_extents: Vec3::splat(1.0),
            cost: 1.0,
            walkable: true,
        }
    }
}

/// Explicit off-mesh connection. Endpoints are local to the owning actor.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct NavigationLink {
    pub start: Vec3,
    pub end: Vec3,
    #[serde(default = "default_true")]
    pub bidirectional: bool,
    #[serde(default = "default_one")]
    pub cost: f32,
    #[serde(default = "default_true")]
    pub enabled: bool,
}

impl Default for NavigationLink {
    fn default() -> Self {
        Self {
            start: Vec3::ZERO,
            end: Vec3::new(0.0, 0.0, 2.0),
            bidirectional: true,
            cost: 1.0,
            enabled: true,
        }
    }
}

fn default_true() -> bool {
    true
}

fn default_one() -> f32 {
    1.0
}

#[cfg(test)]
mod tests {
    use super::{
        navigation_artifact_path_for_actor, navigation_generated_artifact_path,
        NavigationAgentProfile, NAVIGATION_GENERATED_ARTIFACT_DIRECTORY,
    };

    #[test]
    fn default_agent_profile_is_valid() {
        assert_eq!(NavigationAgentProfile::default().validate(), Ok(()));
    }

    #[test]
    fn invalid_agent_dimensions_are_actionable() {
        let mut profile = NavigationAgentProfile::default();
        profile.height = profile.radius;
        assert_eq!(
            profile.validate(),
            Err("agent height must be finite and greater than twice its radius")
        );
    }

    #[test]
    fn pathological_voxel_settings_are_rejected() {
        let profile = NavigationAgentProfile {
            cell_size_fraction: 1_000_000.0,
            ..Default::default()
        };
        assert_eq!(
            profile.validate(),
            Err("cell size fraction must be finite and in [1, 32]")
        );

        let profile = NavigationAgentProfile {
            merge_region_size: 256,
            ..Default::default()
        };
        assert_eq!(
            profile.validate(),
            Err("navigation region sizes must not exceed 255 voxels")
        );
    }

    #[test]
    fn generated_artifact_paths_are_stable_and_actor_specific() {
        let first = navigation_artifact_path_for_actor("Bounds / Main");
        assert_eq!(first, navigation_artifact_path_for_actor("Bounds / Main"));
        assert_ne!(first, navigation_artifact_path_for_actor("Bounds / Other"));
        assert!(first.starts_with(NAVIGATION_GENERATED_ARTIFACT_DIRECTORY));
        assert!(first.ends_with(".nav.ron"));
    }

    #[test]
    fn generated_artifact_policy_rejects_escapes_and_other_directories() {
        assert_eq!(
            navigation_generated_artifact_path("assets\\navigation\\generated\\bounds.nav.ron"),
            Ok("assets/navigation/generated/bounds.nav.ron".into())
        );
        assert_eq!(
            navigation_generated_artifact_path(
                "assets/navigation/generated//nested///bounds.nav.ron"
            ),
            Ok("assets/navigation/generated/nested/bounds.nav.ron".into())
        );
        assert!(navigation_generated_artifact_path("assets/navigation/bounds.nav.ron").is_err());
        assert!(
            navigation_generated_artifact_path("assets/navigation/generated/bounds.ron").is_err()
        );
        assert!(navigation_generated_artifact_path(
            "assets/navigation/generated/../bounds.nav.ron"
        )
        .is_err());
        assert!(
            navigation_generated_artifact_path(NAVIGATION_GENERATED_ARTIFACT_DIRECTORY).is_err()
        );
    }
}
