use std::path::Path;

use eidos_kernel::schema::Node;
use serde::Serialize;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ProjectZone {
    Frontend,
    Backend,
    Shared,
    Test,
    Cli,
    Api,
    Db,
    Config,
}

impl ProjectZone {
    pub fn label(self) -> &'static str {
        match self {
            ProjectZone::Frontend => "frontend",
            ProjectZone::Backend => "backend",
            ProjectZone::Shared => "shared",
            ProjectZone::Test => "test",
            ProjectZone::Cli => "cli",
            ProjectZone::Api => "api",
            ProjectZone::Db => "db",
            ProjectZone::Config => "config",
        }
    }

    fn aliases(self) -> &'static [&'static str] {
        match self {
            ProjectZone::Frontend => &["frontend", "frontend zone"],
            ProjectZone::Backend => &["backend", "backend zone"],
            ProjectZone::Shared => &["shared", "shared zone"],
            ProjectZone::Test => &["test", "tests", "test zone"],
            ProjectZone::Cli => &["cli", "command line", "cli zone"],
            ProjectZone::Api => &["api", "api zone"],
            ProjectZone::Db => &["db", "database", "db zone"],
            ProjectZone::Config => &["config", "configuration", "config zone"],
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectZoneConfidence {
    Path,
    Extension,
    FrameworkEvidence,
    MixedAmbiguous,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ProjectZoneAssignment {
    pub zone: ProjectZone,
    pub confidence: ProjectZoneConfidence,
    pub source: String,
}

impl ProjectZoneAssignment {
    fn path(zone: ProjectZone, source: impl Into<String>) -> Self {
        Self {
            zone,
            confidence: ProjectZoneConfidence::Path,
            source: source.into(),
        }
    }

    fn extension(zone: ProjectZone, source: impl Into<String>) -> Self {
        Self {
            zone,
            confidence: ProjectZoneConfidence::Extension,
            source: source.into(),
        }
    }

    fn framework(zone: ProjectZone, source: impl Into<String>) -> Self {
        Self {
            zone,
            confidence: ProjectZoneConfidence::FrameworkEvidence,
            source: source.into(),
        }
    }

    fn mixed(zone: ProjectZone, source: impl Into<String>) -> Self {
        Self {
            zone,
            confidence: ProjectZoneConfidence::MixedAmbiguous,
            source: source.into(),
        }
    }
}

/// Add conservative project-zone aliases derived from paths plus deterministic framework aliases.
pub(crate) fn annotate_project_zones(nodes: &mut [Node], root: &Path) {
    for node in nodes {
        let zones = zone_assignments_for_node(node, root);
        let mut labels = zones
            .iter()
            .map(|assignment| assignment.zone)
            .collect::<Vec<_>>();
        labels.sort();
        labels.dedup();
        let existing_aliases = node.aliases.clone();
        for zone in labels {
            for alias in zone.aliases() {
                push_alias(&mut node.aliases, alias);
            }
            for alias in existing_aliases
                .iter()
                .filter(|alias| is_specific_alias(alias))
            {
                push_alias(&mut node.aliases, &format!("{} {}", zone.label(), alias));
            }
        }
        node.aliases.sort();
        node.aliases.dedup();
    }
}

pub fn zone_assignments_for_node(node: &Node, root: &Path) -> Vec<ProjectZoneAssignment> {
    let mut out = Vec::new();
    for source in &node.source_files {
        let path = Path::new(source);
        let rel = path.strip_prefix(root).unwrap_or(path);
        out.extend(zone_assignments_for_path(rel));
    }
    out.extend(zone_assignments_from_aliases(&node.aliases));
    dedupe_assignments(out)
}

pub fn zone_assignments_for_path(path: &Path) -> Vec<ProjectZoneAssignment> {
    let components: Vec<String> = path
        .components()
        .map(|c| c.as_os_str().to_string_lossy().to_ascii_lowercase())
        .collect();
    let file_stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();
    let file_name = path
        .file_name()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();
    let extension = path
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_ascii_lowercase();

    let has = |names: &[&str]| components.iter().any(|c| names.contains(&c.as_str()));
    let file_has_token = |names: &[&str]| {
        file_stem
            .split(['_', '-', '.'])
            .any(|token| names.contains(&token))
    };
    let has_any_substr = |needles: &[&str]| {
        components
            .iter()
            .any(|c| needles.iter().any(|n| c.contains(n)))
    };

    let mut zones = Vec::new();

    if has(&["test", "tests", "__tests__"]) || has_any_substr(&[".test", ".spec"]) {
        zones.push(ProjectZoneAssignment::path(ProjectZone::Test, "test path"));
    }
    if has(&["shared", "common", "lib"]) {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Shared,
            "shared/common/lib path",
        ));
    }
    if has(&["cli", "cmd", "commands", "command"]) {
        zones.push(ProjectZoneAssignment::path(ProjectZone::Cli, "cli path"));
    }
    if has(&[
        "db",
        "database",
        "migrations",
        "prisma",
        "model",
        "models",
        "schema",
        "schemas",
    ]) {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Db,
            "database/model/schema path",
        ));
    }
    if file_has_token(&[
        "model",
        "models",
        "schema",
        "schemas",
        "migration",
        "migrations",
    ]) {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Db,
            "database/model/schema file name",
        ));
    }
    if matches!(extension.as_str(), "sql" | "prisma") {
        zones.push(ProjectZoneAssignment::extension(
            ProjectZone::Db,
            format!(".{extension} file"),
        ));
    }
    if has(&["config", "configs", "configuration"])
        || file_stem.ends_with("config")
        || file_name.starts_with("config.")
        || matches!(file_stem.as_str(), "settings" | "env")
    {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Config,
            "config path/name",
        ));
    }

    let backendish = has(&[
        "backend", "server", "services", "service", "routes", "route",
    ]) || file_has_token(&["server", "services", "service", "routes", "route"]);
    if backendish {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Backend,
            "backend/server/route/service path",
        ));
    }
    if has(&["api", "routes", "route"]) || file_has_token(&["api", "routes", "route"]) {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Api,
            "api/route path",
        ));
    }

    let explicit_frontend = has(&[
        "frontend",
        "components",
        "component",
        "hooks",
        "hook",
        "pages",
        "views",
        "ui",
    ]);
    if explicit_frontend {
        zones.push(ProjectZoneAssignment::path(
            ProjectZone::Frontend,
            "frontend/ui/component path",
        ));
    }
    if extension == "tsx" && !backendish {
        zones.push(ProjectZoneAssignment::extension(
            ProjectZone::Frontend,
            ".tsx file",
        ));
    }

    dedupe_assignments(zones)
}

