// B2b — Tauri commands wrapping kon_storage templates CRUD.
//
// The frontend stores templates as `{ name, sections }` objects. This
// command layer is responsible for the JSON (de)serialisation of the
// `sections` column (the storage layer keeps it as opaque TEXT) and for
// minting fresh ids during the localStorage → SQLite import.

use serde::{Deserialize, Serialize};

use kon_storage::{
    create_template as db_create_template, delete_template as db_delete_template,
    import_templates as db_import_templates, list_templates as db_list_templates,
    update_template as db_update_template, ImportSummary, TemplateRow,
};

use crate::AppState;

/// Frontend-facing template shape. Sections come over the wire as a real
/// array — the JSON encoding is an implementation detail of the storage
/// row that callers shouldn't have to think about.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateDto {
    pub id: String,
    pub name: String,
    pub sections: Vec<String>,
    pub created_at: String,
    pub updated_at: String,
}

impl TryFrom<TemplateRow> for TemplateDto {
    type Error = String;

    fn try_from(r: TemplateRow) -> Result<Self, String> {
        // Malformed sections_json is a soft failure: log + return empty
        // sections rather than failing the whole list. A user will see
        // an empty template they can edit, instead of a broken page.
        let sections: Vec<String> = match serde_json::from_str(&r.sections_json) {
            Ok(v) => v,
            Err(e) => {
                eprintln!(
                    "[templates] sections_json parse failed for template {} ({}): {}",
                    r.id, r.name, e
                );
                Vec::new()
            }
        };
        Ok(Self {
            id: r.id,
            name: r.name,
            sections,
            created_at: r.created_at,
            updated_at: r.updated_at,
        })
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTemplateRequest {
    pub id: String,
    pub name: String,
    pub sections: Vec<String>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTemplatePatch {
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default)]
    pub sections: Option<Vec<String>>,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateImport {
    /// Optional — if absent, the server mints a uuid v4. The localStorage
    /// shape pre-B2b had no id, so the import path needs to backfill it.
    #[serde(default)]
    pub id: Option<String>,
    pub name: String,
    pub sections: Vec<String>,
    /// Accepted but ignored — the DB sets datetime('now') at insert time.
    #[serde(default)]
    pub created_at: Option<String>,
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportSummaryDto {
    pub imported: u32,
    pub skipped: u32,
}

impl From<ImportSummary> for ImportSummaryDto {
    fn from(s: ImportSummary) -> Self {
        Self {
            imported: s.imported as u32,
            skipped: s.skipped as u32,
        }
    }
}

#[tauri::command]
pub async fn list_templates_cmd(
    state: tauri::State<'_, AppState>,
) -> Result<Vec<TemplateDto>, String> {
    let rows = db_list_templates(&state.db).await.map_err(|e| e.to_string())?;
    let mut out = Vec::with_capacity(rows.len());
    for r in rows {
        // TryFrom returns Ok with an empty sections vec on parse failure —
        // there's no error path that bubbles up here today, but keep the
        // ?-style plumbing in case the contract tightens.
        out.push(TemplateDto::try_from(r)?);
    }
    Ok(out)
}

#[tauri::command]
pub async fn create_template_cmd(
    state: tauri::State<'_, AppState>,
    request: CreateTemplateRequest,
) -> Result<TemplateDto, String> {
    let sections_json = serde_json::to_string(&request.sections)
        .map_err(|e| format!("create_template: serialise sections failed: {e}"))?;
    let row = db_create_template(&state.db, &request.id, &request.name, &sections_json)
        .await
        .map_err(|e| e.to_string())?;
    TemplateDto::try_from(row)
}

#[tauri::command]
pub async fn update_template_cmd(
    state: tauri::State<'_, AppState>,
    id: String,
    patch: UpdateTemplatePatch,
) -> Result<TemplateDto, String> {
    let sections_json = match patch.sections {
        Some(s) => Some(
            serde_json::to_string(&s)
                .map_err(|e| format!("update_template: serialise sections failed: {e}"))?,
        ),
        None => None,
    };
    let row = db_update_template(
        &state.db,
        &id,
        patch.name.as_deref(),
        sections_json.as_deref(),
    )
    .await
    .map_err(|e| e.to_string())?;
    TemplateDto::try_from(row)
}

#[tauri::command]
pub async fn delete_template_cmd(
    state: tauri::State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    db_delete_template(&state.db, &id)
        .await
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn import_templates_cmd(
    state: tauri::State<'_, AppState>,
    items: Vec<TemplateImport>,
) -> Result<ImportSummaryDto, String> {
    let mut rows: Vec<TemplateRow> = Vec::with_capacity(items.len());
    for item in items {
        let sections_json = serde_json::to_string(&item.sections)
            .map_err(|e| format!("import_templates: serialise sections failed: {e}"))?;
        let id = item.id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        rows.push(TemplateRow {
            id,
            name: item.name,
            sections_json,
            // The DB column has DEFAULT datetime('now'); these placeholder
            // strings are overwritten by the INSERT for the live row and
            // are only used to build the in-memory TemplateRow shape
            // passed to import_templates.
            created_at: item.created_at.unwrap_or_default(),
            updated_at: String::new(),
        });
    }
    db_import_templates(&state.db, &rows)
        .await
        .map(ImportSummaryDto::from)
        .map_err(|e| e.to_string())
}
