use anyhow::Result;
use axum::extract::{Path as AxumPath, State};
use axum::http::HeaderMap;
use axum::response::Response;
use axum::Form;

use crate::db::{self, Store, TaskId};
use crate::ops;

use super::helpers::{mutation_error, mutation_response};
use super::AppState;

#[derive(serde::Deserialize)]
pub(super) struct ProjectForm {
    name: String,
    #[serde(default)]
    bind_path: String,
}

#[derive(serde::Deserialize)]
pub(super) struct CreateForm {
    title: String,
    #[serde(default)]
    description: String,
    #[serde(default = "default_task_type")]
    task_type: String,
    #[serde(default = "default_priority")]
    priority: String,
    #[serde(default = "default_effort")]
    effort: String,
    #[serde(default)]
    labels: String,
    #[serde(default)]
    parent: String,
}

fn default_task_type() -> String {
    "task".to_string()
}

fn default_priority() -> String {
    "medium".to_string()
}

fn default_effort() -> String {
    "medium".to_string()
}

#[derive(serde::Deserialize)]
pub(super) struct UpdateForm {
    #[serde(default)]
    title: Option<String>,
    #[serde(default)]
    description: Option<String>,
    #[serde(default)]
    status: Option<String>,
    #[serde(default)]
    priority: Option<String>,
    #[serde(default)]
    effort: Option<String>,
    #[serde(default)]
    redirect: Option<String>,
}

#[derive(serde::Deserialize)]
pub(super) struct LogForm {
    message: String,
}

#[derive(serde::Deserialize)]
pub(super) struct LabelForm {
    /// "add" or "rm"
    action: String,
    label: String,
}

#[derive(serde::Deserialize)]
pub(super) struct DepForm {
    /// "add" or "rm"
    action: String,
    blocker: String,
}

