//! Address books — list, create shared books, manage shares, delete.
//! Personal books are owned by individual users and managed from the
//! desktop client (the dashboard refuses to mutate them). Shared books
//! are server-side artifacts: an admin creates one, then grants users
//! `read` / `read+write` / `full` access; the desktop client picks them
//! up via `/api/ab/shared/profiles` on the next AB sync (~30 s).

use super::shared::{fmt_unix, html_escape, notice_html, 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, Form, Path};
use axum::response::Html;
use serde::Deserialize;
use std::fmt::Write as _;
use std::sync::Arc;

pub async fn index(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    Ok(Html(render_index(&state, lang, None).await?))
}

#[derive(Debug, Deserialize)]
pub struct CreateForm {
    pub name: String,
}

pub async fn create(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Form(f): Form<CreateForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let name = f.name.trim();
    if name.is_empty() {
        return Ok(Html(
            render_index(&state, lang, Some(("error", t(lang, "ab.name_required")))).await?,
        ));
    }
    let res = state.db.ab_create_shared(admin.user_id, name).await;
    let notice = match res {
        Ok(_) => Some(("ok", tf1(lang, "ab.created", name))),
        Err(e) => {
            // The unique index trips when the same admin creates two books
            // with the same name. Surface that cleanly instead of leaking
            // the raw SQL error.
            let msg = if e.to_string().to_lowercase().contains("unique") {
                t(lang, "ab.exists").to_string()
            } else {
                tf1(lang, "ab.create_failed", &e.to_string())
            };
            Some(("error", msg))
        }
    };
    let n = notice.as_ref().map(|(k, m)| (*k, m.as_str()));
    Ok(Html(render_index(&state, lang, n).await?))
}

pub async fn delete(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(guid): Path<String>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let ok = state
        .db
        .ab_delete(&guid)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let notice = if ok {
        ("ok", t(lang, "ab.deleted"))
    } else {
        ("error", t(lang, "ab.not_found"))
    };
    Ok(Html(render_index(&state, lang, Some(notice)).await?))
}

pub async fn manage(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(guid): Path<String>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    Ok(Html(render_manage(&state, lang, &guid, None).await?))
}

#[derive(Debug, Deserialize)]
pub struct ShareForm {
    pub user_id: i64,
    pub rule: i64,
}

pub async fn share_add(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(guid): Path<String>,
    Form(f): Form<ShareForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    if !(1..=3).contains(&f.rule) {
        return Ok(Html(
            render_manage(&state, lang, &guid, Some(("error", t(lang, "ab.invalid_rule")))).await?,
        ));
    }
    state
        .db
        .ab_share_set(&guid, f.user_id, f.rule)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_manage(&state, lang, &guid, Some(("ok", t(lang, "ab.share_saved")))).await?,
    ))
}

pub async fn share_remove(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path((guid, user_id)): Path<(String, i64)>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let _ = state
        .db
        .ab_share_remove(&guid, user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(
        render_manage(&state, lang, &guid, Some(("ok", t(lang, "ab.share_removed")))).await?,
    ))
}

// ---------- rendering ----------

