//! 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::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,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    Ok(Html(render_index(&state, None).await?))
}

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

pub async fn create(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    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, Some(("error", "Name is required."))).await?));
    }
    let res = state.db.ab_create_shared(admin.user_id, name).await;
    let notice = match res {
        Ok(_) => Some(("ok", format!("Shared address book '{}' 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") {
                "An address book with that name already exists.".to_string()
            } else {
                format!("Create failed: {}", e)
            };
            Some(("error", msg))
        }
    };
    let n = notice.as_ref().map(|(k, m)| (*k, m.as_str()));
    Ok(Html(render_index(&state, n).await?))
}

pub async fn delete(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    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", "Deleted.")
    } else {
        ("error", "Address book not found.")
    };
    Ok(Html(render_index(&state, Some(notice)).await?))
}

pub async fn manage(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    Path(guid): Path<String>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    Ok(Html(render_manage(&state, &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,
    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, &guid, Some(("error", "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, &guid, Some(("ok", "Share saved."))).await?))
}

pub async fn share_remove(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    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, &guid, Some(("ok", "Share removed."))).await?))
}

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

async fn render_index(
    state: &Arc<AppState>,
    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">Address books</h2>
    <p class="text-xs text-slate-500 mt-1">Personal books are owned by users and managed from their desktop client. Shared books are server-side: create one here, share it with users / rules, and the client picks it up on its next AB sync.</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 book</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>
"##
    );
    if books.is_empty() {
        s.push_str(r##"<p class="text-slate-500 text-sm">No address books exist yet.</p></div>"##);
        return Ok(s);
    }
    s.push_str(
        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">Owner</th>
      <th class="text-left font-medium px-3 py-2">Kind</th>
      <th class="text-left font-medium px-3 py-2">Name</th>
      <th class="text-left font-medium px-3 py-2">Peers</th>
      <th class="text-left font-medium px-3 py-2">GUID</th>
      <th class="text-left font-medium px-3 py-2">Created</th>
      <th class="text-right font-medium px-3 py-2 w-1">Actions</th>
    </tr></thead>
    <tbody class="divide-y divide-slate-800">"##,
    );
    for b in &books {
        let kind_pill = match b.kind {
            0 => r#"<span class="text-xs px-1.5 py-0.5 rounded bg-slate-800 border border-slate-700 text-slate-300">personal</span>"#,
            1 => r#"<span class="text-xs px-1.5 py-0.5 rounded bg-violet-900/40 border border-violet-700/50 text-violet-300">shared</span>"#,
            _ => "",
        };
        // 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 shares
        </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="Delete shared book '{name}'? Peers, tags, and shares will be removed.">
          Delete book
        </button>
      </div>
    </details>"##,
                guid = html_escape(&b.guid),
                name = html_escape(&b.name)
            )
        } 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="Delete {owner}'s personal book? This wipes all peers and tags on the server. If {owner}'s desktop client is still signed in, it will recreate an empty personal book on its next sync (~30 s).">
          Delete book
        </button>
      </div>
    </details>"##,
                guid = html_escape(&b.guid),
                owner = html_escape(&b.owner_username)
            )
        };
        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>,
    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", "Address book not found."),
        ));
    };
    if kind != 1 {
        return Ok(format!(
            r##"<div id="ab-region">{notice}</div>"##,
            notice = notice_html("error", "Personal address books are managed from the desktop 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">Manage shares</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 a share</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>
        <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">"##,
        guid = html_escape(guid),
        notice_html = notice_html,
    );
    if users.is_empty() {
        s.push_str(r#"<option disabled>No users defined</option>"#);
    }
    for u in &users {
        let already = if already_shared.contains(&u.id) { " (existing — will update rule)" } 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">Read</option>
          <option value="2" selected>Read + write</option>
          <option value="3">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 ({n})
    </header>
"##,
        n = shares.len()
    );
    if shares.is_empty() {
        s.push_str(r##"<p class="px-4 py-3 text-slate-500 text-sm">No shares yet. The book is invisible to non-owners until you add at least one user share.</p></section></div>"##);
        return Ok(s);
    }
    s.push_str(
        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</th>
        <th class="text-left font-medium px-3 py-2">Rule</th>
        <th class="text-right font-medium px-3 py-2 w-1"></th>
      </tr></thead>
      <tbody class="divide-y divide-slate-800">"##,
    );
    for sh in &shares {
        let rule = match sh.rule {
            1 => "Read",
            2 => "Read + write",
            3 => "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="Remove {user}'s access?">
      Remove
    </button>
  </td>
</tr>"##,
            user = html_escape(&sh.username),
            rule = rule,
            guid = html_escape(guid),
            uid = sh.user_id,
        );
    }
    s.push_str("</tbody></table></section></div>");
    Ok(s)
}
