//! Device-groups page — list, create, delete, add/remove member.
//! Strategies and AB shares hang off device-group membership, so this is
//! the canonical place to manage who can see whose devices.

use super::shared::{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_full(&state, lang).await?))
}

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

pub async fn create(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Form(form): Form<CreateForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    if form.name.trim().is_empty() {
        return notice_then(&state, lang, "error", t(lang, "groups.name_required")).await;
    }
    state
        .db
        .device_group_create(form.name.trim())
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(&state, lang, "ok", &tf1(lang, "groups.created", &form.name)).await
}

pub async fn delete(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let ok = state
        .db
        .device_group_delete(id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(
        &state,
        lang,
        if ok { "ok" } else { "error" },
        if ok { t(lang, "groups.deleted") } else { t(lang, "common.already_gone") },
    )
    .await
}

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

pub async fn add_member(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
    Form(form): Form<MemberForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    state
        .db
        .device_group_add_member(id, form.user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(render_full(&state, lang).await?))
}

pub async fn remove_member(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path((id, user_id)): Path<(i64, i64)>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    state
        .db
        .device_group_remove_member(id, user_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(render_full(&state, lang).await?))
}

#[derive(Debug, Deserialize)]
pub struct PeerForm {
    pub peer_id: String,
}

pub async fn add_peer(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
    Form(form): Form<PeerForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let peer_id = form.peer_id.trim();
    if peer_id.is_empty() {
        return notice_then(&state, lang, "error", t(lang, "groups.peer_id_required")).await;
    }
    let exists = state
        .db
        .peer_exists(peer_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    if !exists {
        return notice_then(
            &state,
            lang,
            "error",
            &tf1(lang, "groups.no_device_yet", peer_id),
        )
        .await;
    }
    state
        .db
        .device_group_add_peer(id, peer_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(render_full(&state, lang).await?))
}

pub async fn remove_peer(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path((id, peer_id)): Path<(i64, String)>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    state
        .db
        .device_group_remove_peer(id, &peer_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    Ok(Html(render_full(&state, lang).await?))
}

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

/// Minimal percent-encoder for path segments. Peer IDs are usually digits,
/// but the schema allows arbitrary text — encode anything outside the
/// unreserved set so a literal `/` or `?` in a peer id can't break routing.
fn url_encode(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for b in s.as_bytes() {
        match b {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
                out.push(*b as char);
            }
            _ => {
                use std::fmt::Write;
                let _ = write!(out, "%{:02X}", b);
            }
        }
    }
    out
}

async fn notice_then(
    state: &Arc<AppState>,
    lang: Lang,
    kind: &str,
    msg: &str,
) -> Result<Html<String>, ApiError> {
    let mut html = notice_html(kind, msg);
    html.push_str(&render_full(state, lang).await?);
    Ok(Html(html))
}

async fn render_full(state: &Arc<AppState>, lang: Lang) -> Result<String, ApiError> {
    let groups = state
        .db
        .device_groups_list_all()
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let (_, all_users) = state
        .db
        .users_list_all(0, 1000)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;

    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div id="groups-region" class="space-y-6">
  <header><h2 class="text-lg font-semibold">{heading}</h2></header>
  <section class="rounded-md border border-slate-800 bg-slate-900 p-4">
    <h3 class="text-sm font-semibold text-slate-300 mb-3">{create_heading}</h3>
    <form class="flex gap-2 text-sm" hx-post="/admin/pages/groups/create" hx-target="#groups-region" hx-swap="outerHTML">
      <input name="name" placeholder="{ph}" required class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
      <button class="bg-sky-600 hover:bg-sky-500 rounded px-3 py-1.5 font-medium text-white">{create}</button>
    </form>
  </section>
"##,
        heading = t(lang, "groups.heading"),
        create_heading = t(lang, "groups.create_heading"),
        ph = t(lang, "groups.group_name"),
        create = t(lang, "common.create"),
    );

    if groups.is_empty() {
        let _ = write!(
            s,
            r##"<p class="text-slate-500 text-sm">{}</p>"##,
            t(lang, "groups.no_groups"),
        );
    }
    for g in &groups {
        let members = state
            .db
            .device_group_members(g.id)
            .await
            .map_err(|e| ApiError::Internal(e.to_string()))?;
        let peer_members = state
            .db
            .device_group_peer_members(g.id)
            .await
            .map_err(|e| ApiError::Internal(e.to_string()))?;
        let _ = write!(
            s,
            r##"<section class="rounded-md border border-slate-800 bg-slate-900 p-4 space-y-4">
  <header class="flex items-center justify-between">
    <h3 class="font-semibold">{name}</h3>
    <button class="text-xs text-rose-400 hover:text-rose-300"
      hx-post="/admin/pages/groups/{id}/delete"
      hx-confirm="{confirm}"
      hx-target="#groups-region" hx-swap="outerHTML">{delete_group}</button>
  </header>
  <div>
    <h4 class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-1">{users}</h4>
    <ul class="text-sm divide-y divide-slate-800">"##,
            id = g.id,
            name = html_escape(&g.name),
            confirm = html_escape(&tf1(lang, "groups.confirm_delete", &g.name)),
            delete_group = t(lang, "groups.delete_group"),
            users = t(lang, "groups.users"),
        );
        if members.is_empty() {
            let _ = write!(
                s,
                r##"<li class="py-2 text-slate-500 text-xs">{}</li>"##,
                t(lang, "groups.no_user_members"),
            );
        }
        for u in &members {
            let _ = write!(
                s,
                r##"<li class="py-2 flex items-center justify-between">
  <span class="text-slate-200">{username}</span>
  <button class="text-xs text-slate-400 hover:text-rose-300"
    hx-post="/admin/pages/groups/{gid}/members/{uid}/remove"
    hx-target="#groups-region" hx-swap="outerHTML">{remove}</button>
</li>"##,
                username = html_escape(&u.username),
                gid = g.id,
                uid = u.id,
                remove = t(lang, "common.remove"),
            );
        }
        s.push_str("</ul>");
        // Add-member form: dropdown of users not currently in the group.
        let in_group: std::collections::HashSet<i64> =
            members.iter().map(|u| u.id).collect();
        let candidates: Vec<_> =
            all_users.iter().filter(|u| !in_group.contains(&u.id)).collect();
        if !candidates.is_empty() {
            let _ = write!(
                s,
                r##"<form class="flex gap-2 text-sm pt-2 border-t border-slate-800"
  hx-post="/admin/pages/groups/{id}/members/add"
  hx-target="#groups-region" hx-swap="outerHTML">
  <select name="user_id" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1.5">
"##,
                id = g.id
            );
            for u in &candidates {
                let _ = write!(
                    s,
                    r##"<option value="{uid}">{username}</option>"##,
                    uid = u.id,
                    username = html_escape(&u.username)
                );
            }
            let _ = write!(
                s,
                r##"</select>
  <button class="bg-sky-700 hover:bg-sky-600 rounded px-3 py-1.5 text-xs">{add_user}</button>
</form>"##,
                add_user = t(lang, "groups.add_user"),
            );
        }
        s.push_str("</div>");

        // ---- Devices section ----
        let _ = write!(
            s,
            r##"<div>
    <h4 class="text-xs font-semibold text-slate-400 uppercase tracking-wide mb-1">{devices}</h4>
    <ul class="text-sm divide-y divide-slate-800">"##,
            devices = t(lang, "groups.devices_section"),
        );
        if peer_members.is_empty() {
            let _ = write!(
                s,
                r##"<li class="py-2 text-slate-500 text-xs">{}</li>"##,
                t(lang, "groups.no_peer_members"),
            );
        }
        for (peer_id, owner) in &peer_members {
            let owner_label = if owner.is_empty() {
                format!(
                    r##"<span class="text-slate-500">{}</span>"##,
                    t(lang, "groups.unowned"),
                )
            } else {
                format!(
                    r##"<span class="text-slate-500">{}</span>"##,
                    html_escape(&tf1(lang, "groups.owner_label", owner)),
                )
            };
            let _ = write!(
                s,
                r##"<li class="py-2 flex items-center justify-between">
  <span class="font-mono text-slate-200">{pid}</span>
  <span class="text-xs flex items-center gap-3">
    {owner_label}
    <button class="text-slate-400 hover:text-rose-300"
      hx-post="/admin/pages/groups/{gid}/peers/{pid_url}/remove"
      hx-target="#groups-region" hx-swap="outerHTML">{remove}</button>
  </span>
</li>"##,
                pid = html_escape(peer_id),
                pid_url = url_encode(peer_id),
                gid = g.id,
                owner_label = owner_label,
                remove = t(lang, "common.remove"),
            );
        }
        s.push_str("</ul>");
        let _ = write!(
            s,
            r##"<form class="flex gap-2 text-sm pt-2 border-t border-slate-800"
  hx-post="/admin/pages/groups/{id}/peers/add"
  hx-target="#groups-region" hx-swap="outerHTML">
  <input name="peer_id" placeholder="{ph}" required
    class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1.5 font-mono"/>
  <button class="bg-sky-700 hover:bg-sky-600 rounded px-3 py-1.5 text-xs">{add_device}</button>
</form></div>"##,
            id = g.id,
            ph = t(lang, "groups.peer_id_placeholder"),
            add_device = t(lang, "groups.add_device"),
        );

        s.push_str("</section>");
    }
    s.push_str("</div>");
    Ok(s)
}