async fn render_index(
    state: &Arc<AppState>,
    lang: Lang,
    notice: Option<(&str, &str)>,
) -> Result<String, ApiError> {
    let books = state
        .db
        .ab_list_all_with_owner()
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let notice_html = notice.map(|(k, m)| notice_html(k, m)).unwrap_or_default();
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div id="ab-region" class="space-y-6">
  <header>
    <h2 class="text-lg font-semibold">{heading}</h2>
    <p class="text-xs text-slate-500 mt-1">{tagline}</p>
  </header>

  {notice_html}

  <form
    class="flex items-end gap-2 bg-slate-900 border border-slate-800 rounded-lg p-3"
    hx-post="/admin/pages/address-books/create"
    hx-target="#ab-region" hx-swap="outerHTML"
  >
    <div class="flex-1">
      <label class="block text-xs font-medium text-slate-400 mb-1" for="ab-name">{new_shared}</label>
      <input id="ab-name" name="name" type="text" required
        placeholder="Engineering laptops"
        class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500" />
    </div>
    <button type="submit"
      class="bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium rounded px-4 py-2 transition">
      {create}
    </button>
  </form>
"##,
        heading = t(lang, "ab.heading"),
        tagline = t(lang, "ab.tagline"),
        new_shared = t(lang, "ab.new_shared"),
        create = t(lang, "common.create"),
    );
    if books.is_empty() {
        let _ = write!(
            s,
            r##"<p class="text-slate-500 text-sm">{}</p></div>"##,
            t(lang, "ab.no_books"),
        );
        return Ok(s);
    }
    let _ = write!(
        s,
        r##"<div class="rounded-md border border-slate-800 bg-slate-900">
  <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_owner}</th>
      <th class="text-left font-medium px-3 py-2">{c_kind}</th>
      <th class="text-left font-medium px-3 py-2">{c_name}</th>
      <th class="text-left font-medium px-3 py-2">{c_peers}</th>
      <th class="text-left font-medium px-3 py-2">{c_guid}</th>
      <th class="text-left font-medium px-3 py-2">{c_created}</th>
      <th class="text-right font-medium px-3 py-2 w-1">{c_actions}</th>
    </tr></thead>
    <tbody class="divide-y divide-slate-800">"##,
        c_owner = t(lang, "ab.col_owner"),
        c_kind = t(lang, "ab.col_kind"),
        c_name = t(lang, "ab.col_name"),
        c_peers = t(lang, "ab.col_peers"),
        c_guid = t(lang, "ab.col_guid"),
        c_created = t(lang, "ab.col_created"),
        c_actions = t(lang, "common.actions"),
    );
    for b in &books {
        let kind_pill = match b.kind {
            0 => format!(
                r#"<span class="text-xs px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 text-slate-300">{}</span>"#,
                t(lang, "ab.kind_personal"),
            ),
            1 => format!(
                r#"<span class="text-xs px-1.5 py-0.5 rounded bg-violet-900/40 border border-violet-700/50 text-violet-300">{}</span>"#,
                t(lang, "ab.kind_shared"),
            ),
            _ => String::new(),
        };
        // Both kinds get a delete action. Shared books additionally get
        // "Manage shares". Personal books carry an extra warning in the
        // confirm because the owning user's desktop client may resync
        // and recreate the book on next AB tick — deletion is "reset to
        // empty", not "permanently revoked".
        let actions = if b.kind == 1 {
            format!(
                r##"<details class="text-right relative">
      <summary class="cursor-pointer list-none text-xs text-slate-400 hover:text-slate-200 select-none">···</summary>
      <div class="absolute right-2 mt-1 z-10 w-48 bg-slate-900 border border-slate-700 rounded shadow-lg p-2 space-y-1 text-left">
        <button class="w-full text-left px-2 py-1 text-xs hover:bg-slate-800 rounded"
          hx-get="/admin/pages/address-books/{guid}/manage"
          hx-target="#ab-region" hx-swap="outerHTML">
          {manage}
        </button>
        <hr class="border-slate-700 my-1" />
        <button class="w-full text-left px-2 py-1 text-xs text-rose-300 hover:bg-rose-900/40 rounded"
          hx-post="/admin/pages/address-books/{guid}/delete"
          hx-target="#ab-region" hx-swap="outerHTML"
          hx-confirm="{confirm}">
          {delete}
        </button>
      </div>
    </details>"##,
                guid = html_escape(&b.guid),
                manage = t(lang, "ab.manage_shares"),
                confirm = html_escape(&tf1(lang, "ab.confirm_delete_shared", &b.name)),
                delete = t(lang, "ab.delete_book"),
            )
        } else {
            format!(
                r##"<details class="text-right relative">
      <summary class="cursor-pointer list-none text-xs text-slate-400 hover:text-slate-200 select-none">···</summary>
      <div class="absolute right-2 mt-1 z-10 w-56 bg-slate-900 border border-slate-700 rounded shadow-lg p-2 space-y-1 text-left">
        <button class="w-full text-left px-2 py-1 text-xs text-rose-300 hover:bg-rose-900/40 rounded"
          hx-post="/admin/pages/address-books/{guid}/delete"
          hx-target="#ab-region" hx-swap="outerHTML"
          hx-confirm="{confirm}">
          {delete}
        </button>
      </div>
    </details>"##,
                guid = html_escape(&b.guid),
                confirm = html_escape(&tf1(lang, "ab.confirm_delete_personal", &b.owner_username)),
                delete = t(lang, "ab.delete_book"),
            )
        };
        let _ = write!(
            s,
            r##"<tr>
  <td class="px-3 py-2 text-slate-200">{owner}</td>
  <td class="px-3 py-2">{kind}</td>
  <td class="px-3 py-2 text-slate-300">{name}</td>
  <td class="px-3 py-2 text-slate-400">{count}</td>
  <td class="px-3 py-2 font-mono text-xs text-slate-500">{guid}</td>
  <td class="px-3 py-2 text-slate-500 text-xs">{created}</td>
  <td class="px-3 py-2">{actions}</td>
</tr>"##,
            owner = html_escape(&b.owner_username),
            kind = kind_pill,
            name = html_escape(&b.name),
            count = b.peer_count,
            guid = html_escape(&b.guid),
            created = html_escape(&fmt_unix(b.created_at)),
            actions = actions,
        );
    }
    s.push_str("</tbody></table></div></div>");
    Ok(s)
}

