//! Audit log browser — three tabs (conn / file / alarm), each capped at the
//! latest 200 rows. M5c MVP. Pagination/filtering by date range can come in
//! a follow-up if the operator outgrows this view.

use super::shared::{fmt_unix, html_escape, require_admin};
use crate::api::admin::i18n::{t, tf1, Lang};
use crate::api::error::ApiError;
use crate::api::middleware::AuthedUser;
use crate::api::state::AppState;
use axum::extract::{Extension, Query};
use axum::response::Html;
use serde::Deserialize;
use std::fmt::Write as _;
use std::sync::Arc;

const PAGE_SIZE: i64 = 200;

#[derive(Debug, Deserialize)]
pub struct TabQuery {
    #[serde(default)]
    pub tab: Option<String>,
}

pub async fn index(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Query(q): Query<TabQuery>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let tab = q.tab.as_deref().unwrap_or("conn");
    let body = match tab {
        "file" => render_file(&state, lang).await?,
        "alarm" => render_alarm(&state, lang).await?,
        _ => render_conn(&state, lang).await?,
    };
    let pill = |id: &str, label: &str| {
        let active = id == tab;
        let cls = if active {
            "bg-slate-800 text-sky-300 border-sky-800"
        } else {
            "bg-slate-900 text-slate-400 border-slate-800 hover:text-slate-200"
        };
        format!(
            r##"<a href="#audit" hx-get="/admin/pages/audit?tab={id}" hx-target="#main" class="px-3 py-1 rounded border {cls}">{label}</a>"##,
            id = id,
            cls = cls,
            label = label,
        )
    };
    Ok(Html(format!(
        r##"<div class="space-y-4">
  <header class="flex items-center justify-between">
    <h2 class="text-lg font-semibold">{heading}</h2>
    <p class="text-xs text-slate-500">{latest}</p>
  </header>
  <div class="flex gap-2 text-xs">{pill_conn}{pill_file}{pill_alarm}</div>
  {body}
</div>"##,
        heading = t(lang, "audit.heading"),
        latest = tf1(lang, "audit.latest", &PAGE_SIZE.to_string()),
        pill_conn = pill("conn", t(lang, "audit.tab_conn")),
        pill_file = pill("file", t(lang, "audit.tab_file")),
        pill_alarm = pill("alarm", t(lang, "audit.tab_alarm")),
        body = body,
    )))
}

async fn render_conn(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
    let rows = state
        .db
        .audit_conn_list(PAGE_SIZE)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if rows.is_empty() {
        return Ok(empty_table(t(lang, "audit.no_conn")));
    }
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
  <table class="w-full text-sm">
    <thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
      <th class="text-left font-medium px-3 py-2">{c_when}</th>
      <th class="text-left font-medium px-3 py-2">{c_peer}</th>
      <th class="text-left font-medium px-3 py-2">{c_conn}</th>
      <th class="text-left font-medium px-3 py-2">{c_ip}</th>
      <th class="text-left font-medium px-3 py-2">{c_action}</th>
      <th class="text-left font-medium px-3 py-2">{c_note}</th>
    </tr></thead>
    <tbody class="divide-y divide-slate-800">"##,
        c_when = t(lang, "audit.col_when"),
        c_peer = t(lang, "audit.col_peer"),
        c_conn = t(lang, "audit.col_conn_session"),
        c_ip = t(lang, "audit.col_ip"),
        c_action = t(lang, "audit.col_action"),
        c_note = t(lang, "audit.col_note"),
    );
    for r in &rows {
        let _ = write!(
            s,
            r##"<tr>
  <td class="px-3 py-2 text-slate-500 text-xs">{when}</td>
  <td class="px-3 py-2 font-mono text-slate-200">{peer}</td>
  <td class="px-3 py-2 text-slate-400">{conn} / {sess}</td>
  <td class="px-3 py-2 text-slate-400 font-mono text-xs">{ip}</td>
  <td class="px-3 py-2 text-slate-300">{action}</td>
  <td class="px-3 py-2 text-slate-400">{note}</td>
</tr>"##,
            when = html_escape(&fmt_unix(r.started_at)),
            peer = html_escape(&r.peer_id),
            conn = r.conn_id,
            sess = r.session_id,
            ip = html_escape(&r.ip),
            action = html_escape(&r.action),
            note = html_escape(&r.note)
        );
    }
    s.push_str("</tbody></table></div>");
    Ok(s)
}

