// Tauri commands wrapping lumotia_storage task CRUD.
// Pattern mirrors transcripts.rs — TaskDto is the camelCase frontend shape,
// storage functions are aliased with db_ prefix to avoid name collisions.

use serde::{Deserialize, Serialize};

use uuid::Uuid;

use lumotia_llm::prompts::FeedbackExample as LlmFeedbackExample;
use lumotia_storage::{
    complete_subtask_and_check_parent as db_complete_subtask, complete_task as db_complete_task,
    delete_task as db_delete_task, get_task_by_id as db_get_task,
    insert_subtask as db_insert_subtask, insert_task as db_insert_task,
    list_feedback_examples as db_list_feedback_examples,
    list_recent_completions as db_list_recent_completions, list_subtasks as db_list_subtasks,
    list_tasks as db_list_tasks, set_task_energy as db_set_task_energy,
    uncomplete_task as db_uncomplete_task, update_task as db_update_task, DailyCompletionCount,
    FeedbackRow, FeedbackTargetType, TaskRow,
};

use crate::commands::power::PowerAssertion;
use crate::AppState;

/// Frontend-facing task shape. Matches the in-memory object in page.svelte.js.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskDto {
    pub id: String,
    pub text: String,
    pub bucket: String,
    pub list_id: Option<String>,
    pub effort: Option<String>,
    pub notes: String,
    pub done: bool,
    pub done_at: Option<String>,
    pub created_at: String,
    pub source_transcript_id: Option<String>,
    pub parent_task_id: Option<String>,
    pub energy: Option<String>,
}

impl From<TaskRow> for TaskDto {
    fn from(r: TaskRow) -> Self {
        Self {
            id: r.id,
            text: r.text,
            bucket: r.bucket,
            list_id: r.list_id,
            effort: r.effort,
            notes: r.notes,
            done: r.done,
            done_at: r.done_at,
            created_at: r.created_at,
            source_transcript_id: r.source_transcript_id,
            parent_task_id: r.parent_task_id,
            energy: r.energy,
        }
    }
}

/// Accepted energy tag values. Kept as a const so frontend and storage
/// validate against the same list. Migration v11 enforces the same
/// set via a CHECK constraint.
const ENERGY_LEVELS: &[&str] = &["high", "medium", "brain_dead"];

fn validate_energy(raw: Option<&str>) -> Result<Option<&str>, String> {
    match raw {
        None => Ok(None),
        Some(s) if ENERGY_LEVELS.contains(&s) => Ok(Some(s)),
        Some(other) => Err(format!(
            "energy must be one of {:?} or null, got {:?}",
            ENERGY_LEVELS, other
        )),
    }
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskRequest {
    pub id: String,
    pub text: String,
    pub bucket: String,
    #[serde(default)]
    pub source_transcript_id: Option<String>,
    #[serde(default)]
    pub list_id: Option<String>,
    #[serde(default)]
    pub effort: Option<String>,
    #[serde(default)]
    pub energy: Option<String>,
}

#[tauri::command]
pub async fn create_task_cmd(
    state: tauri::State<'_, AppState>,
    request: CreateTaskRequest,
) -> Result<TaskDto, String> {
    let energy = validate_energy(request.energy.as_deref())?;
    db_insert_task(
        &state.db,
        &request.id,
        &request.text,
        &request.bucket,
        request.source_transcript_id.as_deref(),
        request.list_id.as_deref(),
        request.effort.as_deref(),
        energy,
    )
    .await
    .map_err(|e| e.to_string())?;

    // Fetch the freshly-inserted row so the frontend can stop doing
    // client-side object construction. Mirrors list_tasks_cmd's shape.
    db_get_task(&state.db, &request.id)
        .await
        .map_err(|e| e.to_string())?
        .map(TaskDto::from)
        .ok_or_else(|| format!("Task {} not found after insert", request.id))
}

/// Patch-shaped update. Any field omitted (or explicit `null`) leaves the
/// column untouched via `COALESCE` in the storage layer. Matches
/// `update_transcript`'s partial-update philosophy. `done` / `doneAt` are
/// intentionally absent — those flow through `complete_task_cmd` /
/// `uncomplete_task_cmd` to stamp the server-side timestamp.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTaskRequest {
    #[serde(default)]
    pub text: Option<String>,
    #[serde(default)]
    pub bucket: Option<String>,
    #[serde(default)]
    pub list_id: Option<String>,
    #[serde(default)]
    pub effort: Option<String>,
    #[serde(default)]
    pub notes: Option<String>,
}

