use std::collections::{HashMap, HashSet};

use super::node_types::{
    ContentGraph, EdgeKind, EdgeSource, EdgeWeight, GraphEdge, GraphMeta, GraphNode,
    OntologyEdgeRaw, OntologyInstance,
};

// ── Edge key for deduplication
// ────────────────────────────────────────────────

/// Unique key for a directed edge.
#[derive(Debug, PartialEq, Eq, Hash)]
struct EdgeKey {
    from: String,
    to: String,
    kind: String,
}

impl EdgeKey {
    fn new(from: &str, to: &str, kind: &EdgeKind) -> Self {
        Self {
            from: from.to_owned(),
            to: to.to_owned(),
            kind: format!("{kind:?}"),
        }
    }
}

/// Unordered pair key — used to detect existing coverage for SharedTag
/// suppression.
fn pair_key(a: &str, b: &str) -> (String, String) {
    if a <= b {
        (a.to_owned(), b.to_owned())
    } else {
        (b.to_owned(), a.to_owned())
    }
}

// ── Public API
// ────────────────────────────────────────────────────────────────

/// Resolve all edges for the graph applying the precedence:
///
/// ```text
/// Declared > AutoBacklink > Implicit(SharedTag)
/// ```
///
/// For any `(from, to, kind)` triple, the highest-precedence source wins.
/// SharedTag edges are additionally suppressed when any Declared or
/// AutoBacklink edge already exists between the same node pair (in either
/// direction).
pub fn resolve_edges(
    graph: &mut ContentGraph,
    content_graph_metas: &[(String, GraphMeta)], // (node_id, graph_meta)
    ontology_edges_raw: &[OntologyEdgeRaw],
    _ontology_instance: OntologyInstance,
) {
    let mut edge_map: HashMap<EdgeKey, GraphEdge> = HashMap::new();

    // 1. Ontology native edges (from core.ncl) — treated as Declared at ontology
    //    layer
    for raw in ontology_edges_raw {
        let Some(kind) = raw.to_edge_kind() else {
            continue;
        };
        let weight = raw.to_weight();
        let key = EdgeKey::new(&raw.from, &raw.to, &kind);
        edge_map.entry(key).or_insert_with(|| GraphEdge {
            from: raw.from.clone(),
            to: raw.to.clone(),
            kind,
            weight,
            source: EdgeSource::Declared,
            note: raw.note.clone(),
        });
    }

    // 2. Declared content edges + AutoBacklinks
    let mut declared_pairs: HashSet<(String, String)> = HashSet::new();

    for (node_id, meta) in content_graph_metas {
        let declared = collect_declared_edges(node_id, meta);

        for edge in declared {
            declared_pairs.insert(pair_key(&edge.from, &edge.to));

            // AutoBacklink first (lower precedence — Declared will override below)
            if let Some(bk_kind) = edge.kind.backlink() {
                let bk_weight = bk_kind.default_weight();
                let bk_key = EdgeKey::new(&edge.to, &edge.from, &bk_kind);
                edge_map.entry(bk_key).or_insert_with(|| GraphEdge {
                    from: edge.to.clone(),
                    to: edge.from.clone(),
                    kind: bk_kind,
                    weight: bk_weight,
                    source: EdgeSource::AutoBacklink,
                    note: None,
                });
            }

            // Declared overwrites any existing entry for this key
            let key = EdgeKey::new(&edge.from, &edge.to, &edge.kind);
            edge_map.insert(key, edge);
        }
    }

    // 3. Implicit SharedTag edges — only when no Declared/Auto pair exists
    for edge in compute_implicit_tag_edges(&graph.nodes) {
        let p = pair_key(&edge.from, &edge.to);
        if declared_pairs.contains(&p) {
            continue; // suppressed — explicit relationship already covers this
                      // pair
        }
        let key = EdgeKey::new(&edge.from, &edge.to, &edge.kind);
        edge_map.entry(key).or_insert(edge);
    }

    graph.edges = edge_map.into_values().collect();
}

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

fn collect_declared_edges(node_id: &str, meta: &GraphMeta) -> Vec<GraphEdge> {
    let mut edges = Vec::new();

    let push = |edges: &mut Vec<GraphEdge>, targets: &[String], kind: EdgeKind| {
        for target in targets {
            let weight = kind.default_weight();
            edges.push(GraphEdge {
                from: node_id.to_owned(),
                to: target.clone(),
                kind: kind.clone(),
                weight,
                source: EdgeSource::Declared,
                note: None,
            });
        }
    };

    push(&mut edges, &meta.implements, EdgeKind::Implements);
    push(&mut edges, &meta.related_to, EdgeKind::RelatedTo);
    push(&mut edges, &meta.part_of, EdgeKind::PartOf);
    push(&mut edges, &meta.references, EdgeKind::References);
    push(&mut edges, &meta.extends, EdgeKind::Extends);

    edges
}

fn compute_implicit_tag_edges(nodes: &[GraphNode]) -> Vec<GraphEdge> {
    // Build tag index: tag → [node_id]
    let mut tag_index: HashMap<String, Vec<String>> = HashMap::new();
    for node in nodes {
        for tag in node.tags() {
            tag_index
                .entry(tag.clone())
                .or_default()
                .push(node.id().to_owned());
        }
    }

    let mut seen: HashSet<(String, String)> = HashSet::new();
    let mut edges = Vec::new();

    for node_ids in tag_index.values() {
        if node_ids.len() < 2 {
            continue;
        }

        for i in 0..node_ids.len() {
            for j in (i + 1)..node_ids.len() {
                let a = &node_ids[i];
                let b = &node_ids[j];
                let p = pair_key(a, b);

                if seen.insert(p) {
                    edges.push(GraphEdge {
                        from: a.clone(),
                        to: b.clone(),
                        kind: EdgeKind::SharedTag,
                        weight: EdgeWeight::Low,
                        source: EdgeSource::Implicit,
                        note: None,
                    });
                }
            }
        }
    }

    edges
}
