//! Strategies page — list / create / edit-config / delete, plus the
//! assignment matrix that decides which clients receive each strategy.
//! Assignments are scoped to device groups or individual peers; user-level
//! assignments are still SQL-driven (rare in practice).

use super::shared::{html_escape, notice_html, require_admin};
use crate::api::admin::i18n::{t, tf1, tf2, 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,
    #[serde(default)]
    pub config_options_json: 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;
    }
    let cfg = if form.config_options_json.trim().is_empty() {
        "{}".to_string()
    } else {
        // Validate it's a JSON object — empty object is fine, anything else
        // gets rejected with a friendly message.
        match serde_json::from_str::<serde_json::Value>(&form.config_options_json) {
            Ok(v) if v.is_object() => form.config_options_json.clone(),
            Ok(_) => {
                return notice_then(&state, lang, "error", t(lang, "strategies.json_obj_required")).await
            }
            Err(e) => {
                return notice_then(
                    &state,
                    lang,
                    "error",
                    &tf1(lang, "strategies.invalid_json", &e.to_string()),
                )
                .await
            }
        }
    };
    state
        .db
        .strategy_create(form.name.trim(), &cfg, "{}")
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(
        &state,
        lang,
        "ok",
        &tf1(lang, "strategies.created", &form.name),
    )
    .await
}

#[derive(Debug, Deserialize)]
pub struct UpdateForm {
    pub config_options_json: String,
}

