//! Phase 7 implementation intentions: small if-then automation rules.
//!
//! The frontend owns scheduling and execution because the v1 triggers are
//! app-local events (timer/task/ritual) plus local clock checks. Rust owns
//! durable rule storage, validation, and main-window-only command access.

use serde::{Deserialize, Serialize};
use uuid::Uuid;

use lumotia_storage::{
    delete_implementation_rule as db_delete_rule, get_task_by_id,
    insert_implementation_rule as db_insert_rule, list_implementation_rules as db_list_rules,
    mark_implementation_rule_fired as db_mark_rule_fired,
    set_implementation_rule_enabled as db_set_rule_enabled, ImplementationRuleRow,
};

use crate::commands::security::ensure_main_window;
use crate::AppState;

#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RuleTriggerKind {
    TimeOfDay,
    TaskCompleted,
    MorningTriageFinished,
}

impl RuleTriggerKind {
    fn as_str(&self) -> &'static str {
        match self {
            RuleTriggerKind::TimeOfDay => "time_of_day",
            RuleTriggerKind::TaskCompleted => "task_completed",
            RuleTriggerKind::MorningTriageFinished => "morning_triage_finished",
        }
    }
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceTarget {
    Inbox,
    Today,
    Tasks,
    Task,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuleAction {
    Surface {
        target: SurfaceTarget,
        #[serde(default)]
        #[serde(rename = "taskId")]
        task_id: Option<String>,
        #[serde(default)]
        label: Option<String>,
    },
    StartTimer {
        seconds: u32,
        #[serde(default)]
        #[serde(rename = "taskId")]
        task_id: Option<String>,
        #[serde(default)]
        label: Option<String>,
    },
    SpeakLine {
        text: String,
    },
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateImplementationRuleRequest {
    pub trigger_kind: RuleTriggerKind,
    pub trigger_value: String,
    pub actions: Vec<RuleAction>,
    #[serde(default = "default_enabled")]
    pub enabled: bool,
    #[serde(default)]
    pub last_fired_key: Option<String>,
}

fn default_enabled() -> bool {
    true
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImplementationRuleDto {
    pub id: String,
    pub enabled: bool,
    pub trigger_kind: RuleTriggerKind,
    pub trigger_value: String,
    pub actions: Vec<RuleAction>,
    pub last_fired_key: Option<String>,
    pub created_at: String,
    pub updated_at: String,
}

impl TryFrom<ImplementationRuleRow> for ImplementationRuleDto {
    type Error = String;

    fn try_from(row: ImplementationRuleRow) -> Result<Self, Self::Error> {
        let trigger_kind = match row.trigger_kind.as_str() {
            "time_of_day" => RuleTriggerKind::TimeOfDay,
            "task_completed" => RuleTriggerKind::TaskCompleted,
            "morning_triage_finished" => RuleTriggerKind::MorningTriageFinished,
            other => return Err(format!("unknown rule trigger kind: {other}")),
        };
        let actions = serde_json::from_str::<Vec<RuleAction>>(&row.actions_json)
            .map_err(|e| format!("invalid rule actions JSON for {}: {e}", row.id))?;
        Ok(Self {
            id: row.id,
            enabled: row.enabled,
            trigger_kind,
            trigger_value: row.trigger_value,
            actions,
            last_fired_key: row.last_fired_key,
            created_at: row.created_at,
            updated_at: row.updated_at,
        })
    }
}

fn validate_hhmm(value: &str) -> Result<(), String> {
    let (h, m) = value
        .split_once(':')
        .ok_or_else(|| "time rules need HH:MM".to_string())?;
    if h.len() != 2 || m.len() != 2 {
        return Err("time rules need HH:MM".into());
    }
    let hour: u8 = h
        .parse()
        .map_err(|_| "time hour must be 00-23".to_string())?;
    let minute: u8 = m
        .parse()
        .map_err(|_| "time minute must be 00-59".to_string())?;
    if hour > 23 || minute > 59 {
        return Err("time must be between 00:00 and 23:59".into());
    }
    Ok(())
}

async fn validate_actions(
    state: &tauri::State<'_, AppState>,
    actions: &[RuleAction],
) -> Result<(), String> {
    if actions.is_empty() {
        return Err("add at least one rule action".into());
    }
    for action in actions {
        match action {
            RuleAction::Surface {
                target,
                task_id,
                label: _,
            } => {
                if matches!(target, SurfaceTarget::Task) {
                    let task_id = task_id
                        .as_deref()
                        .map(str::trim)
                        .filter(|id| !id.is_empty())
                        .ok_or_else(|| "surface-task actions need a task".to_string())?;
                    let exists = get_task_by_id(&state.db, task_id)
                        .await
                        .map_err(|e| e.to_string())?
                        .is_some();
                    if !exists {
                        return Err(format!("task {task_id} does not exist"));
                    }
                }
            }
            RuleAction::StartTimer { seconds, .. } => {
                if *seconds != 300 {
                    return Err("v1 rule timers are fixed at 5 minutes".into());
                }
            }
            RuleAction::SpeakLine { text } => {
                let trimmed = text.trim();
                if trimmed.is_empty() {
                    return Err("speak actions need a line to read".into());
                }
                if trimmed.chars().count() > 240 {
                    return Err("speak lines must be 240 characters or fewer".into());
                }
            }
        }
    }
    Ok(())
}

async fn validate_request(
    state: &tauri::State<'_, AppState>,
    request: &CreateImplementationRuleRequest,
) -> Result<(), String> {
    match request.trigger_kind {
        RuleTriggerKind::TimeOfDay => validate_hhmm(request.trigger_value.trim())?,
        RuleTriggerKind::TaskCompleted | RuleTriggerKind::MorningTriageFinished => {
            if !request.trigger_value.trim().is_empty() {
                return Err("this trigger does not take an extra value".into());
            }
        }
    }
    validate_actions(state, &request.actions).await
}

#[tauri::command]
pub async fn list_implementation_rules(
    window: tauri::WebviewWindow,
    state: tauri::State<'_, AppState>,
) -> Result<Vec<ImplementationRuleDto>, String> {
    ensure_main_window(&window)?;
    db_list_rules(&state.db)
        .await
        .map_err(|e| e.to_string())?
        .into_iter()
        .map(ImplementationRuleDto::try_from)
        .collect()
}

#[tauri::command]
pub async fn create_implementation_rule(
    window: tauri::WebviewWindow,
    state: tauri::State<'_, AppState>,
    request: CreateImplementationRuleRequest,
) -> Result<ImplementationRuleDto, String> {
    ensure_main_window(&window)?;
    validate_request(&state, &request).await?;

    let id = Uuid::new_v4().to_string();
    let actions_json = serde_json::to_string(&request.actions)
        .map_err(|e| format!("serialise rule actions failed: {e}"))?;
    let trigger_value = match request.trigger_kind {
        RuleTriggerKind::TimeOfDay => request.trigger_value.trim().to_string(),
        RuleTriggerKind::TaskCompleted | RuleTriggerKind::MorningTriageFinished => String::new(),
    };

    db_insert_rule(
        &state.db,
        &id,
        request.enabled,
        request.trigger_kind.as_str(),
        &trigger_value,
        &actions_json,
        request.last_fired_key.as_deref(),
    )
    .await
    .map_err(|e| e.to_string())
    .and_then(ImplementationRuleDto::try_from)
}

#[tauri::command]
pub async fn set_implementation_rule_enabled(
    window: tauri::WebviewWindow,
    state: tauri::State<'_, AppState>,
    id: String,
    enabled: bool,
) -> Result<ImplementationRuleDto, String> {
    ensure_main_window(&window)?;
    db_set_rule_enabled(&state.db, &id, enabled)
        .await
        .map_err(|e| e.to_string())
        .and_then(ImplementationRuleDto::try_from)
}

#[tauri::command]
pub async fn mark_implementation_rule_fired(
    window: tauri::WebviewWindow,
    state: tauri::State<'_, AppState>,
    id: String,
    fired_key: String,
) -> Result<ImplementationRuleDto, String> {
    ensure_main_window(&window)?;
    let key = fired_key.trim();
    if key.is_empty() {
        return Err("fired_key is required".into());
    }
    db_mark_rule_fired(&state.db, &id, key)
        .await
        .map_err(|e| e.to_string())
        .and_then(ImplementationRuleDto::try_from)
}

#[tauri::command]
pub async fn delete_implementation_rule(
    window: tauri::WebviewWindow,
    state: tauri::State<'_, AppState>,
    id: String,
) -> Result<(), String> {
    ensure_main_window(&window)?;
    db_delete_rule(&state.db, &id)
        .await
        .map_err(|e| e.to_string())
}

#[cfg(test)]
mod tests {
    use super::validate_hhmm;

    #[test]
    fn hhmm_validation_accepts_and_rejects_expected_shapes() {
        assert!(validate_hhmm("09:00").is_ok());
        assert!(validate_hhmm("23:59").is_ok());
        assert!(validate_hhmm("9:00").is_err());
        assert!(validate_hhmm("24:00").is_err());
        assert!(validate_hhmm("08:60").is_err());
    }
}