#[tauri::command]
pub async fn update_task_cmd(
    state: tauri::State<'_, AppState>,
    id: String,
    patch: UpdateTaskRequest,
) -> Result<TaskDto, String> {
    let row = db_update_task(
        &state.db,
        &id,
        patch.text.as_deref(),
        patch.bucket.as_deref(),
        patch.list_id.as_deref(),
        patch.effort.as_deref(),
        patch.notes.as_deref(),
    )
    .await
    .map_err(|e| e.to_string())?;
    Ok(TaskDto::from(row))
}

#[tauri::command]
pub async fn list_tasks_cmd(state: tauri::State<'_, AppState>) -> Result<Vec<TaskDto>, String> {
    db_list_tasks(&state.db)
        .await
        .map(|rows| rows.into_iter().map(TaskDto::from).collect())
        .map_err(|e| e.to_string())
}

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

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

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

/// Phase 3: set or clear the `energy` tag on a task. Dedicated command
/// rather than a field on `update_task_cmd` because the existing update
/// path uses `COALESCE` semantics where `None` means "preserve" — which
/// makes clearing the tag impossible. This command always writes exactly
/// what you send, including `None` to explicitly clear.
#[tauri::command]
pub async fn set_task_energy_cmd(
    state: tauri::State<'_, AppState>,
    id: String,
    energy: Option<String>,
) -> Result<TaskDto, String> {
    let validated = validate_energy(energy.as_deref())?;
    let row = db_set_task_energy(&state.db, &id, validated)
        .await
        .map_err(|e| e.to_string())?;
    Ok(TaskDto::from(row))
}

/// Convert HITL feedback rows fetched from storage into the few-shot
/// exemplar shape the LLM crate consumes. We reconstruct the `input`
/// (parent task text, transcript chunk) from `context_json` where the
/// recorder has stored it. Rows without usable input are dropped —
/// the prompt builder filters them too, but doing it here keeps the
/// exemplar list tight and the prompt budget predictable.
///
/// Malformed `context_json` is logged rather than silently dropped so
/// data-integrity regressions surface instead of disappearing.
fn to_llm_examples(rows: Vec<FeedbackRow>) -> Vec<LlmFeedbackExample> {
    rows.into_iter()
        .filter_map(|r| {
            let raw = r.context_json.as_deref().unwrap_or("{}");
            let ctx: serde_json::Value = match serde_json::from_str(raw) {
                Ok(v) => v,
                Err(e) => {
                    tracing::warn!(
                        target: "lumotia_lib::feedback",
                        row_id = r.id,
                        error = %e,
                        "skipping feedback row with malformed context_json"
                    );
                    return None;
                }
            };
            let input = ctx
                .get("input")
                .and_then(|v| v.as_str())
                .map(str::to_string)
                .unwrap_or_default();
            if input.trim().is_empty() {
                return None;
            }
            Some(LlmFeedbackExample {
                input,
                original_output: r.original_text,
                corrected_output: r.corrected_text,
            })
        })
        .collect()
}

/// Rough character budget for the few-shot block. Qwen's tokenizer
/// averages ~3.5 chars per token in English, so 2000 chars is ~570
/// tokens — well inside the 64-token reserve + response-token gap
/// against the 8192-token context cap (see `LlmEngine::generate`).
///
/// Exceed this and we drop the oldest examples first. Rationale: the
/// retrieval already orders most-recent-first, and the most recent
/// correction is usually the one carrying the user's live preference.
const FEW_SHOT_CHAR_BUDGET: usize = 2000;

