use std::sync::Arc;

use axum::extract::{Path, State};
use axum::response::Html;
use tera::Context;
use tracing::warn;

use super::super::auth::AuthUser;
use super::common::{
    auth_role_str, epoch_secs, extract_card_from_config, insert_brand_ctx, load_config_json,
    ncl_export, now_hms, readme_description, render, tera_ref, UiError,
};
use super::graph_helpers::{
    api_catalog_graph_nodes, manifest_graph_nodes, read_project_api_catalogs, state_graph_nodes,
    workspace_crate_nodes,
};
use super::picker::{
    count_ncl_files, detect_generated, detect_showcase, git_remotes, load_pending_migrations,
};
use crate::api::AppState;

#[derive(serde::Serialize)]
struct BacklogStats {
    total: usize,
    open: usize,
    inprog: usize,
    done: usize,
    cancelled: usize,
    critical: usize,
}

async fn load_backlog_items(
    cache: &Arc<crate::cache::NclCache>,
    backlog_path: &std::path::Path,
    import_path: Option<&str>,
) -> Vec<serde_json::Value> {
    let Ok((json, _)) = cache.export(backlog_path, import_path).await else {
        return vec![];
    };
    json.get("items")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default()
}

fn backlog_stats(items: &[serde_json::Value]) -> BacklogStats {
    let count = |key: &str, val: &str| {
        items
            .iter()
            .filter(|it| it.get(key).and_then(|v| v.as_str()) == Some(val))
            .count()
    };
    BacklogStats {
        total: items.len(),
        open: count("status", "Open"),
        inprog: count("status", "InProgress"),
        done: count("status", "Done"),
        cancelled: count("status", "Cancelled"),
        critical: count("priority", "Critical"),
    }
}

pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;

    let hits = state.cache.hit_count();
    let misses = state.cache.miss_count();
    let total = hits + misses;
    let hit_rate = if total > 0 {
        format!("{:.1}%", hits as f64 / total as f64 * 100.0)
    } else {
        "—".to_string()
    };

    let mut ctx = Context::new();
    ctx.insert("uptime_secs", &state.started_at.elapsed().as_secs());
    ctx.insert("cache_entries", &state.cache.len());
    ctx.insert("cache_hits", &hits);
    ctx.insert("cache_misses", &misses);
    ctx.insert("cache_hit_rate", &hit_rate);
    ctx.insert("active_actors", &state.actors.count());
    ctx.insert(
        "notification_count",
        &state.notifications.all_recent().len(),
    );
    ctx.insert("project_root", &state.project_root.display().to_string());
    ctx.insert("now", &epoch_secs());
    ctx.insert("now_hms", &now_hms());
    ctx.insert("base_url", "/ui");
    insert_brand_ctx(
        &mut ctx,
        &state.project_root,
        &state.cache,
        state.nickel_import_path.as_deref(),
        "/ui",
        &state.registry,
        &state.service_flags,
    )
    .await;

    render(tera, "pages/dashboard.html", &ctx).await
}

pub async fn graph(State(state): State<AppState>) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;

    let core_json = match ncl_export(&state, ".ontology/core.ncl").await {
        Ok(v) => v,
        Err(e) => {
            warn!(error = %e, "failed to load .ontology/core.ncl for graph");
            serde_json::json!({ "nodes": [], "edges": [] })
        }
    };

    let graph_json = serde_json::to_string(&core_json).unwrap_or_else(|_| "{}".to_string());

    let mut ctx = Context::new();
    ctx.insert("graph_json", &graph_json);
    ctx.insert("project_root", &state.project_root.display().to_string());
    ctx.insert("base_url", "/ui");
    insert_brand_ctx(
        &mut ctx,
        &state.project_root,
        &state.cache,
        state.nickel_import_path.as_deref(),
        "/ui",
        &state.registry,
        &state.service_flags,
    )
    .await;

    render(tera, "pages/graph.html", &ctx).await
}

