Skip to content
use actix_identity::Identity;
use actix_web::{HttpRequest, HttpResponse, Responder, get, post, web};
use maud::{Markup, html};
use serde::Deserialize;
use uuid::Uuid;

use crate::auth::current_user;
use crate::db::Database;
use crate::db::tickets::TicketDetails;
use crate::models::{Tag, Ticket, User};
use crate::view::{page_or_partial, render_error};

#[derive(Deserialize, Default)]
pub struct BoardQuery {
    pub tag: Option<String>,
    pub view: Option<String>,
}

/// Which slice of the task list a visitor is looking at.
#[derive(Clone, Copy)]
enum TaskView {
    Open,
    Closed,
}

impl TaskView {
    fn from_query(view: Option<&str>) -> Self {
        match view {
            Some("closed") => Self::Closed,
            _ => Self::Open,
        }
    }

    fn is_closed(self) -> bool {
        matches!(self, Self::Closed)
    }

    fn label(self) -> &'static str {
        match self {
            Self::Open => "Open",
            Self::Closed => "Closed",
        }
    }
}

/// Builds a link that preserves the active view and optional tag filter.
fn view_href(view: TaskView, tag: Option<&str>) -> String {
    let mut params: Vec<String> = Vec::new();
    if let Some(tag) = tag {
        params.push(format!("tag={tag}"));
    }
    if view.is_closed() {
        params.push("view=closed".to_string());
    }
    if params.is_empty() {
        "/".to_string()
    } else {
        format!("/?{}", params.join("&"))
    }
}

fn form_values(fields: &std::collections::HashMap<String, Vec<String>>, key: &str) -> Vec<String> {
    fields.get(key).cloned().unwrap_or_default()
}

fn form_value(fields: &std::collections::HashMap<String, Vec<String>>, key: &str) -> String {
    fields
        .get(key)
        .and_then(|values| values.first())
        .cloned()
        .unwrap_or_default()
}

fn tag_pill(tag: &Tag) -> Markup {
    html! { span class="tag" { "#" (tag.name) } }
}

fn ticket_card(details: &TicketDetails, viewer_id: Option<&str>) -> Markup {
    let closed = details.closed_as.is_some();
    let owner = viewer_id == Some(details.ticket.creator_id.as_str());
    html! {
        article class=(if closed { "task is-closed" } else { "task" }) {
            div class="task-body" {
                h3 class="task-title" { (details.ticket.title) }
                @if !details.ticket.description.is_empty() {
                    p class="task-note" { (details.ticket.description) }
                }
                @if !details.tags.is_empty() {
                    div class="task-tags" {
                        @for tag in &details.tags { (tag_pill(tag)) }
                    }
                }
            }
            div class="task-side" {
                @if let Some(outcome) = details.closed_as.as_deref() {
                    span class=(if outcome == "done" { "task-outcome is-done" } else { "task-outcome" }) {
                        @if outcome == "done" { "Done" } @else { "Not planned" }
                    }
                }
                @if owner {
                    @if closed {
                        form method="POST" action=(format!("/tickets/{}/reopen", details.ticket.id)) {
                            button class="btn btn-ghost btn-sm" type="submit" { "Reopen" }
                        }
                    } @else {
                        form method="POST" action=(format!("/tickets/{}/close", details.ticket.id)) {
                            input type="hidden" name="outcome" value="done";
                            button class="btn btn-primary btn-sm" type="submit" { "Done" }
                        }
                        form method="POST" action=(format!("/tickets/{}/close", details.ticket.id)) {
                            input type="hidden" name="outcome" value="not_planned";
                            button class="btn btn-ghost btn-sm" type="submit" { "Not planned" }
                        }
                    }
                }
            }
        }
    }
}

fn render_guest_home(done_count: u64) -> Markup {
    html! {
        section class="guest-landing" {
            p class="eyebrow" { "Private task list" }
            h1 { "Keep track of what matters." }
            p class="guest-intro" { "A simple place for personal tasks. Your list stays private; only the number of tickets done is shared here." }
            div class="guest-count" {
                strong { (done_count) }
                span { "tickets done" }
            }
            div class="guest-actions" {
                a class="btn btn-primary" href="https://me.silenlocatelli.ch" { "Visit me.silenlocatelli.ch" }
                a class="text-link" href="/auth/login" { "Log in" }
            }
        }
    }
}