async fn render_manage(
    state: &Arc<AppState>,
    lang: Lang,
    guid: &str,
    notice: Option<(&str, &str)>,
) -> Result<String, ApiError> {
    let owner_kind = state
        .db
        .ab_get_owner_kind(guid)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let Some((_owner_id, kind)) = owner_kind else {
        return Ok(format!(
            r##"<div id="ab-region">{notice}</div>"##,
            notice = notice_html("error", t(lang, "ab.not_found")),
        ));
    };
    if kind != 1 {
        return Ok(format!(
            r##"<div id="ab-region">{notice}</div>"##,
            notice = notice_html("error", t(lang, "ab.personal_managed_at_client")),
        ));
    }
    let shares = state
        .db
        .ab_list_shares(guid)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let (_total, users) = state
        .db
        .users_list_all(0, 1000)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let already_shared: std::collections::HashSet<i64> =
        shares.iter().map(|s| s.user_id).collect();

    let notice_html = notice.map(|(k, m)| notice_html(k, m)).unwrap_or_default();
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div id="ab-region" class="space-y-6">
  <header class="flex items-center justify-between">
    <div>
      <h2 class="text-lg font-semibold">{heading}</h2>
      <p class="text-xs text-slate-500 mt-1 font-mono">{guid}</p>
    </div>
    <button
      class="text-xs text-slate-300 hover:text-slate-100 px-2 py-1 rounded border border-slate-700 hover:border-slate-500"
      hx-get="/admin/pages/address-books"
      hx-target="#ab-region" hx-swap="outerHTML">
      {back}
    </button>
  </header>

  {notice_html}

  <section class="rounded-md border border-slate-800 bg-slate-900 p-4 space-y-3">
    <h3 class="text-sm font-semibold text-slate-200">{add_or_update}</h3>
    <form
      class="flex flex-wrap items-end gap-2"
      hx-post="/admin/pages/address-books/{guid}/shares/add"
      hx-target="#ab-region" hx-swap="outerHTML"
    >
      <div class="flex-1 min-w-[200px]">
        <label class="block text-xs font-medium text-slate-400 mb-1" for="share-user">{user_label}</label>
        <select id="share-user" name="user_id" required
          class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500">"##,
        heading = t(lang, "ab.manage_heading"),
        back = t(lang, "ab.back"),
        add_or_update = t(lang, "ab.add_or_update"),
        user_label = t(lang, "ab.user_label"),
        guid = html_escape(guid),
        notice_html = notice_html,
    );
    if users.is_empty() {
        let _ = write!(
            s,
            r#"<option disabled>{}</option>"#,
            t(lang, "ab.no_users"),
        );
    }
    let already_text = t(lang, "ab.existing_will_update");
    for u in &users {
        let already = if already_shared.contains(&u.id) { already_text } else { "" };
        let _ = write!(
            s,
            r#"<option value="{id}">{name}{already}</option>"#,
            id = u.id,
            name = html_escape(&u.username),
            already = html_escape(already),
        );
    }
    let _ = write!(
        s,
        r##"</select>
      </div>
      <div>
        <label class="block text-xs font-medium text-slate-400 mb-1" for="share-rule">{rule}</label>
        <select id="share-rule" name="rule"
          class="bg-slate-800 border border-slate-700 rounded px-3 py-2 text-sm focus:outline-none focus:border-sky-500">
          <option value="1">{r_read}</option>
          <option value="2" selected>{r_rw}</option>
          <option value="3">{r_full}</option>
        </select>
      </div>
      <button type="submit"
        class="bg-sky-600 hover:bg-sky-500 text-white text-sm font-medium rounded px-4 py-2 transition">
        {save}
      </button>
    </form>
  </section>

  <section class="rounded-md border border-slate-800 bg-slate-900">
    <header class="px-4 py-2 text-xs uppercase text-slate-500 border-b border-slate-800">
      {current_shares}
    </header>
"##,
        rule = t(lang, "ab.rule"),
        r_read = t(lang, "ab.rule_read"),
        r_rw = t(lang, "ab.rule_read_write"),
        r_full = t(lang, "ab.rule_full"),
        save = t(lang, "common.save"),
        current_shares = tf1(lang, "ab.current_shares", &shares.len().to_string()),
    );
    if shares.is_empty() {
        let _ = write!(
            s,
            r##"<p class="px-4 py-3 text-slate-500 text-sm">{}</p></section></div>"##,
            t(lang, "ab.no_shares"),
        );
        return Ok(s);
    }
    let _ = write!(
        s,
        r##"<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">{user_l}</th>
        <th class="text-left font-medium px-3 py-2">{rule_l}</th>
        <th class="text-right font-medium px-3 py-2 w-1"></th>
      </tr></thead>
      <tbody class="divide-y divide-slate-800">"##,
        user_l = t(lang, "ab.user_label"),
        rule_l = t(lang, "ab.rule"),
    );
    for sh in &shares {
        let rule = match sh.rule {
            1 => t(lang, "ab.rule_read"),
            2 => t(lang, "ab.rule_read_write"),
            3 => t(lang, "ab.rule_full"),
            _ => "?",
        };
        let _ = write!(
            s,
            r##"<tr>
  <td class="px-3 py-2 text-slate-200">{user}</td>
  <td class="px-3 py-2 text-slate-400">{rule}</td>
  <td class="px-3 py-2 text-right">
    <button class="text-xs text-rose-300 hover:text-rose-200 px-2 py-1 rounded hover:bg-rose-900/40"
      hx-post="/admin/pages/address-books/{guid}/shares/{uid}/remove"
      hx-target="#ab-region" hx-swap="outerHTML"
      hx-confirm="{confirm}">
      {remove}
    </button>
  </td>
</tr>"##,
            user = html_escape(&sh.username),
            rule = rule,
            guid = html_escape(guid),
            uid = sh.user_id,
            confirm = html_escape(&tf1(lang, "ab.confirm_remove", &sh.username)),
            remove = t(lang, "common.remove"),
        );
    }
    s.push_str("</tbody></table></section></div>");
    Ok(s)
}