pub async fn dashboard_mp(
    State(state): State<AppState>,
    Path(slug): Path<String>,
    auth: AuthUser,
) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;
    let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;

    let hits = ctx_ref.cache.hit_count();
    let misses = ctx_ref.cache.miss_count();
    let total = hits + misses;
    let hit_rate = if total > 0 {
        format!("{:.1}%", hits as f64 / total as f64 * 100.0)
    } else {
        "—".to_string()
    };

    let base_url = format!("/ui/{slug}");
    let backlog_path =
        ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
    let backlog_items = if backlog_path.exists() {
        load_backlog_items(
            &ctx_ref.cache,
            &backlog_path,
            ctx_ref.import_path.as_deref(),
        )
        .await
    } else {
        vec![]
    };
    let backlog = backlog_stats(&backlog_items);
    let adr_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(
        &ctx_ref.root,
        "adrs",
    ));
    let mode_count = count_ncl_files(&ontoref_ontology::layout::resolve_section(
        &ctx_ref.root,
        "reflection/modes",
    ));

    // ── Project identity (same data as project_picker card) ──────────────────
    let config_json = load_config_json(
        &ctx_ref.root,
        &ctx_ref.cache,
        ctx_ref.import_path.as_deref(),
    )
    .await;
    let card = config_json
        .as_ref()
        .map(extract_card_from_config)
        .unwrap_or(serde_json::Value::Null);
    let description = if card
        .get("tagline")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .is_empty()
    {
        readme_description(&ctx_ref.root)
    } else {
        String::new()
    };
    let manifest_path =
        ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
    let (layers, op_modes, default_mode, repo_kind) = if manifest_path.exists() {
        match ctx_ref
            .cache
            .export(&manifest_path, ctx_ref.import_path.as_deref())
            .await
        {
            Ok((json, _)) => {
                let layers: Vec<serde_json::Value> = json
                    .get("layers")
                    .and_then(|v| v.as_array())
                    .map(|arr| {
                        arr.iter()
                            .map(|l| {
                                serde_json::json!({
                                    "id":          l.get("id").and_then(|v| v.as_str()).unwrap_or(""),
                                    "description": l.get("description").and_then(|v| v.as_str()).unwrap_or(""),
                                })
                            })
                            .collect()
                    })
                    .unwrap_or_default();
                let op_modes: Vec<serde_json::Value> = json
                    .get("operational_modes")
                    .and_then(|v| v.as_array())
                    .map(|arr| {
                        arr.iter()
                            .map(|m| {
                                serde_json::json!({
                                    "id":          m.get("id").and_then(|v| v.as_str()).unwrap_or(""),
                                    "description": m.get("description").and_then(|v| v.as_str()).unwrap_or(""),
                                })
                            })
                            .collect()
                    })
                    .unwrap_or_default();
                let default_mode = json
                    .get("default_mode")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                let repo_kind = json
                    .get("repo_kind")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();
                (layers, op_modes, default_mode, repo_kind)
            }
            Err(_) => (vec![], vec![], String::new(), String::new()),
        }
    } else {
        (vec![], vec![], String::new(), String::new())
    };
    let repos = git_remotes(&ctx_ref.root);
    let showcase = detect_showcase(&ctx_ref.root, &base_url);
    let generated = detect_generated(&ctx_ref.root, &base_url);
    let pending_migrations = load_pending_migrations(&ctx_ref.root).await;

    let mut ctx = Context::new();
    ctx.insert("uptime_secs", &state.started_at.elapsed().as_secs());
    ctx.insert("cache_entries", &ctx_ref.cache.len());
    ctx.insert("cache_hits", &hits);
    ctx.insert("cache_misses", &misses);
    ctx.insert("cache_hit_rate", &hit_rate);
    ctx.insert("active_actors", &ctx_ref.actors.count());
    ctx.insert(
        "notification_count",
        &ctx_ref.notifications.all_recent().len(),
    );
    ctx.insert("project_root", &ctx_ref.root.display().to_string());
    ctx.insert("now", &epoch_secs());
    ctx.insert("now_hms", &now_hms());
    ctx.insert("base_url", &base_url);
    ctx.insert("slug", &slug);
    ctx.insert("backlog", &backlog);
    ctx.insert("adr_count", &adr_count);
    ctx.insert("mode_count", &mode_count);
    ctx.insert("current_role", &auth_role_str(&auth));
    ctx.insert("card", &card);
    ctx.insert("description", &description);
    ctx.insert("repo_kind", &repo_kind);
    ctx.insert("repos", &repos);
    ctx.insert("layers", &layers);
    ctx.insert("op_modes", &op_modes);
    ctx.insert("default_mode", &default_mode);
    ctx.insert("pending_migrations", &pending_migrations);
    ctx.insert("showcase", &showcase);
    ctx.insert("generated", &generated);

    let file_versions: std::collections::BTreeMap<String, u64> = ctx_ref
        .file_versions
        .iter()
        .map(|r| {
            let name = r
                .key()
                .file_name()
                .unwrap_or_default()
                .to_string_lossy()
                .into_owned();
            (name, *r.value())
        })
        .collect();
    ctx.insert("file_versions", &file_versions);

    insert_brand_ctx(
        &mut ctx,
        &ctx_ref.root,
        &ctx_ref.cache,
        ctx_ref.import_path.as_deref(),
        &base_url,
        &state.registry,
        &state.service_flags,
    )
    .await;

    render(tera, "pages/dashboard.html", &ctx).await
}

