use std::collections::HashMap;
use std::path::Path;

use super::{
    node_types::{ContentGraph, EdgeKind, GraphNode, OntologyLevel},
    BuildError,
};

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

/// Derives a site URL from an ontology node `artifact_paths` entry.
///
/// Expects paths of the form `site/content/{type}/en/{category}/` and returns
/// `/{type}/{category}`. Returns `None` for project/non-content paths.
fn ontology_artifact_to_url(path: &str) -> Option<String> {
    let rest = path.strip_prefix("site/content/")?;
    let segs: Vec<&str> = rest.split('/').filter(|s| !s.is_empty()).collect();
    // Require at least [content_type, lang, category]
    if segs.len() < 3 {
        return None;
    }
    if !["en", "es"].contains(&segs[1]) {
        return None;
    }
    // Drop the language segment; join remaining with content_type prefix.
    let mut parts = vec![segs[0]];
    parts.extend_from_slice(&segs[2..]);
    Some(format!("/{}", parts.join("/")))
}

// ── Public entry point
// ────────────────────────────────────────────────────────

/// Write all generated artefacts to `out_dir`.
///
/// Outputs:
/// - `content_graph.json`      — full serialised graph for Cytoscape
/// - `related_map.rs`          — static map node_id → related nodes
///   (RelatedContent, no JS)
/// - `ontology_context_map.rs` — static map node_id → connected ontology nodes
/// - `graph_mini_map.rs`       — static map node_id → ego SVG string
///   (GraphMini, SSR-safe)
pub fn write_outputs(graph: &ContentGraph, out_dir: &Path) -> Result<(), BuildError> {
    write_graph_json(graph, out_dir)?;
    write_related_map(graph, out_dir)?;
    write_ontology_context_map(graph, out_dir)?;
    write_graph_mini_map(graph, out_dir)?;
    Ok(())
}

// ── content_graph.json
// ────────────────────────────────────────────────────────

fn write_graph_json(graph: &ContentGraph, out_dir: &Path) -> Result<(), BuildError> {
    let json = serde_json::to_string_pretty(graph)?;
    std::fs::write(out_dir.join("content_graph.json"), json)?;
    Ok(())
}

// ── related_map.rs
// ────────────────────────────────────────────────────────────