pub(super) async fn create_project_handler(
    State(state): State<AppState>,
    headers: HeaderMap,
    Form(form): Form<ProjectForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<String> {
        let bind = if form.bind_path.is_empty() {
            None
        } else {
            Some(std::path::PathBuf::from(&form.bind_path))
        };
        ops::init_project(&root, &form.name, bind.as_deref())?;
        Ok(form.name)
    })
    .await;

    match result {
        Ok(Ok(name)) => {
            let redirect = format!("/projects/{name}");
            mutation_response(&headers, &redirect, serde_json::json!({"name": name}))
        }
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn create_handler(
    State(state): State<AppState>,
    AxumPath(name): AxumPath<String>,
    headers: HeaderMap,
    Form(form): Form<CreateForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(db::Task, String)> {
        let store = Store::open(&root, &name)?;

        let parent = if form.parent.is_empty() {
            None
        } else {
            Some(db::resolve_task_id(&store, &form.parent, false)?)
        };

        let labels: Vec<String> = form
            .labels
            .split(',')
            .map(str::trim)
            .filter(|l| !l.is_empty())
            .map(String::from)
            .collect();

        let task = ops::create_task(
            &store,
            ops::CreateOpts {
                title: form.title,
                description: form.description,
                task_type: form.task_type,
                priority: db::parse_priority(&form.priority)?,
                effort: db::parse_effort(&form.effort)?,
                parent,
                labels,
            },
        )?;

        let redirect = format!("/projects/{}/tasks/{}", name, task.id.as_str());
        Ok((task, redirect))
    })
    .await;

    match result {
        Ok(Ok((task, redirect))) => mutation_response(
            &headers,
            &redirect,
            serde_json::to_value(&task).unwrap_or_default(),
        ),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn update_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
    Form(form): Form<UpdateForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(db::Task, String)> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;

        let task = ops::update_task(
            &store,
            &task_id,
            ops::UpdateOpts {
                status: form
                    .status
                    .as_deref()
                    .filter(|s| !s.is_empty())
                    .map(db::parse_status)
                    .transpose()?,
                priority: form
                    .priority
                    .as_deref()
                    .filter(|s| !s.is_empty())
                    .map(db::parse_priority)
                    .transpose()?,
                effort: form
                    .effort
                    .as_deref()
                    .filter(|s| !s.is_empty())
                    .map(db::parse_effort)
                    .transpose()?,
                title: form.title.filter(|s| !s.is_empty()),
                description: form.description,
            },
        )?;

        let redirect = form
            .redirect
            .filter(|r| r.starts_with('/'))
            .unwrap_or_else(|| format!("/projects/{}/tasks/{}", name, task.id.as_str()));
        Ok((task, redirect))
    })
    .await;

    match result {
        Ok(Ok((task, redirect))) => mutation_response(
            &headers,
            &redirect,
            serde_json::to_value(&task).unwrap_or_default(),
        ),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn log_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
    Form(form): Form<LogForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(db::LogEntry, String)> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;
        let entry = ops::add_log(&store, &task_id, &form.message)?;
        let redirect = format!("/projects/{}/tasks/{}", name, task_id.as_str());
        Ok((entry, redirect))
    })
    .await;

    match result {
        Ok(Ok((entry, redirect))) => mutation_response(
            &headers,
            &redirect,
            serde_json::to_value(&entry).unwrap_or_default(),
        ),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn done_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(TaskId, String)> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;
        ops::mark_done(&store, &task_id)?;
        let redirect = format!("/projects/{}/tasks/{}", name, task_id.as_str());
        Ok((task_id, redirect))
    })
    .await;

    match result {
        Ok(Ok((task_id, redirect))) => mutation_response(
            &headers,
            &redirect,
            serde_json::json!({"id": task_id, "status": "closed"}),
        ),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn reopen_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(TaskId, String)> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;
        ops::reopen_task(&store, &task_id)?;
        let redirect = format!("/projects/{}/tasks/{}", name, task_id.as_str());
        Ok((task_id, redirect))
    })
    .await;

    match result {
        Ok(Ok((task_id, redirect))) => mutation_response(
            &headers,
            &redirect,
            serde_json::json!({"id": task_id, "status": "open"}),
        ),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn label_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
    Form(form): Form<LabelForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<String> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;
        match form.action.as_str() {
            "add" => ops::add_label(&store, &task_id, &form.label)?,
            "rm" => ops::remove_label(&store, &task_id, &form.label)?,
            other => anyhow::bail!("unknown label action '{other}'; expected 'add' or 'rm'"),
        }
        Ok(format!("/projects/{}/tasks/{}", name, task_id.as_str()))
    })
    .await;

    match result {
        Ok(Ok(redirect)) => mutation_response(&headers, &redirect, serde_json::json!({"ok": true})),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn dep_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
    Form(form): Form<DepForm>,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<String> {
        let store = Store::open(&root, &name)?;
        let child_id = db::resolve_task_id(&store, &id, false)?;
        let blocker_id = db::resolve_task_id(&store, &form.blocker, form.action == "rm")?;
        match form.action.as_str() {
            "add" => ops::add_dep(&store, &child_id, &blocker_id)?,
            "rm" => ops::remove_dep(&store, &child_id, &blocker_id)?,
            other => anyhow::bail!("unknown dep action '{other}'; expected 'add' or 'rm'"),
        }
        Ok(format!("/projects/{}/tasks/{}", name, child_id.as_str()))
    })
    .await;

    match result {
        Ok(Ok(redirect)) => mutation_response(&headers, &redirect, serde_json::json!({"ok": true})),
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}

pub(super) async fn delete_handler(
    State(state): State<AppState>,
    AxumPath((name, id)): AxumPath<(String, String)>,
    headers: HeaderMap,
) -> Response {
    let root = state.data_root.clone();
    let result = tokio::task::spawn_blocking(move || -> Result<(ops::DeleteResult, String)> {
        let store = Store::open(&root, &name)?;
        let task_id = db::resolve_task_id(&store, &id, false)?;
        let dr = ops::soft_delete(&store, &[task_id], false)?;
        let redirect = format!("/projects/{}", name);
        Ok((dr, redirect))
    })
    .await;

    match result {
        Ok(Ok((dr, redirect))) => {
            let json_body = serde_json::json!({
                "deleted_ids": dr.deleted_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
                "unblocked_ids": dr.unblocked_ids.iter().map(ToString::to_string).collect::<Vec<_>>(),
            });
            mutation_response(&headers, &redirect, json_body)
        }
        Ok(Err(e)) => mutation_error(&headers, 400, &e),
        Err(e) => mutation_error(&headers, 500, &e.into()),
    }
}
