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

use serde_json::Value;

use super::{
    node_types::{
        AdrRaw, ContentKind, GraphMeta, GraphNode, OntologyEdgeRaw, OntologyInstance,
        OntologyNodeRaw,
    },
    BuildError,
};

// ── NCL export
// ────────────────────────────────────────────────────────────────

/// Run `nickel export --format json` with the given import paths.
/// Returns the parsed JSON value.
fn export_ncl(file: &Path, import_paths: &[&Path]) -> Result<Value, BuildError> {
    let mut cmd = Command::new("nickel");
    cmd.arg("export").arg("--format").arg("json");

    for p in import_paths {
        cmd.arg("--import-path").arg(p);
    }
    cmd.arg(file);

    let out = cmd
        .output()
        .map_err(|e| BuildError::NiclCommand(e.to_string()))?;

    if !out.status.success() {
        return Err(BuildError::NclExport {
            path: file.display().to_string(),
            stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
        });
    }

    serde_json::from_slice(&out.stdout).map_err(|e| BuildError::NclParse(e.to_string()))
}

// ── Content NCL parsing
// ───────────────────────────────────────────────────────

/// Configuration for parsing content NCL files.
pub struct ContentParserConfig {
    /// Root of the site's nickel schema directory (contains content/metadata/).
    /// Used as --import-path for nickel export on content files.
    pub nickel_schema_dir: PathBuf,
}

/// Parse a single content `.ncl` file and extract graph metadata + node info.
/// Returns `None` if the file is not published or is a draft.
pub fn parse_content_ncl(
    file: &Path,
    config: &ContentParserConfig,
) -> Result<Option<(GraphNode, GraphMeta)>, BuildError> {
    // Emit rerun directive for incremental builds
    println!("cargo:rerun-if-changed={}", file.display());

    let import_paths = [
        config.nickel_schema_dir.as_path(),
        file.parent().unwrap_or(Path::new(".")),
    ];

    let value = export_ncl(file, &import_paths)?;

    // Skip unpublished and draft content
    if value.get("published").and_then(Value::as_bool) == Some(false) {
        return Ok(None);
    }
    if value.get("draft").and_then(Value::as_bool) == Some(true) {
        return Ok(None);
    }

    let graph_meta = extract_graph_meta(&value);
    let node = build_content_node(file, &value)?;

    Ok(Some((node, graph_meta)))
}

fn extract_graph_meta(value: &Value) -> GraphMeta {
    let empty = Value::Object(Default::default());
    let graph = value.get("graph").unwrap_or(&empty);

    GraphMeta {
        implements: str_array(graph, "implements"),
        related_to: str_array(graph, "related_to"),
        part_of: str_array(graph, "part_of"),
        references: str_array(graph, "references"),
        extends: str_array(graph, "extends"),
    }
}

fn build_content_node(file: &Path, value: &Value) -> Result<GraphNode, BuildError> {
    let (content_kind, lang, category) = infer_content_kind_and_lang(file);

    let id = value
        .get("id")
        .and_then(Value::as_str)
        .or_else(|| value.get("slug").and_then(Value::as_str))
        .unwrap_or_else(|| {
            file.file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown")
        })
        .to_owned();

    let title = value
        .get("title")
        .and_then(Value::as_str)
        .unwrap_or("Untitled")
        .to_owned();

    let slug = value
        .get("slug")
        .and_then(Value::as_str)
        .unwrap_or(&id)
        .to_owned();

    let page_route = value.get("page_route").and_then(Value::as_str);

    let url = if let Some(route) = page_route {
        route.to_owned()
    } else {
        build_content_url(&content_kind, &lang, category.as_deref(), &slug)
    };

    let tags = str_array(value, "tags");

    let excerpt = value
        .get("excerpt")
        .and_then(Value::as_str)
        .map(str::to_owned);

    Ok(GraphNode::Content {
        id,
        title,
        content_kind,
        lang,
        url,
        tags,
        excerpt,
    })
}

/// Infer content kind, language, and optional category from the file path.
///
/// Expected path segments (relative to site/content/):
///   blog/en/category/post.ncl        → Blog,     "en", Some("category")
///   recipes/es/category/recipe.ncl   → Recipe,   "es", Some("category")
///   projects/en/slug/index.ncl       → Project,  "en", None
///   activities/en/category/item.ncl  → Activity, "en", Some("category")
fn infer_content_kind_and_lang(file: &Path) -> (ContentKind, String, Option<String>) {
    let parts: Vec<&str> = file
        .components()
        .filter_map(|c| c.as_os_str().to_str())
        .collect();

    // Find "blog" | "recipes" | "projects" | "activities" in path
    let kind_idx = parts
        .iter()
        .position(|&s| matches!(s, "blog" | "recipes" | "projects" | "activities"));

    let Some(ki) = kind_idx else {
        return (ContentKind::Blog, "en".into(), None);
    };

    let kind = match parts[ki] {
        "recipes" => ContentKind::Recipe,
        "projects" => ContentKind::Project,
        "activities" => ContentKind::Activity,
        _ => ContentKind::Blog,
    };

    let lang = parts.get(ki + 1).copied().unwrap_or("en").to_owned();
    let category = if matches!(
        kind,
        ContentKind::Blog | ContentKind::Recipe | ContentKind::Activity
    ) {
        parts.get(ki + 2).map(|s| (*s).to_owned())
    } else {
        None
    };

    (kind, lang, category)
}

