use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::time::Duration;

use serde_json::Value;
use tracing::{debug, warn};

use crate::registry::{ProjectContext, ProjectRegistry};

/// Maximum cross-project traversal depth to prevent unbounded recursion.
const MAX_FEDERATION_DEPTH: u32 = 5;

/// HTTP timeout for remote project queries.
const REMOTE_TIMEOUT: Duration = Duration::from_secs(5);

/// A node resolved from a potentially remote project.
#[derive(Debug, Clone, serde::Serialize)]
pub struct FederatedNode {
    pub slug: String,
    pub node_id: String,
    pub node: Value,
    pub depth: u32,
    pub via: String,
}

/// Cross-project impact graph entry.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ImpactEntry {
    pub slug: String,
    pub node_id: String,
    pub node_name: String,
    pub depth: u32,
    pub direction: String,
    pub via: String,
}

/// Mutable traversal state threaded through BFS helpers.
struct Traversal<'a> {
    visited: &'a mut HashSet<String>,
    queue: &'a mut VecDeque<(String, String, u32)>,
    results: &'a mut Vec<ImpactEntry>,
}

/// Resolves nodes and builds cross-project impact graphs.
pub struct FederatedQuery {
    registry: Arc<ProjectRegistry>,
    client: reqwest::Client,
}

impl FederatedQuery {
    pub fn new(registry: Arc<ProjectRegistry>) -> Self {
        let client = reqwest::Client::builder()
            .timeout(REMOTE_TIMEOUT)
            .build()
            .unwrap_or_default();
        Self { registry, client }
    }

    /// Resolve a node by `(slug, node_id)`.
    ///
    /// - Local slug with filesystem access: NclCache lookup.
    /// - Push-only slug with `remote_url`: HTTP GET
    ///   `{remote_url}/graph/node/{node_id}`.
    /// - Unknown slug: `None` with a warning.
    pub async fn resolve(&self, slug: &str, node_id: &str) -> Option<FederatedNode> {
        let ctx = self.registry.get(slug)?;
        if !ctx.push_only {
            return resolve_local(&ctx, slug, node_id).await;
        }
        if ctx.remote_url.is_empty() {
            warn!(
                slug,
                node_id, "push_only project has no remote_url — cannot resolve node"
            );
            return None;
        }
        resolve_remote(&self.client, &ctx.remote_url, slug, node_id).await
    }

    /// Build a cross-project impact graph starting from `(slug, node_id)`.
    ///
    /// Traverses local ontology edges up to `max_depth` hops. When
    /// `include_external` is set, also follows `connections.ncl` entries to
    /// external projects.
    ///
    /// Anti-cycle: visited set keyed by `"slug:node_id"` prevents re-traversal.
    pub async fn impact_graph(
        &self,
        start_slug: &str,
        start_node: &str,
        max_depth: u32,
        include_external: bool,
    ) -> Vec<ImpactEntry> {
        let depth = max_depth.min(MAX_FEDERATION_DEPTH);
        let mut visited: HashSet<String> = HashSet::new();
        let mut queue: VecDeque<(String, String, u32)> = VecDeque::new();
        let mut results: Vec<ImpactEntry> = Vec::new();

        visited.insert(format!("{start_slug}:{start_node}"));
        queue.push_back((start_slug.to_owned(), start_node.to_owned(), 0));

        while let Some((slug, node_id, current_depth)) = queue.pop_front() {
            if current_depth >= depth {
                continue;
            }
            let mut t = Traversal {
                visited: &mut visited,
                queue: &mut queue,
                results: &mut results,
            };
            self.expand_local(&slug, &node_id, current_depth, &mut t)
                .await;
            if include_external {
                self.expand_external(&slug, &node_id, current_depth, &mut t)
                    .await;
            }
        }
        results
    }

