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

use super::super::auth::AuthUser;
use super::common::{
    epoch_secs, extract_card_from_config, extract_registry_ctx, extract_vault_ctx, insert_mcp_ctx,
    load_config_json, readme_description, render, tera_ref, UiError,
};
use crate::api::AppState;

pub(crate) fn count_ncl_files(dir: &std::path::Path) -> usize {
    std::fs::read_dir(dir)
        .map(|rd| {
            rd.flatten()
                .filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("ncl"))
                .count()
        })
        .unwrap_or(0)
}

/// Read git remote names+URLs from `{root}/.git/config` by parsing the ini file
/// directly. Returns `[{ "name": "origin", "url": "git@github.com:..." },
/// ...]`.
pub(crate) fn git_remotes(root: &std::path::Path) -> Vec<serde_json::Value> {
    let Ok(content) = std::fs::read_to_string(root.join(".git").join("config")) else {
        return vec![];
    };
    let mut remotes = Vec::new();
    let mut current_name: Option<String> = None;
    for line in content.lines() {
        let t = line.trim();
        if let Some(inner) = t
            .strip_prefix("[remote \"")
            .and_then(|s| s.strip_suffix("\"]"))
        {
            current_name = Some(inner.to_string());
        } else if t.starts_with('[') {
            current_name = None;
        } else if let Some(name) = &current_name {
            if let Some(url) = t.strip_prefix("url = ") {
                remotes.push(serde_json::json!({ "name": name, "url": url }));
                current_name = None;
            }
        }
    }
    remotes
}

/// Detect known showcase pages in `assets/` (committed artefacts).
pub fn detect_showcase(root: &std::path::Path, base_url: &str) -> Vec<serde_json::Value> {
    let candidates = [
        (
            "branding",
            "Branding",
            &["branding/index.html", "index.html"] as &[&str],
        ),
        ("web", "Website", &["web/index.html"]),
        (
            "presentation",
            "Presentation",
            &[
                "presentation/dist/index.html",
                "presentation/slides/index.html",
            ],
        ),
    ];
    candidates
        .iter()
        .filter_map(|(id, label, paths)| {
            let found = paths
                .iter()
                .find(|p| root.join("assets").join(p).exists())?;
            Some(serde_json::json!({
                "id":    id,
                "label": label,
                "url":   format!("{base_url}/assets/{found}"),
            }))
        })
        .collect()
}

/// Scan `public/` for generated artefacts — any subdirectory that contains
/// an `index.html` becomes a served link.
///
/// Well-known subdirectory names get a display label; unknown names are
/// title-cased from the directory name.
pub fn detect_generated(root: &std::path::Path, base_url: &str) -> Vec<serde_json::Value> {
    let public_dir = root.join("public");
    let Ok(entries) = std::fs::read_dir(&public_dir) else {
        return vec![];
    };
    let known_labels: std::collections::HashMap<&str, &str> = [
        ("cargo-doc", "API Docs (cargo doc)"),
        ("docs", "Documentation"),
        ("mdbook", "MDBook"),
        ("book", "Book"),
        ("coverage", "Coverage Report"),
        ("typedoc", "TypeDoc"),
        ("storybook", "Storybook"),
    ]
    .into_iter()
    .collect();

    let mut items: Vec<serde_json::Value> = entries
        .flatten()
        .filter_map(|e| {
            let path = e.path();
            if !path.is_dir() {
                return None;
            }
            if !path.join("index.html").exists() {
                return None;
            }
            let dir_name = path.file_name()?.to_str()?.to_string();
            let label = known_labels
                .get(dir_name.as_str())
                .copied()
                .unwrap_or(&dir_name)
                .to_string();
            Some(serde_json::json!({
                "id":    dir_name,
                "label": label,
                "url":   format!("{base_url}/public/{dir_name}/"),
            }))
        })
        .collect();

    items.sort_by_key(|v| v["id"].as_str().unwrap_or("").to_string());
    items
}

