// Tauri commands wrapping the lumotia_storage transcript CRUD.
// These are the bridge that lets the Svelte frontend treat SQLite as the
// canonical store rather than localStorage.
//
// Day 4 of the upgrade plan. The frontend HistoryPage rename flow has had
// a `// TODO: persist to SQLite when update_transcript exists` for some
// time; that command now exists.
//
// Task 16 — the legacy global-dictionary commands
// (`list_dictionary_command`, `add_dictionary_entry_command`,
// `delete_dictionary_entry_command`) were dropped; profile-scoped
// `profile_terms` commands in `commands::profiles` are now canonical.

use serde::{Deserialize, Serialize};

use lumotia_storage::{
    delete_transcript as db_delete_transcript, get_transcript as db_get_transcript,
    insert_transcript as db_insert_transcript, list_transcripts_paged,
    list_trashed_transcripts as db_list_trashed_transcripts,
    restore_transcript as db_restore_transcript, search_transcripts as db_search_transcripts,
    update_transcript as db_update_transcript, update_transcript_meta as db_update_transcript_meta,
    InsertTranscriptParams, TranscriptRow,
};

use crate::AppState;

/// Frontend-facing shape of a stored transcript. Mirrors `TranscriptRow`
/// from the storage crate but drops fields the UI does not need yet
/// (sample_rate, audio_channels, format flags) and renames to camelCase.
///
/// Task 2.5 — `starred`, `manualTags`, `template`, `language`, `segmentsJson`
/// were added to back the viewer metadata that previously lived only in the
/// removed `lumotia_history` localStorage cache.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptDto {
    pub id: String,
    pub text: String,
    pub source: String,
    pub profile_id: String,
    pub title: Option<String>,
    pub audio_path: Option<String>,
    pub duration: f64,
    pub engine: Option<String>,
    pub model_id: Option<String>,
    pub created_at: String,
    pub starred: bool,
    pub manual_tags: String,
    pub template: String,
    pub language: String,
    pub segments_json: String,
    /// Phase 9 LLM-generated content tags ("topic:...", "intent:...").
    pub llm_tags: String,
}