fn example_char_cost(ex: &LlmFeedbackExample) -> usize {
    // Matches the render path in `prompts::render_feedback_exemplar`:
    // "Input: {input}\nGood output: {good}". Prefix strings + newlines
    // + the two bodies. Slight overestimate to leave headroom.
    let good_len = ex
        .corrected_output
        .as_deref()
        .or(ex.original_output.as_deref())
        .map(str::len)
        .unwrap_or(0);
    ex.input.len() + good_len + 24
}

fn trim_to_budget(mut examples: Vec<LlmFeedbackExample>) -> Vec<LlmFeedbackExample> {
    let mut running = 0usize;
    let mut kept = Vec::with_capacity(examples.len());
    for ex in examples.drain(..) {
        let cost = example_char_cost(&ex);
        if running + cost > FEW_SHOT_CHAR_BUDGET {
            break;
        }
        running += cost;
        kept.push(ex);
    }
    kept
}

#[tauri::command]
pub async fn decompose_and_store(
    state: tauri::State<'_, AppState>,
    parent_task_id: String,
    profile_id: Option<String>,
) -> Result<Vec<TaskDto>, String> {
    let parent = db_get_task(&state.db, &parent_task_id)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| format!("Task {parent_task_id} not found"))?;

    // Pull recent micro-step feedback so the system prompt gets
    // conditioned on the user's preferred decomposition style. We
    // cap at 5 examples AND at a char budget to keep the prompt
    // under token budget regardless of how much feedback has been
    // captured, and scope by profile so per-profile styles do not
    // leak into each other.
    let examples = db_list_feedback_examples(
        &state.db,
        FeedbackTargetType::MicroStep,
        5,
        0,
        profile_id.as_deref(),
    )
    .await
    .map(to_llm_examples)
    .map(trim_to_budget)
    .unwrap_or_default();

    let engine = state.llm_engine.clone();
    let parent_text = parent.text.clone();
    let steps = tokio::task::spawn_blocking(move || {
        let _power_guard = PowerAssertion::begin("lumotia LLM micro-step decomposition");
        engine.decompose_task_with_feedback(&parent_text, &examples)
    })
    .await
    .map_err(|e| e.to_string())?
    .map_err(|e| e.to_string())?;

    let mut created = Vec::new();
    for text in steps {
        let id = Uuid::new_v4().to_string();
        db_insert_subtask(&state.db, &id, &text, &parent_task_id)
            .await
            .map_err(|e| e.to_string())?;
        if let Some(row) = db_get_task(&state.db, &id)
            .await
            .map_err(|e| e.to_string())?
        {
            created.push(TaskDto::from(row));
        }
    }

    Ok(created)
}

#[tauri::command]
pub async fn extract_tasks_from_transcript_cmd(
    state: tauri::State<'_, AppState>,
    transcript: String,
    profile_id: Option<String>,
) -> Result<Vec<String>, String> {
    let examples = db_list_feedback_examples(
        &state.db,
        FeedbackTargetType::TaskExtraction,
        5,
        0,
        profile_id.as_deref(),
    )
    .await
    .map(to_llm_examples)
    .map(trim_to_budget)
    .unwrap_or_default();

    let engine = state.llm_engine.clone();
    tokio::task::spawn_blocking(move || {
        let _power_guard = PowerAssertion::begin("lumotia LLM task extraction");
        engine.extract_tasks_with_feedback(&transcript, &examples)
    })
    .await
    .map_err(|e| e.to_string())?
    .map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn list_subtasks_cmd(
    state: tauri::State<'_, AppState>,
    parent_task_id: String,
) -> Result<Vec<TaskDto>, String> {
    db_list_subtasks(&state.db, &parent_task_id)
        .await
        .map(|rows| rows.into_iter().map(TaskDto::from).collect())
        .map_err(|e| e.to_string())
}

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

/// Phase 8: daily completion counts for the Tasks-page badge and the
/// 7-day momentum sparkline. Returns a fixed-length oldest-first
/// series. Empty days are explicit zeros.
#[tauri::command]
pub async fn list_recent_completions_cmd(
    state: tauri::State<'_, AppState>,
    days: u32,
) -> Result<Vec<DailyCompletionCount>, String> {
    db_list_recent_completions(&state.db, days)
        .await
        .map_err(|e| e.to_string())
}