/// HTMX polling endpoint — returns only the stats bar fragment.
/// Called every 10s by the dashboard's `hx-get` trigger.
pub async fn dashboard_stats_mp(
    State(state): State<AppState>,
    Path(slug): Path<String>,
    _auth: AuthUser,
) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;
    let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;

    let hits = ctx_ref.cache.hit_count();
    let misses = ctx_ref.cache.miss_count();
    let total = hits + misses;
    let hit_rate = if total > 0 {
        format!("{:.1}%", hits as f64 / total as f64 * 100.0)
    } else {
        "—".to_string()
    };

    let mut ctx = Context::new();
    ctx.insert("uptime_secs", &state.started_at.elapsed().as_secs());
    ctx.insert("cache_entries", &ctx_ref.cache.len());
    ctx.insert("cache_hits", &hits);
    ctx.insert("cache_misses", &misses);
    ctx.insert("cache_hit_rate", &hit_rate);
    ctx.insert("active_actors", &ctx_ref.actors.count());
    ctx.insert(
        "notification_count",
        &ctx_ref.notifications.all_recent().len(),
    );
    ctx.insert("base_url", &format!("/ui/{slug}"));
    render(tera, "partials/dashboard_stats.html", &ctx).await
}

pub async fn graph_mp(
    State(state): State<AppState>,
    Path(slug): Path<String>,
    auth: AuthUser,
) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;
    let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;

    let core_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/core.ncl");
    let mut graph = match ctx_ref
        .cache
        .export(&core_path, ctx_ref.import_path.as_deref())
        .await
    {
        Ok((v, _)) => v,
        Err(e) => {
            warn!(error = %e, slug = %slug, "failed to load ontology/core.ncl for graph");
            serde_json::json!({ "nodes": [], "edges": [] })
        }
    };

    // Merge api-catalog artifacts as Crate + Route nodes/edges.
    let catalogs = read_project_api_catalogs(
        &ctx_ref.root,
        &ctx_ref.cache,
        ctx_ref.import_path.as_deref(),
    )
    .await;
    let (api_nodes, api_edges) = api_catalog_graph_nodes(&catalogs);

    // Merge FSM state dimensions from ontology/state.ncl.
    let state_path = ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/state.ncl");
    let (state_nodes, state_edges) = if state_path.exists() {
        match ctx_ref
            .cache
            .export(&state_path, ctx_ref.import_path.as_deref())
            .await
        {
            Ok((v, _)) => state_graph_nodes(&v),
            Err(e) => {
                warn!(error = %e, "failed to load ontology/state.ncl for graph");
                (vec![], vec![])
            }
        }
    } else {
        (vec![], vec![])
    };

    // Merge workspace crate dependency layer.
    let (ws_nodes, ws_edges) = workspace_crate_nodes(&ctx_ref.root);

    // Merge manifest config sections + requirements.
    let manifest_path =
        ontoref_ontology::layout::resolve_section(&ctx_ref.root, "ontology/manifest.ncl");
    let (manifest_nodes, manifest_edges) = if manifest_path.exists() {
        match ctx_ref
            .cache
            .export(&manifest_path, ctx_ref.import_path.as_deref())
            .await
        {
            Ok((v, _)) => manifest_graph_nodes(&v),
            Err(e) => {
                warn!(error = %e, "failed to load ontology/manifest.ncl for graph");
                (vec![], vec![])
            }
        }
    } else {
        (vec![], vec![])
    };

    let all_extra_nodes = api_nodes
        .into_iter()
        .chain(state_nodes)
        .chain(ws_nodes)
        .chain(manifest_nodes)
        .collect::<Vec<_>>();
    let all_extra_edges = api_edges
        .into_iter()
        .chain(state_edges)
        .chain(ws_edges)
        .chain(manifest_edges)
        .collect::<Vec<_>>();

    if !all_extra_nodes.is_empty() {
        if let Some(nodes) = graph.get_mut("nodes").and_then(|v| v.as_array_mut()) {
            nodes.extend(all_extra_nodes);
        }
        if let Some(edges) = graph.get_mut("edges").and_then(|v| v.as_array_mut()) {
            edges.extend(all_extra_edges);
        }
    }

    let graph_json = serde_json::to_string(&graph).unwrap_or_else(|_| "{}".to_string());

    let base_url = format!("/ui/{slug}");
    let mut ctx = Context::new();
    ctx.insert("graph_json", &graph_json);
    ctx.insert("project_root", &ctx_ref.root.display().to_string());
    ctx.insert("base_url", &base_url);
    ctx.insert("slug", &slug);
    ctx.insert("current_role", &auth_role_str(&auth));
    insert_brand_ctx(
        &mut ctx,
        &ctx_ref.root,
        &ctx_ref.cache,
        ctx_ref.import_path.as_deref(),
        &base_url,
        &state.registry,
        &state.service_flags,
    )
    .await;

    render(tera, "pages/graph.html", &ctx).await
}

