//! Shared mutation helpers for task operations.
//!
//! These functions encapsulate the Loro document mutations so that both
//! CLI commands and web handlers can perform the same operations without
//! duplicating store logic.

use std::collections::{HashMap, HashSet, VecDeque};
use std::path::Path;

use anyhow::{anyhow, bail, Result};
use loro::LoroMap;

use crate::db::{self, LogEntry, Store, Task, TaskId};

/// Create a new project and optionally bind a directory path to it.
pub fn init_project(root: &Path, name: &str, bind_path: Option<&Path>) -> Result<()> {
    std::fs::create_dir_all(root.join(db::PROJECTS_DIR))?;
    Store::init(root, name)?;
    if let Some(path) = bind_path {
        db::use_project(path, name)?;
    }
    Ok(())
}

/// Input for creating a new task.
pub struct CreateOpts {
    pub title: String,
    pub description: String,
    pub task_type: String,
    pub priority: db::Priority,
    pub effort: db::Effort,
    pub parent: Option<TaskId>,
    pub labels: Vec<String>,
}

/// Create a task and return the hydrated result.
pub fn create_task(store: &Store, opts: CreateOpts) -> Result<Task> {
    let ts = db::now_utc();
    let id = db::gen_id();

    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let task = db::insert_task_map(&tasks, &id)?;

        task.insert("title", opts.title.as_str())?;
        task.insert("description", opts.description.as_str())?;
        task.insert("type", opts.task_type.as_str())?;
        task.insert("priority", db::priority_label(opts.priority))?;
        task.insert("status", db::status_label(db::Status::Open))?;
        task.insert("effort", db::effort_label(opts.effort))?;
        task.insert(
            "parent",
            opts.parent.as_ref().map(|p| p.as_str()).unwrap_or(""),
        )?;
        task.insert("created_at", ts.clone())?;
        task.insert("updated_at", ts.clone())?;
        task.insert("deleted_at", "")?;
        task.insert_container("labels", LoroMap::new())?;
        task.insert_container("blockers", LoroMap::new())?;
        task.insert_container("logs", LoroMap::new())?;

        if !opts.labels.is_empty() {
            let labels = db::get_or_create_child_map(&task, "labels")?;
            for lbl in &opts.labels {
                labels.insert(lbl, true)?;
            }
        }

        Ok(())
    })?;

    store
        .get_task(&id, false)?
        .ok_or_else(|| anyhow!("failed to reload created task"))
}

/// Input for updating an existing task.  All fields are optional; only
/// populated fields are written.
pub struct UpdateOpts {
    pub status: Option<db::Status>,
    pub priority: Option<db::Priority>,
    pub effort: Option<db::Effort>,
    pub title: Option<String>,
    pub description: Option<String>,
}

/// Update task fields and return the refreshed task.
pub fn update_task(store: &Store, task_id: &TaskId, opts: UpdateOpts) -> Result<Task> {
    let ts = db::now_utc();

    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;

        if let Some(s) = opts.status {
            task.insert("status", db::status_label(s))?;
        }
        if let Some(p) = opts.priority {
            task.insert("priority", db::priority_label(p))?;
        }
        if let Some(e) = opts.effort {
            task.insert("effort", db::effort_label(e))?;
        }
        if let Some(ref t) = opts.title {
            task.insert("title", t.as_str())?;
        }
        if let Some(ref d) = opts.description {
            task.insert("description", d.as_str())?;
        }
        task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;

    store
        .get_task(task_id, false)?
        .ok_or_else(|| anyhow!("task not found"))
}

/// Mark a single task as closed.
pub fn mark_done(store: &Store, task_id: &TaskId) -> Result<()> {
    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        if let Some(task) = db::get_task_map(&tasks, task_id)? {
            task.insert("status", db::status_label(db::Status::Closed))?;
            task.insert("updated_at", ts.clone())?;
        }
        Ok(())
    })?;
    Ok(())
}

/// Reopen a single closed task.
pub fn reopen_task(store: &Store, task_id: &TaskId) -> Result<()> {
    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        if let Some(task) = db::get_task_map(&tasks, task_id)? {
            task.insert("status", db::status_label(db::Status::Open))?;
            task.insert("updated_at", ts.clone())?;
        }
        Ok(())
    })?;
    Ok(())
}

/// Append a log entry to a task and return the entry.
pub fn add_log(store: &Store, task_id: &TaskId, message: &str) -> Result<LogEntry> {
    let log_id = db::gen_id();
    let ts = db::now_utc();

    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
        let logs = db::get_or_create_child_map(&task, "logs")?;
        let entry = logs.insert_container(log_id.as_str(), LoroMap::new())?;
        entry.insert("timestamp", ts.clone())?;
        entry.insert("message", message)?;
        task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;

    Ok(LogEntry {
        id: log_id,
        timestamp: ts,
        message: message.to_string(),
    })
}

/// Add a label to a task.
pub fn add_label(store: &Store, task_id: &TaskId, label: &str) -> Result<()> {
    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
        let labels = db::get_or_create_child_map(&task, "labels")?;
        labels.insert(label, true)?;
        task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;
    Ok(())
}