async fn render_file(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
    let rows = state
        .db
        .audit_file_list(PAGE_SIZE)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if rows.is_empty() {
        return Ok(empty_table(t(lang, "audit.no_file")));
    }
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
  <table class="w-full text-sm">
    <thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
      <th class="text-left font-medium px-3 py-2">{c_when}</th>
      <th class="text-left font-medium px-3 py-2">{c_peer}</th>
      <th class="text-left font-medium px-3 py-2">{c_dir}</th>
      <th class="text-left font-medium px-3 py-2">{c_path}</th>
      <th class="text-left font-medium px-3 py-2">{c_remote}</th>
    </tr></thead>
    <tbody class="divide-y divide-slate-800">"##,
        c_when = t(lang, "audit.col_when"),
        c_peer = t(lang, "audit.col_peer"),
        c_dir = t(lang, "audit.col_direction"),
        c_path = t(lang, "audit.col_path"),
        c_remote = t(lang, "audit.col_remote"),
    );
    for r in &rows {
        let dir = match r.direction {
            0 => t(lang, "audit.dir_to_remote"),
            1 => t(lang, "audit.dir_from_remote"),
            _ => "?",
        };
        let _ = write!(
            s,
            r##"<tr>
  <td class="px-3 py-2 text-slate-500 text-xs">{when}</td>
  <td class="px-3 py-2 font-mono text-slate-200">{peer}</td>
  <td class="px-3 py-2 text-slate-400">{dir}</td>
  <td class="px-3 py-2 text-slate-300 font-mono text-xs">{path}</td>
  <td class="px-3 py-2 text-slate-400 font-mono text-xs">{remote}</td>
</tr>"##,
            when = html_escape(&fmt_unix(r.at)),
            peer = html_escape(&r.peer_id),
            dir = dir,
            path = html_escape(&r.path),
            remote = html_escape(&r.remote_peer)
        );
    }
    s.push_str("</tbody></table></div>");
    Ok(s)
}

async fn render_alarm(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
    let rows = state
        .db
        .audit_alarm_list(PAGE_SIZE)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if rows.is_empty() {
        return Ok(empty_table(t(lang, "audit.no_alarm")));
    }
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div class="rounded-md border border-slate-800 bg-slate-900 overflow-hidden">
  <table class="w-full text-sm">
    <thead class="text-xs uppercase text-slate-500 bg-slate-950"><tr>
      <th class="text-left font-medium px-3 py-2">{c_when}</th>
      <th class="text-left font-medium px-3 py-2">{c_peer}</th>
      <th class="text-left font-medium px-3 py-2">{c_type}</th>
      <th class="text-left font-medium px-3 py-2">{c_info}</th>
    </tr></thead>
    <tbody class="divide-y divide-slate-800">"##,
        c_when = t(lang, "audit.col_when"),
        c_peer = t(lang, "audit.col_peer"),
        c_type = t(lang, "audit.col_type"),
        c_info = t(lang, "audit.col_info"),
    );
    for r in &rows {
        let typ = match r.typ {
            0 => "IpWhitelist",
            1 => "ExceedThirtyAttempts",
            2 => "SixAttemptsWithinOneMinute",
            6 => "ExceedIPv6PrefixAttempts",
            n => return Ok(format!("(unknown alarm type {})", n)),
        };
        let _ = write!(
            s,
            r##"<tr>
  <td class="px-3 py-2 text-slate-500 text-xs">{when}</td>
  <td class="px-3 py-2 font-mono text-slate-200">{peer}</td>
  <td class="px-3 py-2 text-amber-300">{typ}</td>
  <td class="px-3 py-2 text-slate-400 font-mono text-xs break-all">{info}</td>
</tr>"##,
            when = html_escape(&fmt_unix(r.at)),
            peer = html_escape(&r.peer_id),
            typ = typ,
            info = html_escape(&r.info_json)
        );
    }
    s.push_str("</tbody></table></div>");
    Ok(s)
}

fn empty_table(msg: &str) -> String {
    format!(
        r##"<div class="rounded-md border border-slate-800 bg-slate-900 p-6 text-center text-sm text-slate-500">{}</div>"##,
        html_escape(msg)
    )
}