impl From<TranscriptRow> for TranscriptDto {
    fn from(r: TranscriptRow) -> Self {
        Self {
            id: r.id,
            text: r.text,
            source: r.source,
            profile_id: r.profile_id,
            title: r.title,
            audio_path: r.audio_path,
            duration: r.duration,
            engine: r.engine,
            model_id: r.model_id,
            created_at: r.created_at,
            starred: r.starred,
            manual_tags: r.manual_tags,
            template: r.template,
            language: r.language,
            segments_json: r.segments_json,
            llm_tags: r.llm_tags,
        }
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTranscriptRequest {
    pub id: String,
    pub text: String,
    pub source: String,
    pub profile_id: Option<String>,
    pub title: Option<String>,
    pub audio_path: Option<String>,
    pub duration: f64,
    pub engine: Option<String>,
    pub model_id: Option<String>,
    pub inference_ms: Option<i64>,
    pub sample_rate: Option<i64>,
    pub audio_channels: Option<i64>,
    pub format_mode: Option<String>,
    pub remove_fillers: bool,
    pub british_english: bool,
    pub anti_hallucination: bool,
}

/// Insert a transcript into the canonical SQLite store. Called from the
/// frontend after a recording or file transcription completes. The FTS5
/// index is updated automatically by trigger.
#[tauri::command]
pub async fn add_transcript(
    state: tauri::State<'_, AppState>,
    transcript: CreateTranscriptRequest,
) -> Result<(), String> {
    let params = InsertTranscriptParams {
        id: &transcript.id,
        text: &transcript.text,
        source: &transcript.source,
        profile_id: transcript
            .profile_id
            .as_deref()
            .unwrap_or(lumotia_storage::DEFAULT_PROFILE_ID),
        title: transcript.title.as_deref(),
        audio_path: transcript.audio_path.as_deref(),
        duration: transcript.duration,
        engine: transcript.engine.as_deref(),
        model_id: transcript.model_id.as_deref(),
        inference_ms: transcript.inference_ms,
        sample_rate: transcript.sample_rate,
        audio_channels: transcript.audio_channels,
        format_mode: transcript.format_mode.as_deref(),
        remove_fillers: transcript.remove_fillers,
        british_english: transcript.british_english,
        anti_hallucination: transcript.anti_hallucination,
    };
    db_insert_transcript(&state.db, &params)
        .await
        .map_err(|e| e.to_string())
}

/// Paginated history list. Defaults to 50 rows from the most recent.
#[tauri::command]
pub async fn list_transcripts(
    state: tauri::State<'_, AppState>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> Result<Vec<TranscriptDto>, String> {
    let limit = limit.unwrap_or(50).clamp(1, 500);
    let offset = offset.unwrap_or(0).max(0);
    list_transcripts_paged(&state.db, limit, offset)
        .await
        .map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_transcript(
    state: tauri::State<'_, AppState>,
    id: String,
) -> Result<Option<TranscriptDto>, String> {
    db_get_transcript(&state.db, &id)
        .await
        .map(|opt| opt.map(TranscriptDto::from))
        .map_err(|e| e.to_string())
}

/// Update mutable fields of an existing transcript. `text` and/or `title`.
/// Returns the row count affected (0 if id not found). Fixes the long-
/// standing "rename in History never persists" bug per
/// architecture-review.md §13.
#[tauri::command]
pub async fn update_transcript(
    state: tauri::State<'_, AppState>,
    id: String,
    text: Option<String>,
    title: Option<String>,
) -> Result<u64, String> {
    db_update_transcript(&state.db, &id, text.as_deref(), title.as_deref())
        .await
        .map_err(|e| e.to_string())
}

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

/// List soft-deleted transcripts for the Trash view. Mirrors
/// `list_transcripts` shape (defaults, clamps) but reads the
/// `deleted_at IS NOT NULL` partition so the trash is the exact
/// complement of the regular history list.
#[tauri::command]
pub async fn list_trashed_transcripts(
    state: tauri::State<'_, AppState>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> Result<Vec<TranscriptDto>, String> {
    let limit = limit.unwrap_or(50).clamp(1, 500);
    let offset = offset.unwrap_or(0).max(0);
    db_list_trashed_transcripts(&state.db, limit, offset)
        .await
        .map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
        .map_err(|e| e.to_string())
}

/// Restore a soft-deleted transcript by clearing its `deleted_at`
/// timestamp. Idempotent: restoring a live row is a no-op. Note that
/// the audio file at `audio_path` may already have been removed by
/// `delete_transcript`'s best-effort filesystem cleanup; restoring
/// recovers the text + metadata but the audio may be gone.
#[tauri::command]
pub async fn restore_transcript(
    state: tauri::State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    db_restore_transcript(&state.db, &id)
        .await
        .map_err(|e| e.to_string())
}

/// FTS5 search. Query syntax: bare words AND together; quote phrases;
/// supports `OR`, `NOT`, prefix `*`. Returns up to 50 best-rank matches.
#[tauri::command]
pub async fn search_transcripts(
    state: tauri::State<'_, AppState>,
    query: String,
) -> Result<Vec<TranscriptDto>, String> {
    let q = query.trim();
    if q.is_empty() {
        return Ok(Vec::new());
    }
    db_search_transcripts(&state.db, q, 50)
        .await
        .map(|rows| rows.into_iter().map(TranscriptDto::from).collect())
        .map_err(|e| e.to_string())
}

/// Task 2.5 — patch-style update for the transcripts_meta columns. Each
/// field is optional: `Some` overwrites, omitted / `None` preserves via
/// COALESCE server-side. Separate from `update_transcript` (text / title)
/// so the existing command surface stays untouched.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTranscriptMetaRequest {
    #[serde(default)]
    pub starred: Option<bool>,
    #[serde(default)]
    pub manual_tags: Option<String>,
    #[serde(default)]
    pub template: Option<String>,
    #[serde(default)]
    pub language: Option<String>,
    #[serde(default)]
    pub segments_json: Option<String>,
    /// Phase 9 LLM content tags. Same comma-joined string convention as
    /// `manual_tags`. Pass `None` to leave unchanged; pass `Some("")` to
    /// explicitly clear.
    #[serde(default)]
    pub llm_tags: Option<String>,
}

#[tauri::command]
pub async fn update_transcript_meta_cmd(
    state: tauri::State<'_, AppState>,
    id: String,
    patch: UpdateTranscriptMetaRequest,
) -> Result<TranscriptDto, String> {
    let row = db_update_transcript_meta(
        &state.db,
        &id,
        patch.starred,
        patch.manual_tags.as_deref(),
        patch.template.as_deref(),
        patch.language.as_deref(),
        patch.segments_json.as_deref(),
        patch.llm_tags.as_deref(),
    )
    .await
    .map_err(|e| e.to_string())?;
    Ok(TranscriptDto::from(row))
}