fn render_tasks_page(
    user: &User,
    done_count: u64,
    tickets: &[TicketDetails],
    selected_tag: Option<&str>,
    view: TaskView,
) -> Markup {
    let mut tags: Vec<&Tag> = Vec::new();
    for ticket in tickets {
        for tag in &ticket.tags {
            if !tags.iter().any(|found| found.id == tag.id) {
                tags.push(tag);
            }
        }
    }

    let mut open: Vec<&TicketDetails> = Vec::new();
    let mut closed: Vec<&TicketDetails> = Vec::new();
    for ticket in tickets {
        if selected_tag.is_some_and(|id| !ticket.tags.iter().any(|tag| tag.id == id)) {
            continue;
        }
        if ticket.closed_as.is_none() {
            open.push(ticket);
        } else {
            closed.push(ticket);
        }
    }
    let items: &[&TicketDetails] = if view.is_closed() { &closed } else { &open };

    html! {
        header class="page-head" {
            div class="page-title" {
                h1 { "Tasks" }
                p class="page-meta" {
                    span { (open.len()) " open" }
                    span class="dot" { "·" }
                    span { (done_count) " done" }
                }
            }
            a class="btn btn-primary" href="/tickets/new" { "New task" }
        }

        nav class="view-tabs" aria-label="Task views" {
            a
                class=(if view.is_closed() { "view-tab" } else { "view-tab is-active" })
                href=(view_href(TaskView::Open, selected_tag))
            {
                "Open" span class="view-count" { (open.len()) }
            }
            a
                class=(if view.is_closed() { "view-tab is-active" } else { "view-tab" })
                href=(view_href(TaskView::Closed, selected_tag))
            {
                "Closed" span class="view-count" { (closed.len()) }
            }
        }

        @if !tags.is_empty() {
            nav class="tag-filters" aria-label="Filter tasks by tag" {
                a
                    class=(if selected_tag.is_none() { "tag-filter is-active" } else { "tag-filter" })
                    href=(view_href(view, None))
                { "All tags" }
                @for tag in &tags {
                    a
                        class=(if selected_tag == Some(tag.id.as_str()) { "tag-filter is-active" } else { "tag-filter" })
                        href=(view_href(view, Some(tag.id.as_str())))
                    { "#" (tag.name) }
                }
            }
        }

        @if items.is_empty() {
            div class="empty-state" {
                p {
                    @if selected_tag.is_some() {
                        "No " (view.label().to_lowercase()) " tasks with this tag."
                    } @else if view.is_closed() {
                        "Nothing closed yet."
                    } @else {
                        "Nothing open. Add a task to get started."
                    }
                }
                @if !view.is_closed() && selected_tag.is_none() {
                    a class="text-link" href="/tickets/new" { "Add a task" }
                }
            }
        } @else {
            div class="task-list" {
                @for task in items { (ticket_card(task, Some(&user.id))) }
            }
        }
    }
}

fn render_new_task_page() -> Markup {
    html! {
        header class="page-head" {
            div class="page-title" {
                p class="eyebrow" { "New" }
                h1 { "Add a task" }
            }
            a class="text-link" href="/" { "Cancel" }
        }

        form method="POST" action="/tickets" class="stack-form" {
            div class="field" {
                label for="ticket-title" { "Task" }
                input
                    class="input"
                    id="ticket-title"
                    name="title"
                    maxlength="120"
                    required
                    autofocus
                    placeholder="What needs doing?";
            }
            div class="field" {
                label for="ticket-description" { "Details" }
                textarea
                    class="textarea"
                    id="ticket-description"
                    name="description"
                    rows="4"
                    placeholder="Optional context…"
                {}
            }
            div class="field" {
                label for="new-tags" { "Tags" }
                input
                    class="input"
                    id="new-tags"
                    name="new_tags"
                    maxlength="300"
                    placeholder="home, writing";
                p class="field-hint" { "Separate tags with commas. Everything here stays private to you." }
            }
            div class="form-actions" {
                a class="btn btn-outline" href="/" { "Cancel" }
                button class="btn btn-primary" type="submit" { "Add task" }
            }
        }
    }
}

#[get("/")]
pub async fn home(
    req: HttpRequest,
    identity: Option<Identity>,
    db: web::Data<Database>,
    query: web::Query<BoardQuery>,
) -> HttpResponse {
    let user = current_user(identity, &db).await;
    let content = if let Some(user) = user.as_ref() {
        let tickets = db
            .visible_tickets(Some(&user.id))
            .await
            .unwrap_or_else(|error| {
                log::error!("Failed to list private tasks: {error}");
                Vec::new()
            });
        let done_count = db.done_ticket_count(&user.id).await.unwrap_or_default();
        let view = TaskView::from_query(query.view.as_deref());
        render_tasks_page(user, done_count, &tickets, query.tag.as_deref(), view)
    } else {
        let done_count = db.all_done_ticket_count().await.unwrap_or_else(|error| {
            log::error!("Failed to count done tickets: {error}");
            0
        });
        render_guest_home(done_count)
    };
    let body = page_or_partial(
        &req,
        content,
        user.as_ref().map(|u| u.username.as_str()),
        "Tasks",
    )
    .into_string();
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(body)
}