pub async fn update(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
    Form(form): Form<UpdateForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    let cfg = match serde_json::from_str::<serde_json::Value>(&form.config_options_json) {
        Ok(v) if v.is_object() => form.config_options_json.clone(),
        _ => {
            return notice_then(&state, lang, "error", t(lang, "strategies.json_obj_required")).await
        }
    };
    state
        .db
        .strategy_update_config(id, &cfg)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(&state, lang, "ok", t(lang, "strategies.updated")).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
        .strategy_delete(id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(
        &state,
        lang,
        if ok { "ok" } else { "error" },
        if ok { t(lang, "strategies.deleted") } else { t(lang, "common.already_gone") },
    )
    .await
}

#[derive(Debug, Deserialize)]
pub struct AssignGroupForm {
    pub device_group_id: i64,
}

pub async fn assign_group(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
    Form(form): Form<AssignGroupForm>,
) -> Result<Html<String>, ApiError> {
    require_admin(&admin)?;
    state
        .db
        .strategy_assign_group(id, form.device_group_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(&state, lang, "ok", t(lang, "strategies.group_assigned")).await
}

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

pub async fn assign_peer(
    Extension(state): Extension<Arc<AppState>>,
    admin: AuthedUser,
    lang: Lang,
    Path(id): Path<i64>,
    Form(form): Form<AssignPeerForm>,
) -> 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
        .strategy_assign_peer_replace(id, peer_id)
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    notice_then(
        &state,
        lang,
        "ok",
        &tf1(lang, "strategies.peer_assigned", peer_id),
    )
    .await
}

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

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

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 strategies = state
        .db
        .strategies_list_all()
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let all_groups = state
        .db
        .device_groups_list_all()
        .await
        .map_err(|e| ApiError::Internal(e.to_string()))?;
    let mut s = String::new();
    let _ = write!(
        s,
        r##"<div id="strategies-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>
  <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="space-y-2 text-sm" hx-post="/admin/pages/strategies/create" hx-target="#strategies-region" hx-swap="outerHTML">
      <input name="name" placeholder="{ph}" required class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5"/>
      <textarea name="config_options_json" rows="3" placeholder='{{"enable-udp": "N", "whitelist": ""}}'
        class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 font-mono text-xs"></textarea>
      <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, "strategies.heading"),
        tagline = t(lang, "strategies.tagline"),
        create_heading = t(lang, "strategies.create_heading"),
        ph = t(lang, "strategies.name_unique"),
        create = t(lang, "common.create"),
    );
    if strategies.is_empty() {
        let _ = write!(
            s,
            r##"<p class="text-slate-500 text-sm">{}</p>"##,
            t(lang, "strategies.no_strategies"),
        );
    }
    for str_ in &strategies {
        let assignments = state
            .db
            .strategy_assignments_for(str_.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-3">
  <header class="flex items-center justify-between">
    <div>
      <h3 class="font-semibold">{name}</h3>
      <p class="text-xs text-slate-500">{meta}</p>
    </div>
    <button class="text-xs text-rose-400 hover:text-rose-300"
      hx-post="/admin/pages/strategies/{id}/delete"
      hx-confirm="{confirm}"
      hx-target="#strategies-region" hx-swap="outerHTML">{delete}</button>
  </header>
  <form class="space-y-2 text-sm"
    hx-post="/admin/pages/strategies/{id}/update"
    hx-target="#strategies-region" hx-swap="outerHTML">
    <label class="block text-xs text-slate-400">{cfg_label}</label>
    <textarea name="config_options_json" rows="4"
      class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 font-mono text-xs">{cfg}</textarea>
    <button class="bg-sky-700 hover:bg-sky-600 rounded px-3 py-1.5 text-xs">{save}</button>
  </form>"##,
            id = str_.id,
            name = html_escape(&str_.name),
            meta = tf2(lang, "strategies.id_modified", &str_.id.to_string(), &str_.modified_at.to_string()),
            cfg = html_escape(&str_.config_options_json),
            confirm = html_escape(&tf1(lang, "strategies.confirm_delete", &str_.name)),
            delete = t(lang, "common.delete"),
            cfg_label = t(lang, "strategies.config_label"),
            save = t(lang, "common.save"),
        );

        // ---- Assignments section ----
        render_assignments(&mut s, lang, str_.id, &assignments, &all_groups);

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

fn render_assignments(
    s: &mut String,
    lang: Lang,
    strategy_id: i64,
    assignments: &[crate::database::StrategyAssignmentRow],
    all_groups: &[crate::database::DeviceGroupRow],
) {
    let group_assigned: std::collections::HashSet<i64> = assignments
        .iter()
        .filter_map(|a| a.device_group_id)
        .collect();
    let group_rows: Vec<&crate::database::StrategyAssignmentRow> = assignments
        .iter()
        .filter(|a| a.device_group_id.is_some())
        .collect();
    let peer_rows: Vec<&crate::database::StrategyAssignmentRow> = assignments
        .iter()
        .filter(|a| a.peer_id.is_some())
        .collect();

    let _ = write!(
        s,
        r##"<div class="pt-3 border-t border-slate-800 space-y-3">
  <h4 class="text-xs font-semibold text-slate-400 uppercase tracking-wide">{filter_heading}</h4>
  <p class="text-[11px] text-slate-500">{filter_hint}</p>"##,
        filter_heading = t(lang, "strategies.filter_heading"),
        filter_hint = t(lang, "strategies.filter_hint"),
    );

    // ---- Device groups ----
    let _ = write!(
        s,
        r##"<div>
    <div class="text-[11px] font-semibold text-slate-400 mb-1">{groups_label}</div>
    <ul class="text-sm divide-y divide-slate-800">"##,
        groups_label = t(lang, "strategies.groups_label"),
    );
    if group_rows.is_empty() {
        let _ = write!(
            s,
            r##"<li class="py-2 text-slate-500 text-xs">{}</li>"##,
            t(lang, "strategies.no_group_assignments"),
        );
    }
    for a in &group_rows {
        let name = a
            .device_group_name
            .as_deref()
            .unwrap_or("(deleted group)");
        let _ = write!(
            s,
            r##"<li class="py-2 flex items-center justify-between">
  <span class="text-slate-200">{name}</span>
  <button class="text-xs text-slate-400 hover:text-rose-300"
    hx-post="/admin/pages/strategies/{sid}/assignments/{aid}/delete"
    hx-target="#strategies-region" hx-swap="outerHTML">{remove}</button>
</li>"##,
            name = html_escape(name),
            sid = strategy_id,
            aid = a.id,
            remove = t(lang, "common.remove"),
        );
    }
    s.push_str("</ul>");
    let candidates: Vec<&crate::database::DeviceGroupRow> = all_groups
        .iter()
        .filter(|g| !group_assigned.contains(&g.id))
        .collect();
    if !candidates.is_empty() {
        let _ = write!(
            s,
            r##"<form class="flex gap-2 text-sm pt-2"
  hx-post="/admin/pages/strategies/{sid}/assignments/group"
  hx-target="#strategies-region" hx-swap="outerHTML">
  <select name="device_group_id" class="flex-1 bg-slate-800 border border-slate-700 rounded px-2 py-1.5">
"##,
            sid = strategy_id,
        );
        for g in &candidates {
            let _ = write!(
                s,
                r##"<option value="{gid}">{name}</option>"##,
                gid = g.id,
                name = html_escape(&g.name),
            );
        }
        let _ = write!(
            s,
            r##"</select>
  <button class="bg-sky-700 hover:bg-sky-600 rounded px-3 py-1.5 text-xs">{assign}</button>
</form>"##,
            assign = t(lang, "strategies.assign_group"),
        );
    } else if !all_groups.is_empty() {
        let _ = write!(
            s,
            r##"<p class="text-[11px] text-slate-500 pt-1">{}</p>"##,
            t(lang, "strategies.all_groups_assigned"),
        );
    } else {
        let _ = write!(
            s,
            r##"<p class="text-[11px] text-slate-500 pt-1">{}</p>"##,
            t(lang, "strategies.no_groups_exist"),
        );
    }
    s.push_str("</div>");

    // ---- Peers ----
    let _ = write!(
        s,
        r##"<div>
    <div class="text-[11px] font-semibold text-slate-400 mb-1">{peers_label}</div>
    <ul class="text-sm divide-y divide-slate-800">"##,
        peers_label = t(lang, "strategies.peers_label"),
    );
    if peer_rows.is_empty() {
        let _ = write!(
            s,
            r##"<li class="py-2 text-slate-500 text-xs">{}</li>"##,
            t(lang, "strategies.no_peer_assignments"),
        );
    }
    for a in &peer_rows {
        let pid = a.peer_id.as_deref().unwrap_or("");
        let _ = write!(
            s,
            r##"<li class="py-2 flex items-center justify-between">
  <span class="font-mono text-slate-200">{pid}</span>
  <button class="text-xs text-slate-400 hover:text-rose-300"
    hx-post="/admin/pages/strategies/{sid}/assignments/{aid}/delete"
    hx-target="#strategies-region" hx-swap="outerHTML">{remove}</button>
</li>"##,
            pid = html_escape(pid),
            sid = strategy_id,
            aid = a.id,
            remove = t(lang, "common.remove"),
        );
    }
    s.push_str("</ul>");
    let _ = write!(
        s,
        r##"<form class="flex gap-2 text-sm pt-2"
  hx-post="/admin/pages/strategies/{sid}/assignments/peer"
  hx-target="#strategies-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">{assign}</button>
</form>"##,
        sid = strategy_id,
        ph = t(lang, "groups.peer_id_placeholder"),
        assign = t(lang, "strategies.assign_peer"),
    );
    s.push_str("</div></div>");
}