fn build_content_url(kind: &ContentKind, lang: &str, category: Option<&str>, slug: &str) -> String {
    let segment = kind.url_segment_for_lang(lang);
    match (kind, category) {
        (ContentKind::Project, _) => format!("/{segment}/{slug}"),
        (_, Some(cat)) => format!("/{segment}/{cat}/{slug}"),
        (_, None) => format!("/{segment}/{slug}"),
    }
}

// ── Ontology NCL parsing
// ──────────────────────────────────────────────────────

/// Configuration for parsing ontology NCL files.
pub struct OntologyParserConfig {
    /// Path to the ontoref ontology directory (contains defaults/core.ncl).
    /// Provided via `ONTOREF_NICKEL_IMPORT_PATH` environment variable.
    pub ontoref_import_path: PathBuf,
    pub instance: OntologyInstance,
}

/// Parse a `core.ncl` file and return (nodes, edges).
pub fn parse_ontology_core(
    file: &Path,
    config: &OntologyParserConfig,
) -> Result<(Vec<OntologyNodeRaw>, Vec<OntologyEdgeRaw>), BuildError> {
    println!("cargo:rerun-if-changed={}", file.display());

    // AN ONTOLOGY RESOLVES ITS IMPORTS AGAINST ITS OWN SPINE.
    //
    // `core.ncl` opens with `import "defaults/core.ncl"`, and that path is relative
    // to the SPINE (`.ontoref/`), not to `ontology/`. The only import paths
    // passed were a single global `ontoref_import_path` and the file's own
    // directory — so when two ontologies from DIFFERENT projects are parsed in
    // one run (the framework's and the implementation's, which is the whole
    // point of this parser), each was handed the OTHER project's spine and neither
    // could find its own defaults. One global import path cannot serve two
    // projects.
    //
    // The file's grandparent IS its spine: `<project>/.ontoref/ontology/core.ncl` →
    // `<project>/.ontoref`.
    let own_spine = file.parent().and_then(Path::parent);
    let mut import_paths: Vec<&Path> = vec![config.ontoref_import_path.as_path()];
    if let Some(sp) = own_spine {
        import_paths.push(sp);
    }
    if let Some(dir) = file.parent() {
        import_paths.push(dir);
    }

    let value = export_ncl(file, &import_paths)?;

    let nodes = value
        .get("nodes")
        .and_then(Value::as_array)
        .map(|arr| {
            arr.iter()
                .filter_map(|v| serde_json::from_value(v.clone()).ok())
                .collect()
        })
        .unwrap_or_default();

    let edges = value
        .get("edges")
        .and_then(Value::as_array)
        .map(|arr| {
            arr.iter()
                .filter_map(|v| serde_json::from_value(v.clone()).ok())
                .collect()
        })
        .unwrap_or_default();

    Ok((nodes, edges))
}

// ── ADR NCL parsing
// ───────────────────────────────────────────────────────────

/// Parse a single ADR `.ncl` file.
pub fn parse_adr(file: &Path, ontoref_import_path: &Path) -> Result<Option<AdrRaw>, BuildError> {
    println!("cargo:rerun-if-changed={}", file.display());

    let dir = file.parent().unwrap_or(Path::new("."));
    let import_paths = [ontoref_import_path, dir];

    let value = export_ncl(file, &import_paths)?;

    // Skip template files (id starts with "_" or is missing)
    let id = match value.get("id").and_then(Value::as_str) {
        Some(id) if !id.starts_with('_') => id.to_owned(),
        _ => return Ok(None),
    };

    let adr = AdrRaw {
        id,
        title: value
            .get("title")
            .and_then(Value::as_str)
            .unwrap_or("Untitled ADR")
            .to_owned(),
        status: value
            .get("status")
            .and_then(Value::as_str)
            .unwrap_or("Unknown")
            .to_owned(),
    };

    Ok(Some(adr))
}

// ── Conversion helpers (used by graph_generator)
// ──────────────────────────────

pub fn ontology_node_to_graph_node(raw: &OntologyNodeRaw, instance: OntologyInstance) -> GraphNode {
    GraphNode::Ontology {
        id: raw.id.clone(),
        name: raw.name.clone(),
        level: raw.level.clone(),
        pole: raw.pole.clone(),
        description: raw.description.clone(),
        instance,
        artifact_paths: raw.artifact_paths.clone(),
    }
}

pub fn adr_to_graph_node(raw: &AdrRaw, instance: OntologyInstance) -> GraphNode {
    GraphNode::Adr {
        id: raw.id.clone(),
        title: raw.title.clone(),
        status: raw.status.clone(),
        instance,
    }
}

// ── Helpers
// ───────────────────────────────────────────────────────────────────

fn str_array(value: &Value, key: &str) -> Vec<String> {
    value
        .get(key)
        .and_then(Value::as_array)
        .map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_owned))
                .collect()
        })
        .unwrap_or_default()
}