/// Run `ontoref migrate pending --fmt json` for a given project root and return
/// the pending migration list. Returns empty on any error or timeout (2s).
pub async fn load_pending_migrations(project_root: &std::path::Path) -> Vec<serde_json::Value> {
    let Ok(output) = tokio::time::timeout(
        std::time::Duration::from_secs(2),
        tokio::process::Command::new("ontoref")
            .args(["migrate", "pending", "--fmt", "json"])
            .env("ONTOREF_ACTOR", "agent")
            .env("ONTOREF_PROJECT_ROOT", project_root)
            .stdout(std::process::Stdio::piped())
            .stderr(std::process::Stdio::null())
            .output(),
    )
    .await
    else {
        return vec![];
    };
    let Ok(output) = output else { return vec![] };
    if !output.status.success() {
        return vec![];
    }
    serde_json::from_slice::<Vec<serde_json::Value>>(&output.stdout).unwrap_or_default()
}

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

    let registry = &state.registry;

    let now = epoch_secs();
    let mut projects: Vec<serde_json::Value> = Vec::new();

    for proj in registry.all() {
        if !proj.root.join(".ontoref").is_dir() {
            continue;
        }

        // Sessions
        let actor_list = proj.actors.list();
        let sessions: Vec<serde_json::Value> = actor_list
            .iter()
            .map(|(token, s)| {
                serde_json::json!({
                    "token": &token[..8.min(token.len())],
                    "actor_type": s.actor_type,
                    "hostname": s.hostname,
                    "pid": s.pid,
                    "last_seen_ago": now.saturating_sub(s.last_seen),
                })
            })
            .collect();

        // Notifications
        let notif_list = proj.notifications.all_recent();
        let notifications: Vec<serde_json::Value> = notif_list
            .iter()
            .take(8)
            .map(|n| {
                serde_json::json!({
                    "id": n.id,
                    "event": format!("{:?}", n.event),
                    "files": n.files.iter().take(3).collect::<Vec<_>>(),
                    "age_secs": now.saturating_sub(n.timestamp),
                })
            })
            .collect();

        // Backlog — optional NCL export
        let backlog_path =
            ontoref_ontology::layout::resolve_section(&proj.root, "reflection/backlog.ncl");
        let (backlog_items, backlog_open) = if backlog_path.exists() {
            match proj
                .cache
                .export(&backlog_path, proj.import_path.as_deref())
                .await
            {
                Ok((json, _)) => {
                    let items: Vec<serde_json::Value> = json
                        .get("items")
                        .and_then(|v| v.as_array())
                        .map(|arr| {
                            arr.iter()
                                .map(|it| serde_json::json!({
                                    "id":       it.get("id").and_then(|v| v.as_str()).unwrap_or(""),
                                    "title":    it.get("title").and_then(|v| v.as_str()).unwrap_or(""),
                                    "status":   it.get("status").and_then(|v| v.as_str()).unwrap_or(""),
                                    "priority": it.get("priority").and_then(|v| v.as_str()).unwrap_or(""),
                                    "kind":     it.get("kind").and_then(|v| v.as_str()).unwrap_or(""),
                                }))
                                .collect()
                        })
                        .unwrap_or_default();
                    let open = items
                        .iter()
                        .filter(|it| it.get("status").and_then(|v| v.as_str()) == Some("Open"))
                        .count();
                    (items, open)
                }
                Err(_) => (vec![], 0),
            }
        } else {
            (vec![], 0)
        };

        // Manifest — layers, operational_modes, default_mode, repo_kind, registry
        let manifest_path =
            ontoref_ontology::layout::resolve_section(&proj.root, "ontology/manifest.ncl");
        let manifest_json: Option<serde_json::Value> = if manifest_path.exists() {
            proj.cache
                .export(&manifest_path, proj.import_path.as_deref())
                .await
                .ok()
                .map(|(j, _)| j)
        } else {
            None
        };
        let (layers, op_modes, default_mode, repo_kind) = match &manifest_json {
            Some(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)
            }
            None => (vec![], vec![], String::new(), String::new()),
        };
        let (registry_participant, registry_namespaces, registry_endpoint) =
            extract_registry_ctx(manifest_json.as_ref());

        // Vault — read project.ncl::sops (ADR-017 + ADR-019)
        let project_ncl_path = proj.root.join(".ontoref").join("project.ncl");
        let project_json: Option<serde_json::Value> = if project_ncl_path.exists() {
            proj.cache
                .export(&project_ncl_path, proj.import_path.as_deref())
                .await
                .ok()
                .map(|(j, _)| j)
        } else {
            None
        };
        let (has_vault, vault_id, vault_mode, vault_recipients, vault_last_op) =
            extract_vault_ctx(project_json.as_ref());

        // card — loaded from `.ontoref/config.ncl` `card` field
        let config_json =
            load_config_json(&proj.root, &proj.cache, proj.import_path.as_deref()).await;
        let card = config_json
            .as_ref()
            .map(extract_card_from_config)
            .unwrap_or(serde_json::Value::Null);
        tracing::debug!(
            slug = %proj.slug,
            import_path = ?proj.import_path,
            config_loaded = config_json.is_some(),
            card_tagline = card.get("tagline").and_then(|v| v.as_str()).unwrap_or(""),
            "project card loaded"
        );

        // Description — first meaningful text line from README.md (fallback when no
        // card)
        let description = if card
            .get("tagline")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .is_empty()
        {
            readme_description(&proj.root)
        } else {
            String::new()
        };

        let proj_base = format!("/ui/{}", proj.slug);
        let showcase = detect_showcase(&proj.root, &proj_base);
        let generated = detect_generated(&proj.root, &proj_base);

        let repos = git_remotes(&proj.root);
        let opmode = if proj.push_only { "push" } else { "daemon" };

        projects.push(serde_json::json!({
            "slug":                 proj.slug,
            "root":                 proj.root.display().to_string(),
            "auth":                 proj.auth_enabled(),
            "card":                 card,
            "description":          description,
            "default_mode":         default_mode,
            "repo_kind":            repo_kind,
            "repos":                repos,
            "session_count":        sessions.len(),
            "sessions":             sessions,
            "notif_count":          notifications.len(),
            "notifications":        notifications,
            "backlog_open":         backlog_open,
            "backlog_items":        backlog_items,
            "layers":               layers,
            "op_modes":             op_modes,
            "showcase":             showcase,
            "generated":            generated,
            "opmode":               opmode,
            "registry_participant": registry_participant,
            "registry_namespaces":  registry_namespaces,
            "registry_endpoint":    registry_endpoint,
            "has_vault":            has_vault,
            "vault_id":             vault_id,
            "vault_mode":           vault_mode,
            "vault_recipients":     vault_recipients,
            "vault_last_op":        vault_last_op,
        }));
    }

    let mut ctx = Context::new();
    ctx.insert("projects", &projects);
    ctx.insert("base_url", "/ui");
    ctx.insert("hide_project_nav", &true);
    insert_mcp_ctx(&mut ctx, &state.service_flags);
    render(tera, "pages/project_picker.html", &ctx).await
}

/// HTMX lazy-load endpoint — triggered by `hx-trigger="revealed"` on each
/// project card. Returns an HTML fragment: the panel as the main swap target
/// plus the badge as an out-of-band swap (`hx-swap-oob`).
pub async fn pending_migrations_fragment_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 migrations = load_pending_migrations(&ctx_ref.root).await;
    let mut ctx = Context::new();
    ctx.insert("migrations", &migrations);
    ctx.insert("slug", &slug);
    render(tera, "partials/migrations_fragment.html", &ctx).await
}