#[get("/tickets")]
pub async fn index(identity: Option<Identity>, db: web::Data<Database>) -> HttpResponse {
    if current_user(identity, &db).await.is_none() {
        return HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish();
    }
    HttpResponse::SeeOther()
        .insert_header(("Location", "/"))
        .finish()
}

#[get("/tickets/new")]
pub async fn new_ticket_page(
    req: HttpRequest,
    identity: Option<Identity>,
    db: web::Data<Database>,
) -> HttpResponse {
    let Some(user) = current_user(identity, &db).await else {
        return HttpResponse::SeeOther()
            .insert_header(("Location", "/auth/login"))
            .finish();
    };
    let body = page_or_partial(
        &req,
        render_new_task_page(),
        Some(&user.username),
        "New task",
    )
    .into_string();
    HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(body)
}

#[post("/tickets")]
pub async fn create(
    identity: Option<Identity>,
    db: web::Data<Database>,
    payload: web::Bytes,
) -> impl Responder {
    let Some(user): Option<User> = current_user(identity, &db).await else {
        return HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish();
    };
    let Ok(fields) = super::form::parse_form(&payload) else {
        return HttpResponse::BadRequest().body(render_error("Invalid form data.").into_string());
    };
    let title = form_value(&fields, "title");
    let title = title.trim();
    if title.is_empty() || title.chars().count() > 120 {
        return HttpResponse::BadRequest()
            .body(render_error("Tasks need a title of 1–120 characters.").into_string());
    }
    let description = form_value(&fields, "description");
    if description.chars().count() > 2000 {
        return HttpResponse::BadRequest()
            .body(render_error("Task details must be at most 2,000 characters.").into_string());
    }
    let tag_names = form_values(&fields, "new_tags")
        .join(",")
        .split(',')
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(str::to_string)
        .collect::<Vec<_>>();
    if tag_names.iter().any(|name| name.chars().count() > 40) {
        return HttpResponse::BadRequest()
            .body(render_error("Tags must be at most 40 characters.").into_string());
    }
    let mut tag_names = tag_names;
    tag_names.sort();
    tag_names.dedup();
    let ticket = Ticket {
        id: Uuid::new_v4().to_string(),
        title: title.to_string(),
        description: description.trim().to_string(),
        creator_id: user.id,
        created_at: chrono::Utc::now().to_rfc3339(),
    };
    match db.create_ticket(&ticket, &tag_names).await {
        Ok(()) => HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish(),
        Err(error) => {
            log::error!("Failed to create private task: {error}");
            HttpResponse::InternalServerError()
                .body(render_error("Could not add this task.").into_string())
        }
    }
}

#[post("/tickets/{id}/close")]
pub async fn close(
    identity: Option<Identity>,
    db: web::Data<Database>,
    path: web::Path<String>,
    payload: web::Bytes,
) -> HttpResponse {
    let Some(user) = current_user(identity, &db).await else {
        return HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish();
    };
    let Ok(fields) = super::form::parse_form(&payload) else {
        return HttpResponse::BadRequest().finish();
    };
    let outcome = form_value(&fields, "outcome");
    if !matches!(outcome.as_str(), "done" | "not_planned") {
        return HttpResponse::BadRequest()
            .body(render_error("Choose Done or Not planned.").into_string());
    }
    match db
        .close_ticket(&path.into_inner(), &user.id, &outcome)
        .await
    {
        Ok(true | false) => HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish(),
        Err(error) => {
            log::error!("Failed to close task: {error}");
            HttpResponse::InternalServerError().finish()
        }
    }
}

#[post("/tickets/{id}/reopen")]
pub async fn reopen(
    identity: Option<Identity>,
    db: web::Data<Database>,
    path: web::Path<String>,
) -> HttpResponse {
    let Some(user) = current_user(identity, &db).await else {
        return HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish();
    };
    match db.reopen_ticket(&path.into_inner(), &user.id).await {
        Ok(true | false) => HttpResponse::SeeOther()
            .insert_header(("Location", "/"))
            .finish(),
        Err(error) => {
            log::error!("Failed to reopen task: {error}");
            HttpResponse::InternalServerError().finish()
        }
    }
}