/// Generates a phf-style static map:
/// ```rust
/// pub static RELATED_NODES: &[(&str, &[RelatedNode])] = &[...];
/// ```
/// Used by `RelatedContent` component — no JS, fully SSR-safe.
fn write_related_map(graph: &ContentGraph, out_dir: &Path) -> Result<(), BuildError> {
    // Build adjacency: node_id → [(related_id, kind_label)]
    // Use a HashSet keyed by (related_id, kind_label) to prevent duplicate entries
    // from forward + autobacklink edge pairs hitting the same (from, to) pair.
    let mut adjacency: HashMap<String, std::collections::HashSet<(String, String)>> =
        HashMap::new();

    for edge in &graph.edges {
        // Skip SharedTag (too noisy) and RelatedFrom (autobacklink of RelatedTo —
        // the symmetric clause below already mirrors it into the target's sidebar).
        if matches!(edge.kind, EdgeKind::SharedTag | EdgeKind::RelatedFrom) {
            continue;
        }

        let kind_label = format!("{:?}", edge.kind);

        adjacency
            .entry(edge.from.clone())
            .or_default()
            .insert((edge.to.clone(), kind_label.clone()));

        // RelatedTo is symmetric for display — show in both nodes' sidebars.
        if matches!(edge.kind, EdgeKind::RelatedTo) {
            adjacency
                .entry(edge.to.clone())
                .or_default()
                .insert((edge.from.clone(), kind_label));
        }
    }

    // Convert sets to sorted vecs for deterministic output.
    let adjacency: HashMap<String, Vec<(String, String)>> = adjacency
        .into_iter()
        .map(|(k, set)| {
            let mut v: Vec<_> = set.into_iter().collect();
            v.sort();
            (k, v)
        })
        .collect();

    // Build node title lookup
    let titles: HashMap<&str, &str> = graph.nodes.iter().map(|n| (n.id(), n.title())).collect();

    // Build node URL lookup — content nodes use their stored URL; Practice-level
    // ontology nodes derive their URL from the first artifact_path if it points
    // to a content directory (site/content/{type}/en/{category}/).
    let node_urls: HashMap<&str, String> = graph
        .nodes
        .iter()
        .filter_map(|n| match n {
            GraphNode::Content { id, url, .. } => Some((id.as_str(), url.clone())),
            GraphNode::Ontology {
                id,
                level: OntologyLevel::Practice,
                artifact_paths,
                ..
            } => {
                let url = artifact_paths
                    .first()
                    .and_then(|p| ontology_artifact_to_url(p))?;
                Some((id.as_str(), url))
            }
            _ => None,
        })
        .collect();

    // Slug → id aliases: content nodes whose URL slug differs from their id.
    // PostViewer passes the URL slug as node_id, so the map must also answer to it.
    let slug_to_id: HashMap<String, String> = graph
        .nodes
        .iter()
        .filter_map(|n| {
            let slug = n.url_slug()?;
            if slug != n.id() {
                Some((slug.to_owned(), n.id().to_owned()))
            } else {
                None
            }
        })
        .collect();

    let make_entry = |node_id: &str, related: &Vec<(String, String)>| -> String {
        let related_entries: Vec<String> = related.iter().map(|(rid, kind)| {
            let title = titles.get(rid.as_str()).copied().unwrap_or(rid.as_str());
            let url   = node_urls.get(rid.as_str()).map(|s| s.as_str()).unwrap_or("");
            format!(
                r#"        RelatedNode {{ id: {id:?}, title: {title:?}, kind: {kind:?}, url: {url:?} }}"#,
                id    = rid,
                title = title,
                kind  = kind,
                url   = url,
            )
        }).collect();
        format!(
            "    ({node_id:?}, &[\n{entries}\n    ]),",
            node_id = node_id,
            entries = related_entries.join(",\n"),
        )
    };

    let mut entries: Vec<String> = adjacency
        .iter()
        .map(|(node_id, related)| make_entry(node_id, related))
        .collect();

    // Emit slug-aliased entries for content nodes where URL slug != id
    for (slug, id) in &slug_to_id {
        if let Some(related) = adjacency.get(id) {
            entries.push(make_entry(slug, related));
        }
    }

    entries.sort(); // deterministic output for incremental builds

    let code = format!(
        r#"// Generated by content-graph feature — do not edit.
pub struct RelatedNode {{
    pub id:    &'static str,
    pub title: &'static str,
    pub kind:  &'static str,
    pub url:   &'static str,
}}

pub static RELATED_NODES: &[(&'static str, &'static [RelatedNode])] = &[
{entries}
];
"#,
        entries = entries.join("\n"),
    );

    std::fs::write(out_dir.join("related_map.rs"), code)?;
    Ok(())
}

// ── ontology_context_map.rs
// ───────────────────────────────────────────────────

