Skip to content
use actix_web::{HttpResponse, Responder, delete, get, post, web};
use serde::Deserialize;

use crate::{
    auth::NotesContext,
    config::Server,
    view::auth::render_login_success,
    view::session_auth::get_username_from_request,
    view::{render_error, render_success},
};

#[derive(Debug, Deserialize)]
pub struct LoginForm {
    pub username: String,
    pub password: String,
}

#[derive(Debug, Deserialize)]
pub struct SaveForm {
    pub content: String,
    pub category: Option<String>,
    pub tags: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct EditForm {
    pub title: String,
    pub content: String,
    pub category: Option<String>,
    pub tags: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct SearchQuery {
    pub q: Option<String>,
    pub category: Option<String>,
    pub tag: Option<String>,
}

pub fn derive_title(content: &str) -> String {
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            continue;
        }
        // Strip markdown header indicators
        let mut title = trimmed;
        while title.starts_with('#') {
            title = title.trim_start_matches('#').trim();
        }
        // Strip other common markdown symbols from start if any
        title = title.trim_start_matches('*').trim();
        title = title.trim_start_matches('_').trim();

        if title.is_empty() {
            continue;
        }

        if title.chars().count() > 60 {
            let char_vec: Vec<char> = title.chars().take(57).collect();
            return format!("{}...", char_vec.into_iter().collect::<String>());
        } else {
            return title.to_string();
        }
    }
    "Untitled Note".to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_derive_title() {
        assert_eq!(derive_title("hello world"), "hello world");
        assert_eq!(derive_title("# Hello World"), "Hello World");
        assert_eq!(derive_title("   \n##   Hello Rust"), "Hello Rust");
        assert_eq!(derive_title(""), "Untitled Note");
        assert_eq!(derive_title("   \n\n   "), "Untitled Note");
        assert_eq!(
            derive_title(
                "A very long first line of text that exceeds sixty characters in total length to see if truncation works perfectly"
            ),
            "A very long first line of text that exceeds sixty charact..."
        );
    }
}

#[post("/auth/login")]
pub async fn login_handler(
    auth_state: web::Data<NotesContext>,
    config: web::Data<Server>,
    form: web::Form<LoginForm>,
) -> impl Responder {
    if form.username != config.username() || form.password != config.password() {
        return HttpResponse::Unauthorized()
            .content_type("text/html; charset=utf-8")
            .body(render_error("Invalid username or password").into_string());
    }

    let token = match auth_state.create_session(form.username.clone()).await {
        Ok(token) => token,
        Err(e) => {
            log::error!("Failed to create session: {e}");
            return HttpResponse::InternalServerError()
                .content_type("text/html; charset=utf-8")
                .body(render_error("Failed to create session").into_string());
        }
    };

    log::info!("User logged in: {}", form.username);

    let one_year = actix_web::cookie::time::Duration::days(365);

    HttpResponse::Ok()
        .insert_header(("HX-Redirect", "/"))
        .cookie(
            actix_web::cookie::Cookie::build("session", token)
                .path("/")
                .http_only(true)
                .same_site(actix_web::cookie::SameSite::Strict)
                .max_age(one_year)
                .finish(),
        )
        .content_type("text/html; charset=utf-8")
        .body(render_login_success(&form.username).into_string())
}

#[post("/auth/logout")]
pub async fn logout_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
) -> impl Responder {
    let token = match req.cookie("session") {
        Some(cookie) => cookie.value().to_string(),
        None => {
            return HttpResponse::Unauthorized().body("Not logged in");
        }
    };

    if let Err(e) = auth_state.invalidate_token(&token).await {
        log::error!("Failed to invalidate token: {e}");
        return HttpResponse::InternalServerError().body("Failed to logout");
    }

    HttpResponse::Found()
        .insert_header(("Location", "/"))
        .cookie(
            actix_web::cookie::Cookie::build("session", "")
                .path("/")
                .http_only(true)
                .same_site(actix_web::cookie::SameSite::Strict)
                .max_age(actix_web::cookie::time::Duration::ZERO)
                .finish(),
        )
        .finish()
}