/// Remove a label from a task.
pub fn remove_label(store: &Store, task_id: &TaskId, label: &str) -> Result<()> {
    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let task = db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
        let labels = db::get_or_create_child_map(&task, "labels")?;
        labels.delete(label)?;
        task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;
    Ok(())
}

/// Add a blocker dependency (child is blocked by blocker).
///
/// Rejects self-references and cycles.
pub fn add_dep(store: &Store, child_id: &TaskId, blocker_id: &TaskId) -> Result<()> {
    if child_id == blocker_id {
        bail!("adding dependency would create a cycle");
    }
    if would_cycle(store, child_id, blocker_id)? {
        bail!("adding dependency would create a cycle");
    }

    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let child_task =
            db::get_task_map(&tasks, child_id)?.ok_or_else(|| anyhow!("task not found"))?;
        let blockers = db::get_or_create_child_map(&child_task, "blockers")?;
        blockers.insert(blocker_id.as_str(), true)?;
        child_task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;
    Ok(())
}

/// Remove a blocker dependency.
pub fn remove_dep(store: &Store, child_id: &TaskId, blocker_id: &TaskId) -> Result<()> {
    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");
        let child_task =
            db::get_task_map(&tasks, child_id)?.ok_or_else(|| anyhow!("task not found"))?;
        let blockers = db::get_or_create_child_map(&child_task, "blockers")?;
        blockers.delete(blocker_id.as_str())?;
        child_task.insert("updated_at", ts.clone())?;
        Ok(())
    })?;
    Ok(())
}

/// Result of a soft-delete operation.
pub struct DeleteResult {
    pub deleted_ids: Vec<TaskId>,
    pub unblocked_ids: Vec<TaskId>,
}

/// Soft-delete one or more tasks.
///
/// When `recursive` is true, subtrees rooted at the given IDs are also
/// deleted.  Blocker references from surviving tasks are cleaned up
/// automatically; pass `force` to suppress the warning about unblocked
/// tasks.
pub fn soft_delete(store: &Store, ids: &[TaskId], recursive: bool) -> Result<DeleteResult> {
    use std::collections::BTreeSet;

    let all = store.list_tasks_unfiltered()?;

    let mut to_delete = BTreeSet::new();
    for id in ids {
        if recursive {
            collect_subtree(&all, id, &mut to_delete);
        } else {
            if all
                .iter()
                .any(|t| t.parent.as_ref() == Some(id) && t.deleted_at.is_none())
            {
                bail!("task '{id}' has children; use --recursive to delete subtree");
            }
            to_delete.insert(id.clone());
        }
    }

    let deleted_ids: Vec<TaskId> = to_delete.into_iter().collect();
    let deleted_set: HashSet<String> = deleted_ids
        .iter()
        .map(|id| id.as_str().to_string())
        .collect();

    let unblocked_ids: Vec<TaskId> = all
        .iter()
        .filter(|t| !deleted_set.contains(t.id.as_str()))
        .filter(|t| t.blockers.iter().any(|b| deleted_set.contains(b.as_str())))
        .map(|t| t.id.clone())
        .collect();

    let ts = db::now_utc();
    store.apply_and_persist(|doc| {
        let tasks = doc.get_map("tasks");

        for task_id in &deleted_ids {
            let task =
                db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
            task.insert("deleted_at", ts.clone())?;
            task.insert("updated_at", ts.clone())?;
            task.insert("status", db::status_label(db::Status::Closed))?;
        }

        for task in store.list_tasks_unfiltered()? {
            if deleted_set.contains(task.id.as_str()) {
                continue;
            }
            if let Some(task_map) = db::get_task_map(&tasks, &task.id)? {
                let blockers = db::get_or_create_child_map(&task_map, "blockers")?;
                for deleted in &deleted_ids {
                    blockers.delete(deleted.as_str())?;
                }
            }
        }

        Ok(())
    })?;

    Ok(DeleteResult {
        deleted_ids,
        unblocked_ids,
    })
}

fn would_cycle(store: &Store, child: &TaskId, parent: &TaskId) -> Result<bool> {
    let tasks = store.list_tasks_unfiltered()?;
    let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
    for task in tasks {
        for blocker in task.blockers {
            graph
                .entry(task.id.as_str().to_string())
                .or_default()
                .insert(blocker.as_str().to_string());
        }
    }
    graph
        .entry(child.as_str().to_string())
        .or_default()
        .insert(parent.as_str().to_string());

    let mut seen = HashSet::new();
    let mut queue = VecDeque::from([parent.as_str().to_string()]);
    while let Some(node) = queue.pop_front() {
        if node == child.as_str() {
            return Ok(true);
        }
        if !seen.insert(node.clone()) {
            continue;
        }
        if let Some(nexts) = graph.get(&node) {
            queue.extend(nexts.iter().cloned());
        }
    }

    Ok(false)
}

fn collect_subtree(all: &[Task], root: &TaskId, out: &mut std::collections::BTreeSet<TaskId>) {
    if !out.insert(root.clone()) {
        return;
    }
    for task in all {
        if task.parent.as_ref() == Some(root) && task.deleted_at.is_none() {
            collect_subtree(all, &task.id, out);
        }
    }
}