fn zone_assignments_from_aliases(aliases: &[String]) -> Vec<ProjectZoneAssignment> {
    let mut out = Vec::new();
    for alias in aliases.iter().map(|alias| alias.to_ascii_lowercase()) {
        if alias.contains("react component")
            || alias.contains("react hook")
            || alias.contains("react wrapper")
        {
            out.push(ProjectZoneAssignment::framework(
                ProjectZone::Frontend,
                alias.clone(),
            ));
        }
        if alias.contains("fastapi") || alias.contains("api route") {
            out.push(ProjectZoneAssignment::framework(
                ProjectZone::Backend,
                alias.clone(),
            ));
            out.push(ProjectZoneAssignment::framework(ProjectZone::Api, alias));
        }
    }
    dedupe_assignments(out)
}

fn dedupe_assignments(mut zones: Vec<ProjectZoneAssignment>) -> Vec<ProjectZoneAssignment> {
    zones.sort_by(|a, b| {
        (a.zone, confidence_rank(&a.confidence), a.source.as_str()).cmp(&(
            b.zone,
            confidence_rank(&b.confidence),
            b.source.as_str(),
        ))
    });
    zones.dedup_by(|a, b| a.zone == b.zone && a.confidence == b.confidence && a.source == b.source);

    let labels = zones.iter().map(|zone| zone.zone).collect::<Vec<_>>();
    if labels.contains(&ProjectZone::Frontend) && labels.contains(&ProjectZone::Backend) {
        zones.push(ProjectZoneAssignment::mixed(
            ProjectZone::Frontend,
            "frontend and backend evidence both present",
        ));
        zones.push(ProjectZoneAssignment::mixed(
            ProjectZone::Backend,
            "frontend and backend evidence both present",
        ));
    }
    zones
}

fn confidence_rank(confidence: &ProjectZoneConfidence) -> u8 {
    match confidence {
        ProjectZoneConfidence::Path => 0,
        ProjectZoneConfidence::Extension => 1,
        ProjectZoneConfidence::FrameworkEvidence => 2,
        ProjectZoneConfidence::MixedAmbiguous => 3,
    }
}

fn push_alias(aliases: &mut Vec<String>, alias: &str) {
    if !aliases.iter().any(|a| a == alias) {
        aliases.push(alias.to_string());
    }
}

fn is_specific_alias(alias: &str) -> bool {
    alias.contains(' ')
        && !alias.contains(" zone")
        && !matches!(
            alias,
            "exported function"
                | "exported class"
                | "exported interface"
                | "exported type"
                | "async function"
                | "route handler"
                | "command line"
        )
}

#[cfg(test)]
mod tests {
    use super::{ProjectZone, zone_assignments_for_path};
    use std::path::Path;

    fn has_zone(path: &str, zone: ProjectZone) -> bool {
        zone_assignments_for_path(Path::new(path))
            .iter()
            .any(|assignment| assignment.zone == zone)
    }

    #[test]
    fn backend_under_web_does_not_get_frontend_from_web_alone() {
        assert!(has_zone(
            "web/src/backend/useUserProfile.ts",
            ProjectZone::Backend
        ));
        assert!(!has_zone(
            "web/src/backend/useUserProfile.ts",
            ProjectZone::Frontend
        ));
    }

    #[test]
    fn component_under_web_is_frontend() {
        assert!(has_zone(
            "web/src/components/ProfileCard.tsx",
            ProjectZone::Frontend
        ));
    }

    #[test]
    fn model_and_schema_paths_are_db_zone() {
        assert!(has_zone("python/app/models/user.py", ProjectZone::Db));

        assert!(has_zone("web/src/schemas/profile.ts", ProjectZone::Db));
    }

    #[test]
    fn generic_app_path_is_not_frontend_without_evidence() {
        assert!(!has_zone(
            "python/app/services/users.py",
            ProjectZone::Frontend
        ));
    }
}
