use axum::extract::{Form, Path, State};
use axum::response::{Html, IntoResponse, Redirect, Response};
use serde::{Deserialize, Serialize};
use tera::Context;

use super::super::auth::AuthUser;
use super::common::{auth_role_str, epoch_secs, insert_brand_ctx, render, tera_ref, UiError};
use crate::api::AppState;

#[derive(Debug, Serialize)]
pub(crate) struct SessionRow {
    token: String,
    actor_type: String,
    hostname: String,
    pid: u32,
    project: String,
    role: String,
    registered_ago: u64,
    last_seen_ago: u64,
    pending_notifications: u64,
    has_preferences: bool,
}

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

    let now = epoch_secs();
    let rows: Vec<SessionRow> = state
        .actors
        .list()
        .into_iter()
        .map(|(token, s)| SessionRow {
            token,
            actor_type: s.actor_type,
            hostname: s.hostname,
            pid: s.pid,
            project: s.project,
            role: s.role,
            registered_ago: now.saturating_sub(s.registered_at),
            last_seen_ago: now.saturating_sub(s.last_seen),
            pending_notifications: s.pending_notifications,
            has_preferences: !s
                .preferences
                .as_object()
                .map(|m| m.is_empty())
                .unwrap_or(true),
        })
        .collect();

    let mut ctx = Context::new();
    ctx.insert("sessions", &rows);
    ctx.insert("total", &rows.len());
    ctx.insert("base_url", "/ui");
    ctx.insert("current_role", "admin");
    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/sessions.html", &ctx).await
}

pub async fn sessions_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 now = epoch_secs();
    let rows: Vec<SessionRow> = ctx_ref
        .actors
        .list()
        .into_iter()
        .map(|(token, s)| SessionRow {
            token,
            actor_type: s.actor_type,
            hostname: s.hostname,
            pid: s.pid,
            project: s.project,
            role: s.role,
            registered_ago: now.saturating_sub(s.registered_at),
            last_seen_ago: now.saturating_sub(s.last_seen),
            pending_notifications: s.pending_notifications,
            has_preferences: !s
                .preferences
                .as_object()
                .map(|m| m.is_empty())
                .unwrap_or(true),
        })
        .collect();

    let base_url = format!("/ui/{slug}");
    let http_role = auth_role_str(&_auth);
    let mut ctx = Context::new();
    ctx.insert("sessions", &rows);
    ctx.insert("total", &rows.len());
    ctx.insert("base_url", &base_url);
    ctx.insert("slug", &slug);
    ctx.insert("current_role", &http_role);
    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/sessions.html", &ctx).await
}

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

    let now = epoch_secs();

    #[derive(Serialize)]
    struct NotifRow {
        id: u64,
        project: String,
        event: String,
        files: Vec<String>,
        age_secs: u64,
        source_actor: Option<String>,
    }

    let rows: Vec<NotifRow> = state
        .notifications
        .all_recent()
        .into_iter()
        .map(|n| NotifRow {
            id: n.id,
            project: n.project,
            event: format!("{:?}", n.event),
            files: n.files,
            age_secs: now.saturating_sub(n.timestamp),
            source_actor: n.source_actor,
        })
        .collect();

    let mut ctx = Context::new();
    ctx.insert("notifications", &rows);
    ctx.insert("total", &rows.len());
    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/notifications.html", &ctx).await
}

pub async fn notifications_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 now = epoch_secs();

    #[derive(Serialize)]
    struct NotifRow {
        id: u64,
        project: String,
        event: String,
        is_custom: bool,
        files: Vec<String>,
        age_secs: u64,
        source_actor: Option<String>,
        custom_kind: Option<String>,
        custom_title: Option<String>,
        custom_payload: Option<serde_json::Value>,
        source_project: Option<String>,
    }

    let rows: Vec<NotifRow> = ctx_ref
        .notifications
        .all_recent()
        .into_iter()
        .map(|n| {
            let is_custom = matches!(n.event, crate::notifications::NotificationEvent::Custom);
            NotifRow {
                id: n.id,
                project: n.project,
                event: format!("{:?}", n.event),
                is_custom,
                files: n.files,
                age_secs: now.saturating_sub(n.timestamp),
                source_actor: n.source_actor,
                custom_kind: n.custom_kind,
                custom_title: n.custom_title,
                custom_payload: n.custom_payload,
                source_project: n.source_project,
            }
        })
        .collect();

    // All other registered projects for the emit-to selector
    let other_projects: Vec<String> = state
        .registry
        .all()
        .into_iter()
        .map(|p| p.slug.clone())
        .collect();

    let base_url = format!("/ui/{slug}");
    let mut ctx = Context::new();
    ctx.insert("notifications", &rows);
    ctx.insert("total", &rows.len());
    let http_role = auth_role_str(&auth);
    // Viewers can see notifications but cannot emit them — template gates on
    // current_role.
    let empty: Vec<String> = vec![];
    let visible_projects: &Vec<String> = if http_role == "admin" {
        &other_projects
    } else {
        &empty
    };
    ctx.insert("other_projects", visible_projects);
    ctx.insert("base_url", &base_url);
    ctx.insert("slug", &slug);
    ctx.insert("current_role", &http_role);
    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/notifications.html", &ctx).await
}

