use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;

use anyhow::Result;
use chrono::SecondsFormat;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use thiserror::Error;
use uuid::Uuid;

use crate::BeamPaths;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ParsedScheduleKind {
    Once,
    Interval,
    Cron,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ParsedSchedule {
    pub kind: ParsedScheduleKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub run_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub minutes: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub expr: Option<String>,
    pub display: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleRepeat {
    pub times: Option<u64>,
    #[serde(default)]
    pub completed: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ScheduleChatType {
    Group,
    P2p,
    TopicGroup,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum ScheduleDeliver {
    Origin,
    Local,
}

impl Default for ScheduleDeliver {
    fn default() -> Self {
        Self::Origin
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledTask {
    pub id: String,
    pub name: String,
    pub schedule: String,
    pub parsed: ParsedSchedule,
    pub prompt: String,
    pub working_dir: String,
    pub chat_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_message_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chat_type: Option<ScheduleChatType>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lark_app_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_chat_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_root_message_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_lark_app_id: Option<String>,
    #[serde(default = "default_true")]
    pub enabled: bool,
    pub created_at: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_run_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_run_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_status: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_delivery_error: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repeat: Option<ScheduleRepeat>,
    #[serde(default)]
    pub deliver: ScheduleDeliver,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskInput {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
    pub name: String,
    pub schedule: String,
    pub parsed: ParsedSchedule,
    pub prompt: String,
    pub working_dir: String,
    pub chat_id: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_message_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub scope: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chat_type: Option<ScheduleChatType>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub lark_app_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_chat_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_root_message_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub creator_lark_app_id: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_run_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repeat: Option<ScheduleRepeat>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub deliver: Option<ScheduleDeliver>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ScheduleTaskUpdate {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub enabled: Option<bool>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_run_at: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub next_run_at: Option<Option<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_status: Option<Option<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_error: Option<Option<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub last_delivery_error: Option<Option<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub repeat: Option<Option<ScheduleRepeat>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub root_message_id: Option<Option<String>>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub chat_type: Option<Option<ScheduleChatType>>,
}

#[derive(Debug, Error)]
pub enum ScheduleStoreError {
    #[error(
        "IdempotencyConflict: schedule task {task_id} exists with different canonical input (existing={existing_input_hash}…, incoming={incoming_input_hash}…)"
    )]
    IdempotencyConflict {
        task_id: String,
        existing_input_hash: String,
        incoming_input_hash: String,
    },
    #[error(transparent)]
    Io(#[from] std::io::Error),
    #[error(transparent)]
    Serde(#[from] serde_json::Error),
}

pub fn create_task(
    paths: &BeamPaths,
    input: CreateTaskInput,
) -> Result<ScheduledTask, ScheduleStoreError> {
    let mut tasks = load_tasks(paths)?;
    if let Some(id) = &input.id
        && let Some(existing) = tasks.get(id)
    {
        let existing_hash = compute_input_hash(&canonical_schedule_input(existing))?;
        let incoming_hash = compute_input_hash(&canonical_schedule_input_input(&input))?;
        if existing_hash == incoming_hash {
            return Ok(existing.clone());
        }
        return Err(ScheduleStoreError::IdempotencyConflict {
            task_id: id.clone(),
            existing_input_hash: existing_hash,
            incoming_input_hash: incoming_hash,
        });
    }

    let id = input.id.unwrap_or_else(|| {
        Uuid::new_v4()
            .simple()
            .to_string()
            .chars()
            .take(8)
            .collect()
    });
    let task = ScheduledTask {
        id: id.clone(),
        name: input.name,
        schedule: input.schedule,
        parsed: input.parsed,
        prompt: input.prompt,
        working_dir: input.working_dir,
        chat_id: input.chat_id,
        root_message_id: input.root_message_id,
        scope: input.scope,
        chat_type: input.chat_type,
        lark_app_id: input.lark_app_id,
        creator_chat_id: input.creator_chat_id,
        creator_root_message_id: input.creator_root_message_id,
        creator_lark_app_id: input.creator_lark_app_id,
        enabled: true,
        created_at: UtcNow::now(),
        last_run_at: None,
        next_run_at: input.next_run_at,
        last_status: None,
        last_error: None,
        last_delivery_error: None,
        repeat: input.repeat,
        deliver: input.deliver.unwrap_or_default(),
    };
    tasks.insert(id, task.clone());
    save_tasks(paths, &tasks)?;
    Ok(task)
}

pub fn get_task(paths: &BeamPaths, id: &str) -> Result<Option<ScheduledTask>, ScheduleStoreError> {
    let tasks = load_tasks(paths)?;
    Ok(tasks.get(id).cloned())
}

pub fn remove_task(paths: &BeamPaths, id: &str) -> Result<bool, ScheduleStoreError> {
    let mut tasks = load_tasks(paths)?;
    let existed = tasks.remove(id).is_some();
    if existed {
        save_tasks(paths, &tasks)?;
    }
    Ok(existed)
}

pub fn update_task(
    paths: &BeamPaths,
    id: &str,
    updates: ScheduleTaskUpdate,
) -> Result<(), ScheduleStoreError> {
    let mut tasks = load_tasks(paths)?;
    if let Some(task) = tasks.get_mut(id) {
        if let Some(enabled) = updates.enabled {
            task.enabled = enabled;
        }
        if let Some(last_run_at) = updates.last_run_at {
            task.last_run_at = Some(last_run_at);
        }
        if let Some(next_run_at) = updates.next_run_at {
            task.next_run_at = next_run_at;
        }
        if let Some(last_status) = updates.last_status {
            task.last_status = last_status;
        }
        if let Some(last_error) = updates.last_error {
            task.last_error = last_error;
        }
        if let Some(last_delivery_error) = updates.last_delivery_error {
            task.last_delivery_error = last_delivery_error;
        }
        if let Some(repeat) = updates.repeat {
            task.repeat = repeat;
        }
        if let Some(root_message_id) = updates.root_message_id {
            task.root_message_id = root_message_id;
        }
        if let Some(chat_type) = updates.chat_type {
            task.chat_type = chat_type;
        }
        save_tasks(paths, &tasks)?;
    }
    Ok(())
}

pub fn mark_run(
    paths: &BeamPaths,
    id: &str,
    success: bool,
    error: Option<&str>,
    delivery_error: Option<&str>,
) -> Result<(), ScheduleStoreError> {
    let mut tasks = load_tasks(paths)?;
    let Some(task) = tasks.get_mut(id) else {
        return Ok(());
    };

    task.last_run_at = Some(UtcNow::now());
    task.last_status = Some(if success {
        "ok".to_string()
    } else {
        "error".to_string()
    });
    task.last_error = if success {
        None
    } else {
        error.map(|s| s.to_string())
    };
    task.last_delivery_error = delivery_error.map(|s| s.to_string());

    if let Some(repeat) = task.repeat.as_mut() {
        repeat.completed = repeat.completed.saturating_add(1);
        if matches!(repeat.times, Some(times) if times > 0 && repeat.completed >= times) {
            tasks.remove(id);
            save_tasks(paths, &tasks)?;
            return Ok(());
        }
    }

    // Compute next_run_at based on schedule kind.
    let now = chrono::Utc::now();
    match task.parsed.kind {
        ParsedScheduleKind::Once => {
            task.enabled = false;
            task.next_run_at = None;
        }
        ParsedScheduleKind::Interval => {
            if let Some(minutes) = task.parsed.minutes {
                let next = now + chrono::Duration::minutes(minutes as i64);
                task.next_run_at = Some(next.to_rfc3339_opts(SecondsFormat::Millis, true));
            }
        }
        ParsedScheduleKind::Cron => {
            // Cron next-run computation requires a cron library.
            // For now, mark as unsupported to prevent repeated triggering.
            task.enabled = false;
            task.next_run_at = None;
            task.last_error = Some(
                "cron schedule auto-advance not supported; re-create the schedule to re-enable"
                    .to_string(),
            );
        }
    }

    save_tasks(paths, &tasks)?;
    Ok(())
}

pub fn list_tasks(paths: &BeamPaths) -> Result<Vec<ScheduledTask>, ScheduleStoreError> {
    let tasks = load_tasks(paths)?;
    Ok(tasks.values().cloned().collect())
}

pub fn append_output_log(
    paths: &BeamPaths,
    task_id: &str,
    content: &str,
) -> Result<PathBuf, ScheduleStoreError> {
    let dir = task_output_dir(paths, task_id);
    fs::create_dir_all(&dir)?;
    let fname = format!("{}.md", UtcNow::now().replace(':', "-").replace('.', "-"));
    let path = dir.join(fname);
    fs::write(&path, content)?;
    Ok(path)
}

fn load_tasks(paths: &BeamPaths) -> Result<BTreeMap<String, ScheduledTask>, ScheduleStoreError> {
    let path = paths.schedules_json();
    if !path.exists() {
        return Ok(BTreeMap::new());
    }
    let raw = fs::read_to_string(&path)?;
    if raw.trim().is_empty() {
        return Ok(BTreeMap::new());
    }
    let tasks = serde_json::from_str(&raw)?;
    Ok(tasks)
}

fn save_tasks(
    paths: &BeamPaths,
    tasks: &BTreeMap<String, ScheduledTask>,
) -> Result<(), ScheduleStoreError> {
    let path = paths.schedules_json();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    let tmp = path.with_extension("json.tmp");
    fs::write(&tmp, serde_json::to_vec_pretty(tasks)?)?;
    fs::rename(&tmp, &path)?;
    Ok(())
}

fn canonical_schedule_input(task: &ScheduledTask) -> serde_json::Value {
    serde_json::json!({
        "name": task.name,
        "schedule": task.schedule,
        "parsed": {
            "kind": task.parsed.kind,
            "runAt": task.parsed.run_at,
            "minutes": task.parsed.minutes,
            "expr": task.parsed.expr,
        },
        "prompt": task.prompt,
        "workingDir": task.working_dir,
        "chatId": task.chat_id,
        "rootMessageId": task.root_message_id,
        "scope": task.scope,
        "larkAppId": task.lark_app_id,
        "repeat": task.repeat.as_ref().map(|repeat| serde_json::json!({ "times": repeat.times })),
        "deliver": match task.deliver {
            ScheduleDeliver::Origin => "origin",
            ScheduleDeliver::Local => "local",
        }
    })
}

fn canonical_schedule_input_input(input: &CreateTaskInput) -> serde_json::Value {
    serde_json::json!({
        "name": input.name,
        "schedule": input.schedule,
        "parsed": {
            "kind": input.parsed.kind,
            "runAt": input.parsed.run_at,
            "minutes": input.parsed.minutes,
            "expr": input.parsed.expr,
        },
        "prompt": input.prompt,
        "workingDir": input.working_dir,
        "chatId": input.chat_id,
        "rootMessageId": input.root_message_id,
        "scope": input.scope,
        "larkAppId": input.lark_app_id,
        "repeat": input.repeat.as_ref().map(|repeat| serde_json::json!({ "times": repeat.times })),
        "deliver": match input.deliver.clone().unwrap_or_default() {
            ScheduleDeliver::Origin => "origin",
            ScheduleDeliver::Local => "local",
        }
    })
}

fn compute_input_hash(value: &serde_json::Value) -> Result<String, ScheduleStoreError> {
    let canonical = canonical_json(value);
    let mut hasher = Sha256::new();
    hasher.update(canonical.as_bytes());
    Ok(format!("sha256:{:x}", hasher.finalize()))
}

fn canonical_json(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::Null => "null".to_string(),
        serde_json::Value::Bool(v) => if *v { "true" } else { "false" }.to_string(),
        serde_json::Value::Number(v) => v.to_string(),
        serde_json::Value::String(v) => serde_json::to_string(v).expect("string serializable"),
        serde_json::Value::Array(items) => {
            let mut out = String::from("[");
            let mut first = true;
            for item in items {
                if !first {
                    out.push(',');
                }
                first = false;
                out.push_str(&canonical_json(item));
            }
            out.push(']');
            out
        }
        serde_json::Value::Object(map) => {
            let mut keys: Vec<_> = map.keys().collect();
            keys.sort();
            let mut out = String::from("{");
            let mut first = true;
            for key in keys {
                if !first {
                    out.push(',');
                }
                first = false;
                out.push_str(&serde_json::to_string(key).expect("key serializable"));
                out.push(':');
                out.push_str(&canonical_json(&map[key]));
            }
            out.push('}');
            out
        }
    }
}

fn task_output_dir(paths: &BeamPaths, task_id: &str) -> PathBuf {
    paths.schedules_output_dir().join(task_id)
}

struct UtcNow;

impl UtcNow {
    fn now() -> String {
        chrono::Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
    }
}

fn default_true() -> bool {
    true
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_paths(label: &str) -> BeamPaths {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos();
        BeamPaths::from_root(std::env::temp_dir().join(format!(
            "beam-schedule-store-{label}-{nanos}-{}",
            std::process::id()
        )))
    }

    fn parsed_cron() -> ParsedSchedule {
        ParsedSchedule {
            kind: ParsedScheduleKind::Cron,
            run_at: None,
            minutes: None,
            expr: Some("0 9 * * *".to_string()),
            display: "0 9 * * *".to_string(),
        }
    }

    #[test]
    fn create_task_returns_existing_when_canonical_input_matches() {
        let paths = temp_paths("identical");
        let input = CreateTaskInput {
            id: Some("wf_task".to_string()),
            name: "schedule-demo daily 9am".to_string(),
            schedule: "0 9 * * *".to_string(),
            parsed: parsed_cron(),
            prompt: "Schedule demo: run workflow self-check.".to_string(),
            working_dir: "/tmp/beam-schedule-demo".to_string(),
            chat_id: "oc_workflow_demo".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: None,
            repeat: None,
            deliver: None,
        };
        let created = create_task(&paths, input.clone()).expect("create");
        let returned = create_task(&paths, input).expect("create existing");
        assert_eq!(created.id, "wf_task");
        assert_eq!(returned.id, "wf_task");
        assert_eq!(list_tasks(&paths).expect("list").len(), 1);
        let _ = std::fs::remove_dir_all(paths.root());
    }

    #[test]
    fn create_task_conflicts_when_canonical_input_differs() {
        let paths = temp_paths("conflict");
        let input = CreateTaskInput {
            id: Some("wf_task".to_string()),
            name: "schedule-demo daily 9am".to_string(),
            schedule: "0 9 * * *".to_string(),
            parsed: parsed_cron(),
            prompt: "Schedule demo: run workflow self-check.".to_string(),
            working_dir: "/tmp/beam-schedule-demo".to_string(),
            chat_id: "oc_workflow_demo".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: None,
            repeat: None,
            deliver: None,
        };
        let _ = create_task(&paths, input).expect("create");
        let changed = CreateTaskInput {
            id: Some("wf_task".to_string()),
            name: "schedule-demo daily 9am".to_string(),
            schedule: "0 9 * * *".to_string(),
            parsed: parsed_cron(),
            prompt: "changed prompt".to_string(),
            working_dir: "/tmp/beam-schedule-demo".to_string(),
            chat_id: "oc_workflow_demo".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: None,
            repeat: None,
            deliver: None,
        };
        let err = create_task(&paths, changed).expect_err("conflict");
        assert!(matches!(
            err,
            ScheduleStoreError::IdempotencyConflict { .. }
        ));
        let _ = std::fs::remove_dir_all(paths.root());
    }

    #[test]
    fn mark_run_removes_finite_repeat_after_completion() {
        let paths = temp_paths("mark");
        let input = CreateTaskInput {
            id: Some("wf_task".to_string()),
            name: "schedule-demo daily 9am".to_string(),
            schedule: "0 9 * * *".to_string(),
            parsed: parsed_cron(),
            prompt: "Schedule demo: run workflow self-check.".to_string(),
            working_dir: "/tmp/beam-schedule-demo".to_string(),
            chat_id: "oc_workflow_demo".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: None,
            repeat: Some(ScheduleRepeat {
                times: Some(1),
                completed: 0,
            }),
            deliver: None,
        };
        let _ = create_task(&paths, input).expect("create");
        mark_run(&paths, "wf_task", true, None, None).expect("mark run");
        assert!(get_task(&paths, "wf_task").expect("get").is_none());
        let _ = std::fs::remove_dir_all(paths.root());
    }

    #[test]
    fn mark_run_interval_advances_next_run_at() {
        let paths = temp_paths("interval-advance");
        let input = CreateTaskInput {
            id: Some("int_task".to_string()),
            name: "every 10 min".to_string(),
            schedule: "every 10 min".to_string(),
            parsed: ParsedSchedule {
                kind: ParsedScheduleKind::Interval,
                run_at: None,
                minutes: Some(10),
                expr: None,
                display: "every 10 min".to_string(),
            },
            prompt: "interval test".to_string(),
            working_dir: "/tmp".to_string(),
            chat_id: "oc_test".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: Some("2026-01-01T00:00:00.000Z".to_string()),
            repeat: None,
            deliver: None,
        };
        let _ = create_task(&paths, input).expect("create");
        mark_run(&paths, "int_task", true, None, None).expect("mark");
        let task = get_task(&paths, "int_task")
            .expect("get")
            .expect("task exists");
        assert!(task.enabled, "interval should stay enabled");
        assert!(task.next_run_at.is_some(), "should have next_run_at");
        // next_run_at should be in the future (> now)
        let next = task.next_run_at.unwrap();
        let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
        assert!(next > now, "next_run_at={} should be > now={}", next, now);
        let _ = std::fs::remove_dir_all(paths.root());
    }

    #[test]
    fn mark_run_once_disables() {
        let paths = temp_paths("once-disable");
        let input = CreateTaskInput {
            id: Some("once_task".to_string()),
            name: "run once".to_string(),
            schedule: "once".to_string(),
            parsed: ParsedSchedule {
                kind: ParsedScheduleKind::Once,
                run_at: None,
                minutes: None,
                expr: None,
                display: "run once".to_string(),
            },
            prompt: "once test".to_string(),
            working_dir: "/tmp".to_string(),
            chat_id: "oc_test".to_string(),
            root_message_id: None,
            scope: Some("thread".to_string()),
            chat_type: None,
            lark_app_id: None,
            creator_chat_id: None,
            creator_root_message_id: None,
            creator_lark_app_id: None,
            next_run_at: Some("2026-01-01T00:00:00.000Z".to_string()),
            repeat: None,
            deliver: None,
        };
        let _ = create_task(&paths, input).expect("create");
        mark_run(&paths, "once_task", true, None, None).expect("mark");
        let task = get_task(&paths, "once_task")
            .expect("get")
            .expect("task exists");
        assert!(!task.enabled, "once should be disabled after run");
        assert!(task.next_run_at.is_none(), "once should clear next_run_at");
        let _ = std::fs::remove_dir_all(paths.root());
    }
}