/// Generates:
/// ```rust
/// pub static ONTOLOGY_CONTEXT: &[(&str, &[OntologyRef])] = &[...];
/// ```
/// Used by `OntologyContext` badge component.
fn write_ontology_context_map(graph: &ContentGraph, out_dir: &Path) -> Result<(), BuildError> {
    // Find all ontology nodes connected to content nodes
    let ontology_ids: std::collections::HashSet<String> = graph
        .nodes
        .iter()
        .filter_map(|n| match n {
            GraphNode::Ontology { id, .. } | GraphNode::Adr { id, .. } => Some(id.clone()),
            _ => None,
        })
        .collect();

    let ontology_names: HashMap<&str, &str> = graph
        .nodes
        .iter()
        .filter_map(|n| match n {
            GraphNode::Ontology { id, name, .. } => Some((id.as_str(), name.as_str())),
            GraphNode::Adr { id, title, .. } => Some((id.as_str(), title.as_str())),
            _ => None,
        })
        .collect();

    // content_id → [(ontology_id, ontology_name, edge_kind)]
    // Only process content→ontology direction (edge.to ∈ ontology_ids).
    // Processing edge.from as well would double-add entries because every
    // Implements edge has an ImplementedBy autobacklink pointing the other way.
    let mut ctx_map: HashMap<String, std::collections::HashSet<(String, String, String)>> =
        HashMap::new();

    for edge in &graph.edges {
        if ontology_ids.contains(&edge.from) {
            continue;
        } // skip ontology-origin edges
        if ontology_ids.contains(&edge.to) {
            let name = ontology_names
                .get(edge.to.as_str())
                .copied()
                .unwrap_or(&edge.to);
            ctx_map.entry(edge.from.clone()).or_default().insert((
                edge.to.clone(),
                name.to_owned(),
                format!("{:?}", edge.kind),
            ));
        }
    }

    // Convert sets to sorted vecs.
    let ctx_map: HashMap<String, Vec<(String, String, String)>> = ctx_map
        .into_iter()
        .map(|(k, set)| {
            let mut v: Vec<_> = set.into_iter().collect();
            v.sort();
            (k, v)
        })
        .collect();

    // Slug → id aliases (same rationale as write_related_map)
    let slug_to_id: HashMap<String, String> = graph
        .nodes
        .iter()
        .filter_map(|n| {
            let slug = n.url_slug()?;
            if slug != n.id() {
                Some((slug.to_owned(), n.id().to_owned()))
            } else {
                None
            }
        })
        .collect();

    let make_entry = |content_id: &str, refs: &Vec<(String, String, String)>| -> String {
        let ref_entries: Vec<String> = refs
            .iter()
            .map(|(oid, oname, kind)| {
                format!(
                    r#"        OntologyRef {{ id: {oid:?}, name: {oname:?}, kind: {kind:?} }}"#,
                    oid = oid,
                    oname = oname,
                    kind = kind,
                )
            })
            .collect();
        format!(
            "    ({content_id:?}, &[\n{entries}\n    ]),",
            content_id = content_id,
            entries = ref_entries.join(",\n"),
        )
    };

    let mut entries: Vec<String> = ctx_map
        .iter()
        .map(|(content_id, refs)| make_entry(content_id, refs))
        .collect();

    for (slug, id) in &slug_to_id {
        if let Some(refs) = ctx_map.get(id) {
            entries.push(make_entry(slug, refs));
        }
    }

    entries.sort();

    let code = format!(
        r#"// Generated by content-graph feature — do not edit.
pub struct OntologyRef {{
    pub id:   &'static str,
    pub name: &'static str,
    pub kind: &'static str,
}}

pub static ONTOLOGY_CONTEXT: &[(&'static str, &'static [OntologyRef])] = &[
{entries}
];
"#,
        entries = entries.join("\n"),
    );

    std::fs::write(out_dir.join("ontology_context_map.rs"), code)?;
    Ok(())
}

// ── Ego-network SVGs
// ──────────────────────────────────────────────────────────