    async fn expand_local(
        &self,
        slug: &str,
        node_id: &str,
        current_depth: u32,
        t: &mut Traversal<'_>,
    ) {
        let Some(ctx) = self.registry.get(slug) else {
            return;
        };
        if ctx.push_only {
            return;
        }
        let core_path = ctx.root.join(".ontology").join("core.ncl");
        let Ok((json, _)) = ctx
            .cache
            .export(&core_path, ctx.import_path.as_deref())
            .await
        else {
            return;
        };

        let edges = json
            .get("edges")
            .and_then(|e| e.as_array())
            .cloned()
            .unwrap_or_default();
        let nodes = json
            .get("nodes")
            .and_then(|n| n.as_array())
            .cloned()
            .unwrap_or_default();
        let node_map: HashMap<&str, &Value> = nodes
            .iter()
            .filter_map(|n| Some((n.get("id")?.as_str()?, n)))
            .collect();

        let next_depth = current_depth + 1;
        for entry in collect_edge_entries(slug, node_id, &edges, &node_map) {
            let key = format!("{}:{}", entry.slug, entry.node_id);
            if t.visited.contains(&key) {
                continue;
            }
            t.visited.insert(key);
            t.queue
                .push_back((entry.slug.clone(), entry.node_id.clone(), next_depth));
            t.results.push(ImpactEntry {
                depth: next_depth,
                ..entry
            });
        }
    }

    async fn expand_external(
        &self,
        slug: &str,
        _node_id: &str,
        current_depth: u32,
        t: &mut Traversal<'_>,
    ) {
        let Some(ctx) = self.registry.get(slug) else {
            return;
        };
        if ctx.push_only {
            return;
        }
        let conn_path = ctx.root.join(".ontology").join("connections.ncl");
        if !conn_path.exists() {
            return;
        }
        let Ok((conn_json, _)) = ctx
            .cache
            .export(&conn_path, ctx.import_path.as_deref())
            .await
        else {
            return;
        };

        let next_depth = current_depth + 1;
        for direction in ["upstream", "downstream", "peers"] {
            let conns = conn_json
                .get(direction)
                .and_then(|v| v.as_array())
                .cloned()
                .unwrap_or_default();
            self.expand_connections(direction, &conns, next_depth, t)
                .await;
        }
    }

    async fn expand_connections(
        &self,
        direction: &str,
        conns: &[Value],
        next_depth: u32,
        t: &mut Traversal<'_>,
    ) {
        for conn in conns {
            let target_slug = conn.get("project").and_then(|v| v.as_str()).unwrap_or("");
            let target_node = conn.get("node").and_then(|v| v.as_str()).unwrap_or("");
            let via = conn.get("via").and_then(|v| v.as_str()).unwrap_or("http");
            if target_slug.is_empty() || target_node.is_empty() {
                continue;
            }
            let key = format!("{target_slug}:{target_node}");
            if t.visited.contains(&key) {
                continue;
            }
            t.visited.insert(key);
            if let Some(fed) = self.resolve(target_slug, target_node).await {
                let name = fed
                    .node
                    .get("name")
                    .and_then(|v| v.as_str())
                    .unwrap_or(target_node)
                    .to_owned();
                t.results.push(ImpactEntry {
                    slug: target_slug.to_owned(),
                    node_id: target_node.to_owned(),
                    node_name: name,
                    depth: next_depth,
                    direction: direction.to_owned(),
                    via: via.to_owned(),
                });
                t.queue
                    .push_back((target_slug.to_owned(), target_node.to_owned(), next_depth));
            }
        }
    }
}

fn collect_edge_entries<'a>(
    slug: &str,
    node_id: &str,
    edges: &'a [Value],
    node_map: &HashMap<&str, &'a Value>,
) -> Vec<ImpactEntry> {
    let mut out = Vec::new();
    for edge in edges {
        let from = edge.get("from").and_then(|v| v.as_str()).unwrap_or("");
        let to = edge.get("to").and_then(|v| v.as_str()).unwrap_or("");

        let (neighbor, direction) = if from == node_id && !to.is_empty() {
            (to, "depends_on")
        } else if to == node_id && !from.is_empty() {
            (from, "depended_by")
        } else {
            continue;
        };
        let name = node_map
            .get(neighbor)
            .and_then(|n| n.get("name"))
            .and_then(|v| v.as_str())
            .unwrap_or(neighbor)
            .to_owned();
        out.push(ImpactEntry {
            slug: slug.to_owned(),
            node_id: neighbor.to_owned(),
            node_name: name,
            depth: 0, // caller overwrites
            direction: direction.to_owned(),
            via: "local".to_owned(),
        });
    }
    out
}

