use askama::Template;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{Html, IntoResponse, Redirect, Response};
use axum::Json;

use crate::db;

use super::views::ErrorTemplate;

/// Columns the task table can be sorted by.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum SortField {
    Id,
    Status,
    Priority,
    Effort,
    Title,
    Created,
}

impl SortField {
    pub(super) fn parse(s: &str) -> Option<Self> {
        match s {
            "id" => Some(Self::Id),
            "status" => Some(Self::Status),
            "priority" => Some(Self::Priority),
            "effort" => Some(Self::Effort),
            "title" => Some(Self::Title),
            "created" => Some(Self::Created),
            _ => None,
        }
    }

    pub(super) fn as_str(self) -> &'static str {
        match self {
            Self::Id => "id",
            Self::Status => "status",
            Self::Priority => "priority",
            Self::Effort => "effort",
            Self::Title => "title",
            Self::Created => "created",
        }
    }

    /// Sensible default direction when the user first clicks a column.
    pub(super) fn default_order(self) -> SortOrder {
        match self {
            // Newest first, alphabetical ascending for text fields.
            Self::Created => SortOrder::Desc,
            Self::Title | Self::Id => SortOrder::Asc,
            // Highest priority/effort first; open before closed.
            Self::Priority | Self::Effort | Self::Status => SortOrder::Asc,
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(super) enum SortOrder {
    Asc,
    Desc,
}

impl SortOrder {
    pub(super) fn parse(s: &str) -> Option<Self> {
        match s {
            "asc" => Some(Self::Asc),
            "desc" => Some(Self::Desc),
            _ => None,
        }
    }

    pub(super) fn as_str(self) -> &'static str {
        match self {
            Self::Asc => "asc",
            Self::Desc => "desc",
        }
    }
}

/// Map a `Status` to a numeric value for semantic sorting.
/// Lower values sort first in ascending order: open → in_progress → closed.
fn status_sort_key(s: db::Status) -> i32 {
    match s {
        db::Status::Open => 1,
        db::Status::InProgress => 2,
        db::Status::Closed => 3,
    }
}

/// Apply the chosen sort field and direction to a filtered task list.
pub(super) fn sort_tasks(tasks: &mut [&db::Task], field: SortField, order: SortOrder) {
    tasks.sort_by(|a, b| {
        let cmp = match field {
            SortField::Id => a.id.as_str().cmp(b.id.as_str()),
            SortField::Status => status_sort_key(a.status).cmp(&status_sort_key(b.status)),
            SortField::Priority => a.priority.score().cmp(&b.priority.score()),
            SortField::Effort => a.effort.score().cmp(&b.effort.score()),
            SortField::Title => a
                .title
                .to_ascii_lowercase()
                .cmp(&b.title.to_ascii_lowercase()),
            SortField::Created => a.created_at.cmp(&b.created_at),
        };
        match order {
            SortOrder::Asc => cmp,
            SortOrder::Desc => cmp.reverse(),
        }
    });
}

/// Return a human-friendly status label (e.g. "In progress" instead of
/// "in_progress").
pub(super) fn friendly_status(raw: &str) -> &'static str {
    match raw {
        "open" => "Open",
        "in_progress" => "In progress",
        "closed" => "Closed",
        _ => "Open",
    }
}

/// Format an ISO 8601 timestamp into a human-friendly form (e.g. "15 Mar 2026")
/// for the noscript fallback. Returns the original string unchanged on parse
/// failure so the page still renders something sensible.
pub(super) fn friendly_date(iso: &str) -> String {
    chrono::NaiveDateTime::parse_from_str(iso, "%Y-%m-%dT%H:%M:%SZ")
        .map(|dt| dt.format("%-d %b %Y %H:%M").to_string())
        .unwrap_or_else(|_| iso.to_string())
}

/// Render a markdown string to sanitised HTML.
///
/// Uses pulldown-cmark for parsing and ammonia for sanitisation so that
/// untrusted user input can be displayed safely. Images are stripped
/// entirely — only structural/inline markup is allowed through.
pub(super) fn render_markdown(src: &str) -> String {
    use pulldown_cmark::{html::push_html, Parser};

    let parser = Parser::new(src);
    let mut raw_html = String::new();
    push_html(&mut raw_html, parser);

    ammonia::Builder::default()
        .rm_tags(&["img"])
        .clean(&raw_html)
        .to_string()
}

pub(super) fn render(tmpl: impl Template) -> Response {
    match tmpl.render() {
        Ok(html) => Html(html).into_response(),
        Err(e) => error_response(500, &format!("template render failed: {e}"), &[]),
    }
}

pub(super) fn error_response(code: u16, msg: &str, all_projects: &[String]) -> Response {
    let body = ErrorTemplate {
        all_projects: all_projects.to_vec(),
        active_project: None,
        status_code: code,
        message: msg.to_string(),
    };
    let html = body
        .render()
        .unwrap_or_else(|_| format!("<h1>{code}</h1><p>{msg}</p>"));
    let status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
    (status, Html(html)).into_response()
}

pub(super) fn list_projects_safe(root: &std::path::Path) -> Vec<String> {
    db::list_projects_in(root).unwrap_or_default()
}

/// Returns true when the client prefers a JSON response.
fn wants_json(headers: &HeaderMap) -> bool {
    headers
        .get("accept")
        .and_then(|v| v.to_str().ok())
        .map(|v| v.contains("application/json"))
        .unwrap_or(false)
}

/// Build a redirect-or-JSON response after a successful mutation.
pub(super) fn mutation_response(
    headers: &HeaderMap,
    redirect_to: &str,
    json_body: serde_json::Value,
) -> Response {
    if wants_json(headers) {
        Json(json_body).into_response()
    } else {
        Redirect::to(redirect_to).into_response()
    }
}

/// Build an error response appropriate for the caller (JSON or HTML).
pub(super) fn mutation_error(headers: &HeaderMap, code: u16, err: &anyhow::Error) -> Response {
    let status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
    if wants_json(headers) {
        (status, Json(serde_json::json!({"error": err.to_string()}))).into_response()
    } else {
        error_response(code, &err.to_string(), &[])
    }
}