#[post("/save")]
pub async fn save_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
    form: web::Form<SaveForm>,
) -> impl Responder {
    let _username = match get_username_from_request(&req, &auth_state).await {
        Some(name) => name,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    let content = form.content.trim();
    if content.is_empty() {
        return HttpResponse::BadRequest()
            .content_type("text/html; charset=utf-8")
            .body(render_error("Note content cannot be empty").into_string());
    }

    let title = derive_title(content);

    let tags_str = form.tags.clone().unwrap_or_default();
    let tags_list: Vec<String> = tags_str
        .split(',')
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
        .collect();

    let cat_opt = form
        .category
        .as_deref()
        .map(|c| c.trim())
        .filter(|c| !c.is_empty());

    match auth_state
        .db()
        .create_note(&title, content, cat_opt, &tags_list)
        .await
    {
        Ok(_) => HttpResponse::Ok()
            .insert_header(("HX-Redirect", "/view"))
            .finish(),
        Err(e) => {
            log::error!("Error saving note: {e}");
            HttpResponse::InternalServerError()
                .content_type("text/html; charset=utf-8")
                .body(render_error("Failed to save note in database").into_string())
        }
    }
}

#[get("/view/search")]
pub async fn search_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
    query: web::Query<SearchQuery>,
) -> impl Responder {
    let _username = match get_username_from_request(&req, &auth_state).await {
        Some(name) => name,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    let q = query.q.clone().unwrap_or_default();
    let cat = query.category.as_deref().filter(|c| !c.is_empty());
    let tag = query.tag.as_deref().filter(|t| !t.is_empty());

    match auth_state.db().search_notes(&q, cat, tag).await {
        Ok(notes) => {
            let markup = maud::html! {
                div class="flex flex-column gap-2" {
                    @if notes.is_empty() {
                        p class="gray tc mt4 f5" { "No notes found matching your filters." }
                    } @else {
                        @for note in notes {
                            div
                                class="selection-card bg-white pa3 pointer mb2 shadow-hover border-box"
                                hx-get=(format!("/notes/{}", note.id))
                                hx-target="#note-details"
                                style="border: 1px solid #e2e8f0;"
                            {
                                h3 class="f5 fw6 ma0 brand mb1" { (note.title) }
                                p class="f6 black-60 ma0 mb2 truncate" { (note.content) }
                                div class="flex flex-wrap items-center justify-between" {
                                     @if let Some(ref cat) = note.category {
                                         span class="f7 bg-light-blue dark-blue ph2 pv1 mr2" { (cat) }
                                     } @else {
                                         span {}
                                     }
                                     div class="flex flex-wrap gap-1" {
                                         @for tag in &note.tags {
                                             span class="f7 bg-light-gray gray ph2 pv1 mr1" { (tag) }
                                         }
                                     }
                                }
                            }
                        }
                    }
                }
            };
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(markup.into_string())
        }
        Err(e) => {
            log::error!("Error searching notes: {e}");
            HttpResponse::InternalServerError().body("Error searching notes")
        }
    }
}

#[get("/notes/{id}")]
pub async fn note_details_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
    id: web::Path<String>,
) -> impl Responder {
    let _username = match get_username_from_request(&req, &auth_state).await {
        Some(name) => name,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    let id = id.into_inner();
    match auth_state.db().get_note_by_id(&id).await {
        Ok(Some(note)) => {
            let markup = maud::html! {
                div class="bg-white pa4 shadow-4 ba b--black-10" {
                    div class="flex items-center justify-between mb3 pb2 bb b--black-10" {
                        h2 class="f4 fw6 brand ma0" { "Note Details" }
                        button
                            hx-delete=(format!("/notes/{}", note.id))
                            hx-confirm="Are you sure you want to delete this note?"
                            hx-target="#note-details"
                            class="f6 link red hover-dark-red bg-transparent bn pointer underline"
                        {
                            "Delete Note"
                        }
                    }

                    form
                        hx-post=(format!("/notes/{}/edit", note.id))
                        hx-target="#edit-result"
                        class="flex flex-column"
                    {
                        div class="mb3" {
                            label class="db f6 fw6 black-70 mb2" for="edit-title" { "Title" }
                            input
                                type="text"
                                name="title"
                                id="edit-title"
                                required
                                value=(note.title)
                                class="db w-100 pa2 ba b--black-20 f5 fw6 brand";
                        }

                        div class="mb3" {
                            label class="db f6 fw6 black-70 mb2" for="edit-content" { "Note Content" }
                            textarea
                                name="content"
                                id="edit-content"
                                required
                                rows="10"
                                class="db w-100 pa3 ba b--black-20 f5 code"
                                style="resize: vertical;"
                            { (note.content) }
                        }

                        div class="flex flex-column flex-row-ns gap-3 mb4" {
                            div class="w-100 w-50-ns mb3 mb0-ns mr2-ns" {
                                label class="db f6 fw6 black-70 mb2" for="edit-category" { "Category" }
                                input
                                    type="text"
                                    name="category"
                                    id="edit-category"
                                    value=(note.category.unwrap_or_default())
                                    class="db w-100 pa2 ba b--black-20 f5";
                            }

                            div class="w-100 w-50-ns ml2-ns" {
                                label class="db f6 fw6 black-70 mb2" for="edit-tags" { "Tags (comma-separated)" }
                                input
                                    type="text"
                                    name="tags"
                                    id="edit-tags"
                                    value=(note.tags.join(", "))
                                    class="db w-100 pa2 ba b--black-20 f5";
                            }
                        }

                        button
                            type="submit"
                            class="btn-brand f5 fw6 cursor-pointer ph4 pv2 mr2 self-start"
                        {
                            "Save Changes"
                        }
                    }

                    div id="edit-result" class="mt3" {}
                }
            };
            HttpResponse::Ok()
                .content_type("text/html; charset=utf-8")
                .body(markup.into_string())
        }
        Ok(None) => HttpResponse::NotFound().body("Note not found"),
        Err(e) => {
            log::error!("Error fetching note: {e}");
            HttpResponse::InternalServerError().body("Database error")
        }
    }
}

#[post("/notes/{id}/edit")]
pub async fn edit_note_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
    id: web::Path<String>,
    form: web::Form<EditForm>,
) -> impl Responder {
    let _username = match get_username_from_request(&req, &auth_state).await {
        Some(name) => name,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    let id = id.into_inner();
    let content = form.content.trim();
    let title = form.title.trim();

    if content.is_empty() || title.is_empty() {
        return HttpResponse::BadRequest()
            .content_type("text/html; charset=utf-8")
            .body(render_error("Title and content cannot be empty").into_string());
    }

    let tags_str = form.tags.clone().unwrap_or_default();
    let tags_list: Vec<String> = tags_str
        .split(',')
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
        .collect();

    let cat_opt = form
        .category
        .as_deref()
        .map(|c| c.trim())
        .filter(|c| !c.is_empty());

    match auth_state
        .db()
        .update_note(&id, title, content, cat_opt, &tags_list)
        .await
    {
        Ok(()) => HttpResponse::Ok()
            .insert_header(("HX-Trigger", "refresh-list"))
            .content_type("text/html; charset=utf-8")
            .body(render_success("Changes saved successfully!").into_string()),
        Err(e) => {
            log::error!("Error updating note: {e}");
            HttpResponse::InternalServerError()
                .content_type("text/html; charset=utf-8")
                .body(render_error("Failed to save changes to database").into_string())
        }
    }
}

#[delete("/notes/{id}")]
pub async fn delete_note_handler(
    req: actix_web::HttpRequest,
    auth_state: web::Data<NotesContext>,
    id: web::Path<String>,
) -> impl Responder {
    let _username = match get_username_from_request(&req, &auth_state).await {
        Some(name) => name,
        None => return HttpResponse::Unauthorized().body("Unauthorized"),
    };

    let id = id.into_inner();
    match auth_state.db().delete_note(&id).await {
        Ok(()) => {
            let markup = maud::html! {
                div class="bg-white pa4 shadow-4 ba b--black-10 tc gray f5 flex flex-column justify-center items-center" style="min-height: 300px;" {
                    svg class="w3 h3 mb3 black-30" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {
                        path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" {}
                    }
                    div class="ba b--green pa3 bg-washed-green mb3 w-100" {
                        p class="f6 dark-green ma0" { "Note successfully deleted!" }
                    }
                    "Select another note from the left to view and edit its details."
                }
            };
            HttpResponse::Ok()
                .insert_header(("HX-Trigger", "refresh-list"))
                .content_type("text/html; charset=utf-8")
                .body(markup.into_string())
        }
        Err(e) => {
            log::error!("Error deleting note: {e}");
            HttpResponse::InternalServerError()
                .content_type("text/html; charset=utf-8")
                .body(render_error("Failed to delete note from database").into_string())
        }
    }
}