pub async fn api_catalog_page_mp(
    State(state): State<AppState>,
    Path(slug): Path<String>,
    auth: AuthUser,
) -> Result<Html<String>, UiError> {
    let tera = tera_ref(&state)?;
    let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;
    let base_url = format!("/ui/{slug}");

    let is_primary = slug == state.registry.primary_slug();

    let grouped: Vec<serde_json::Value> = if is_primary {
        let routes: Vec<serde_json::Value> = crate::api_catalog::catalog()
            .into_iter()
            .map(|r| {
                let params: Vec<serde_json::Value> = r
                    .params
                    .iter()
                    .map(|p| {
                        serde_json::json!({
                            "name": p.name,
                            "type": p.kind,
                            "constraint": p.constraint,
                            "description": p.description,
                        })
                    })
                    .collect();
                serde_json::json!({
                    "method":       r.method,
                    "path":         r.path,
                    "description":  r.description,
                    "auth":         r.auth,
                    "actors":       r.actors,
                    "params":       params,
                    "tags":         r.tags,
                    "feature":      r.feature,
                    "source_file":  r.source_file,
                })
            })
            .collect();
        vec![serde_json::json!({"name": "ontoref-daemon", "routes": routes})]
    } else {
        read_project_api_catalogs(
            &ctx_ref.root,
            &ctx_ref.cache,
            ctx_ref.import_path.as_deref(),
        )
        .await
        .into_iter()
        .map(|(name, routes)| serde_json::json!({"name": name, "routes": routes}))
        .collect()
    };

    let route_count: usize = grouped
        .iter()
        .filter_map(|g| g["routes"].as_array())
        .map(|a| a.len())
        .sum();
    let catalogs_json = serde_json::to_string(&grouped).unwrap_or_else(|_| "[]".to_string());

    let mut ctx = Context::new();
    ctx.insert("catalogs_json", &catalogs_json);
    ctx.insert("route_count", &route_count);
    ctx.insert("is_primary", &is_primary);
    ctx.insert("base_url", &base_url);
    ctx.insert("slug", &slug);
    ctx.insert("current_role", &auth_role_str(&auth));
    insert_brand_ctx(
        &mut ctx,
        &ctx_ref.root,
        &ctx_ref.cache,
        ctx_ref.import_path.as_deref(),
        &base_url,
        &state.registry,
        &state.service_flags,
    )
    .await;

    render(tera, "pages/api_catalog.html", &ctx).await
}