const MINI_SVG_STYLES: &str = r#"
  .graph-node          { fill: var(--cg-node, #6b7280); stroke: none; }
  .graph-node.ego      { fill: var(--cg-ego,  #3b82f6); }
  .graph-node.ontology { fill: var(--cg-onto, #8b5cf6); }
  .graph-node.adr      { fill: var(--cg-adr,  #f59e0b); }
  .graph-edge          { stroke: var(--cg-edge, #d1d5db); stroke-width: 1.5; }
  .graph-label         { font: 10px sans-serif; fill: var(--cg-label, #374151); text-anchor: middle; }
  .graph-label.ego     { font-weight: 600; }
"#;

/// Generates `graph_mini_map.rs` with all ego-network SVGs inlined:
/// ```rust
/// pub static GRAPH_MINI_SVGS: &[(&str, &str)] = &[
///     ("rustelo", r#"<svg ...>...</svg>"#),
///     ...
/// ];
/// ```
fn write_graph_mini_map(graph: &ContentGraph, out_dir: &Path) -> Result<(), BuildError> {
    // HashSet per node prevents duplicate neighbors from forward + backlink edge
    // pairs (e.g. Implements + ImplementedBy both connect content ↔ ontology
    // node).
    let adjacency: HashMap<String, Vec<String>> = {
        let mut m: HashMap<String, std::collections::HashSet<String>> = HashMap::new();
        for edge in &graph.edges {
            if matches!(edge.kind, EdgeKind::SharedTag) {
                continue;
            }
            m.entry(edge.from.clone())
                .or_default()
                .insert(edge.to.clone());
            m.entry(edge.to.clone())
                .or_default()
                .insert(edge.from.clone());
        }
        m.into_iter()
            .map(|(k, set)| (k, set.into_iter().collect()))
            .collect()
    };

    let node_lookup: HashMap<&str, &GraphNode> = graph.nodes.iter().map(|n| (n.id(), n)).collect();

    let mut entries: Vec<String> = Vec::new();

    for node in &graph.nodes {
        if !matches!(node, GraphNode::Content { .. }) {
            continue;
        }

        let neighbors: Vec<&GraphNode> = adjacency
            .get(node.id())
            .map(|ids| {
                ids.iter()
                    .filter_map(|id| node_lookup.get(id.as_str()).copied())
                    .collect()
            })
            .unwrap_or_default();

        // Isolated nodes carry no graph information — skip them so GraphMini
        // renders nothing rather than showing a single orphan circle.
        if neighbors.is_empty() {
            continue;
        }

        let svg = render_ego_svg(node, &neighbors);
        // Escape any `#` sequences that would break raw string literal
        let delim = pick_raw_delim(&svg);
        let hashes = "#".repeat(delim);
        entries.push(format!(
            "    ({id:?}, r{hashes}\"{svg}\"{hashes})",
            id = node.id(),
        ));

        // Also emit a slug-keyed alias when the URL slug differs from the id.
        // PostViewer identifies content by URL slug, not by the NCL id field.
        if let Some(slug) = node.url_slug() {
            if slug != node.id() {
                entries.push(format!(
                    "    ({slug:?}, r{hashes}\"{svg}\"{hashes})",
                    slug = slug,
                ));
            }
        }
    }

    entries.sort();

    let code = format!(
        "// Generated by content-graph feature — do not edit.\npub static GRAPH_MINI_SVGS: \
         &[(&'static str, &'static str)] = &[\n{entries}\n];\n",
        entries = entries.join(",\n"),
    );

    std::fs::write(out_dir.join("graph_mini_map.rs"), code)?;
    Ok(())
}

/// Find the minimum number of `#` needed for a raw string literal containing
/// `svg`.
fn pick_raw_delim(s: &str) -> usize {
    let mut n = 0usize;
    loop {
        let needle = format!("\"{}", "#".repeat(n));
        if !s.contains(&needle) {
            return n;
        }
        n += 1;
    }
}

fn render_ego_svg(ego: &GraphNode, neighbors: &[&GraphNode]) -> String {
    let (cx, cy) = (180.0_f32, 180.0_f32);
    let radius = 120.0_f32;
    let n = neighbors.len().max(1) as f32;

    // Start angle offset so first node lands at top rather than right,
    // giving a more balanced layout on landscape viewports.
    let start_angle = -std::f32::consts::FRAC_PI_2;

    let lines: String = neighbors.iter().enumerate().map(|(i, _)| {
        let angle = start_angle + (i as f32 / n) * std::f32::consts::TAU;
        let nx = cx + radius * angle.cos();
        let ny = cy + radius * angle.sin();
        format!(r#"  <line x1="{cx:.1}" y1="{cy:.1}" x2="{nx:.1}" y2="{ny:.1}" class="graph-edge"/>"#)
    }).collect::<Vec<_>>().join("\n");

    let neighbor_circles: String = neighbors
        .iter()
        .enumerate()
        .map(|(i, node)| {
            let angle = start_angle + (i as f32 / n) * std::f32::consts::TAU;
            let nx = cx + radius * angle.cos();
            let ny = cy + radius * angle.sin();
            let css = node_css_class(node);
            let label = truncate(node.title(), 22);
            // Place label below circle; shift up when in upper half to avoid clipping.
            let label_y = if angle.sin() < -0.3 {
                ny - 24.0
            } else {
                ny + 28.0
            };
            format!(
                r#"  <circle cx="{nx:.1}" cy="{ny:.1}" r="14" class="graph-node {css}"/>
  <text x="{nx:.1}" y="{label_y:.1}" class="graph-label">{label}</text>"#,
            )
        })
        .collect::<Vec<_>>()
        .join("\n");

    let ego_css = node_css_class(ego);
    let ego_label = truncate(ego.title(), 30);

    format!(
        r#"<svg viewBox="0 0 360 360" xmlns="http://www.w3.org/2000/svg" class="content-graph-mini" role="img" aria-label="Knowledge graph: {ego_label}">
  <style>{MINI_SVG_STYLES}</style>
{lines}
  <circle cx="{cx:.1}" cy="{cy:.1}" r="22" class="graph-node ego {ego_css}"/>
  <text x="{cx:.1}" y="{:.1}" class="graph-label ego">{ego_label}</text>
{neighbor_circles}
</svg>"#,
        cy + 36.0,
    )
}

fn node_css_class(node: &GraphNode) -> &'static str {
    match node {
        GraphNode::Content { .. } => "content",
        GraphNode::Ontology { .. } => "ontology",
        GraphNode::Adr { .. } => "adr",
    }
}

fn truncate(s: &str, max: usize) -> String {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() <= max {
        s.to_owned()
    } else {
        chars[..max - 1].iter().collect::<String>() + "…"
    }
}