async fn resolve_local(ctx: &ProjectContext, slug: &str, node_id: &str) -> Option<FederatedNode> {
    let core_path = ctx.root.join(".ontology").join("core.ncl");
    if !core_path.exists() {
        return None;
    }
    let (json, _) = ctx
        .cache
        .export(&core_path, ctx.import_path.as_deref())
        .await
        .ok()?;
    let node = json
        .get("nodes")?
        .as_array()?
        .iter()
        .find(|n| n.get("id").and_then(|v| v.as_str()) == Some(node_id))?
        .clone();
    debug!(slug, node_id, "federated node resolved from local cache");
    Some(FederatedNode {
        slug: slug.to_owned(),
        node_id: node_id.to_owned(),
        node,
        depth: 0,
        via: "local".to_owned(),
    })
}

async fn resolve_remote(
    client: &reqwest::Client,
    remote_url: &str,
    slug: &str,
    node_id: &str,
) -> Option<FederatedNode> {
    let url = format!("{remote_url}/graph/node/{node_id}");
    debug!(slug, node_id, %url, "federated query to remote daemon");
    let resp = client.get(&url).send().await.ok()?;
    if !resp.status().is_success() {
        warn!(slug, node_id, status = %resp.status(), "remote node fetch failed");
        return None;
    }
    let json: Value = resp.json().await.ok()?;
    Some(FederatedNode {
        slug: slug.to_owned(),
        node_id: node_id.to_owned(),
        node: json,
        depth: 0,
        via: "http".to_owned(),
    })
}

/// Validate all connections declared in a project's `connections.ncl`.
///
/// Returns warnings for unregistered slugs and node IDs that cannot be resolved
/// in the target project's local cache. Push-only targets skip node validation
/// since their cache is not accessible.
pub async fn validate_connections(registry: &Arc<ProjectRegistry>, slug: &str) -> Vec<String> {
    let Some(ctx) = registry.get(slug) else {
        return vec![format!("slug '{slug}' not found in registry")];
    };
    let conn_path = ctx.root.join(".ontology").join("connections.ncl");
    if !conn_path.exists() {
        return vec![];
    }
    let Ok((conn_json, _)) = ctx
        .cache
        .export(&conn_path, ctx.import_path.as_deref())
        .await
    else {
        return vec![format!("failed to export connections.ncl for '{slug}'")];
    };

    let mut warnings = Vec::new();
    for direction in ["upstream", "downstream", "peers"] {
        let conns = conn_json
            .get(direction)
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();
        for conn in &conns {
            check_connection(registry, direction, conn, &mut warnings).await;
        }
    }
    warnings
}

async fn check_connection(
    registry: &Arc<ProjectRegistry>,
    direction: &str,
    conn: &Value,
    warnings: &mut Vec<String>,
) {
    let target = conn.get("project").and_then(|v| v.as_str()).unwrap_or("");
    if target.is_empty() {
        return;
    }
    let Some(target_ctx) = registry.get(target) else {
        warnings.push(format!(
            "{direction}: project '{target}' not registered in this daemon"
        ));
        return;
    };
    if target_ctx.push_only {
        return;
    }
    let target_node = conn.get("node").and_then(|v| v.as_str()).unwrap_or("");
    if target_node.is_empty() {
        return;
    }
    if !node_exists_in_cache(&target_ctx, target_node).await {
        warnings.push(format!(
            "{direction}: node '{target_node}' not found in '{target}' core.ncl"
        ));
    }
}

async fn node_exists_in_cache(ctx: &ProjectContext, node_id: &str) -> bool {
    let core_path = ctx.root.join(".ontology").join("core.ncl");
    let Ok((json, _)) = ctx
        .cache
        .export(&core_path, ctx.import_path.as_deref())
        .await
    else {
        return false;
    };
    json.get("nodes")
        .and_then(|n| n.as_array())
        .map(|nodes| {
            nodes
                .iter()
                .any(|n| n.get("id").and_then(|v| v.as_str()) == Some(node_id))
        })
        .unwrap_or(false)
}