#[derive(Deserialize)]
pub struct EmitNotifForm {
    pub target_slug: String,
    pub kind: String,
    pub title: String,
    #[serde(default)]
    pub payload: String,
    #[serde(default)]
    pub source_actor: String,
}

#[derive(Deserialize)]
pub struct NotifActionForm {
    pub action_id: String,
}

pub async fn notification_action_mp(
    State(state): State<AppState>,
    Path((slug, notif_id)): Path<(String, u64)>,
    _auth: AuthUser,
    headers: axum::http::HeaderMap,
    Form(form): Form<NotifActionForm>,
) -> Result<Response, UiError> {
    use axum::response::Html;

    let ctx_ref = state.registry.get(&slug).ok_or(UiError::NotConfigured)?;

    let notif = ctx_ref
        .notifications
        .get_one(notif_id)
        .ok_or_else(|| UiError::BadRequest(format!("notification {notif_id} not found")))?;

    let actions: Vec<serde_json::Value> = notif
        .custom_payload
        .as_ref()
        .and_then(|p| p.get("actions"))
        .and_then(|a| a.as_array())
        .cloned()
        .unwrap_or_default();

    let action = actions
        .iter()
        .find(|a| a.get("id").and_then(|v| v.as_str()) == Some(form.action_id.as_str()))
        .ok_or_else(|| UiError::BadRequest(format!("action '{}' not found", form.action_id)))?;

    let mode = action
        .get("mode")
        .and_then(|v| v.as_str())
        .unwrap_or("manual");
    let dag_id = action.get("dag").and_then(|v| v.as_str()).unwrap_or("");

    // ── backlog review actions ────────────────────────────────────────────────
    if mode == "backlog_approve" {
        let payload = notif.custom_payload.as_ref().ok_or_else(|| {
            UiError::BadRequest("backlog_approve: notification has no payload".into())
        })?;
        let item_id = payload
            .get("item_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| UiError::BadRequest("backlog_approve: missing item_id".into()))?;
        let proposed = payload
            .get("proposed_status")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                UiError::BadRequest("backlog_approve: missing proposed_status".into())
            })?;
        let backlog_path =
            ontoref_ontology::layout::resolve_section(&ctx_ref.root, "reflection/backlog.ncl");
        let _guard = state.ncl_write_lock.acquire(&backlog_path).await;
        super::super::backlog_ncl::update_status(
            &backlog_path,
            item_id,
            proposed,
            &super::common::today_iso(),
        )
        .map_err(|e| UiError::NclExport {
            path: "backlog.ncl".into(),
            reason: e.to_string(),
        })?;
        ctx_ref
            .cache
            .invalidate_file(&backlog_path.canonicalize().unwrap_or(backlog_path));
        if headers.contains_key("hx-request") {
            return Ok(Html(format!(
                r#"<span class="badge badge-xs badge-success gap-1">✓ approved → {proposed}</span>"#
            ))
            .into_response());
        }
        return Ok(Redirect::to(&format!("/ui/{slug}/notifications")).into_response());
    }

    if mode == "backlog_reject" {
        if headers.contains_key("hx-request") {
            return Ok(Html(
                r#"<span class="badge badge-xs badge-ghost gap-1">✕ rejected</span>"#.to_string(),
            )
            .into_response());
        }
        return Ok(Redirect::to(&format!("/ui/{slug}/notifications")).into_response());
    }

    // ── DAG actions ───────────────────────────────────────────────────────────
    if !dag_id.is_empty() && matches!(mode, "auto" | "semi") {
        let ontoref_bin = ctx_ref.root.join("ontoref");
        if ontoref_bin.exists() {
            tokio::process::Command::new(&ontoref_bin)
                .arg(dag_id)
                .current_dir(&ctx_ref.root)
                .spawn()
                .map_err(|e| UiError::BadRequest(format!("dag spawn failed: {e}")))?;
        } else {
            return Err(UiError::BadRequest(format!(
                "ontoref binary not found at {}",
                ontoref_bin.display()
            )));
        }
    }

    if headers.contains_key("hx-request") {
        return Ok(Html(
            r#"<span class="badge badge-xs badge-success gap-1">✓ triggered</span>"#.to_string(),
        )
        .into_response());
    }
    Ok(Redirect::to(&format!("/ui/{slug}/notifications")).into_response())
}

pub async fn emit_notification_mp(
    State(state): State<AppState>,
    Path(slug): Path<String>,
    _auth: AuthUser,
    Form(form): Form<EmitNotifForm>,
) -> Result<Response, UiError> {
    let registry = &state.registry;
    let target = registry.get(&form.target_slug).ok_or_else(|| {
        UiError::BadRequest(format!("unknown target project: {}", form.target_slug))
    })?;

    let payload = if form.payload.trim().is_empty() {
        None
    } else {
        serde_json::from_str::<serde_json::Value>(&form.payload).ok()
    };
    let actor = if form.source_actor.trim().is_empty() {
        None
    } else {
        Some(form.source_actor)
    };

    target.notifications.push_custom(
        &form.target_slug,
        &form.kind,
        &form.title,
        payload,
        actor,
        Some(slug.clone()),
    );

    Ok(Redirect::to(&format!("/ui/{slug}/notifications")).into_response())
}
