//! HTMX user dashboard endpoints.
//!
//! Tabs are individual GET endpoints that return panel fragments. The shell
//! emits the tab strip + initial tab content; clicking a tab swaps only the
//! `#dashboard-content` region with `hx-push-url` so the back button works.
//!
//! Routes mounted under `/api/htmx/user`:
//!
//! - `GET  /dashboard`                — tab strip + active tab content.
//! - `GET  /dashboard/account`        — account info panel.
//! - `GET  /dashboard/bookmarks`      — bookmarks list panel.
//! - `POST /bookmarks`                — add bookmark for the referrer URL.
//! - `DELETE /bookmarks/{id}`         — remove bookmark.
//! - `GET  /dashboard/notes`          — notes list panel (read-only for now).

#[cfg(feature = "auth")]
mod imp {
    use std::sync::Arc;

    use axum::extract::{Path, Query, State};
    use axum::http::StatusCode;
    use axum::response::{Html, IntoResponse, Response};
    use axum::routing::{delete, get, post};
    use axum::{Extension, Form, Router};
    use html_escape::{encode_double_quoted_attribute, encode_text};
    use serde::Deserialize;

    use crate::api::htmx_sse::{publish_toast, ToastEvent, ToastLevel};
    use crate::auth::middleware::AuthContext;
    use crate::auth::AuthService;
    use crate::database::auth::AddBookmarkRequest;

    #[derive(Debug, Deserialize)]
    pub(super) struct DashboardQuery {
        #[serde(default)]
        pub(super) tab: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    pub(super) struct AddBookmarkForm {
        pub(super) content_type: String,
        pub(super) content_id: String,
        #[serde(default)]
        pub(super) content_title: Option<String>,
        #[serde(default)]
        pub(super) content_url: Option<String>,
    }

    #[derive(Debug, Deserialize)]
    pub(super) struct NoteForm {
        pub(super) title: String,
        pub(super) content: String,
    }

    pub fn htmx_user_routes() -> Router<Arc<AuthService>> {
        Router::new()
            .route("/dashboard", get(dashboard))
            .route("/dashboard/account", get(tab_account))
            .route("/dashboard/bookmarks", get(tab_bookmarks))
            .route("/dashboard/notes", get(tab_notes))
            .route("/bookmarks", post(add_bookmark))
            .route("/bookmarks/{id}", delete(delete_bookmark))
            .route("/notes", post(add_note))
            .route("/notes/{id}", delete(delete_note).put(update_note))
    }

    async fn dashboard(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Query(q): Query<DashboardQuery>,
    ) -> Response {
        let Some(user) = auth.user.clone() else {
            return unauthorised();
        };
        let active = q.tab.as_deref().unwrap_or("account");
        let panel = panel_for(&svc, &user, active).await;
        let html = format!(
            r#"<section id="dashboard"><nav class="ds-tabs" role="tablist">{tabs}</nav><div id="dashboard-content" role="tabpanel">{panel}</div></section>"#,
            tabs = render_tabs(active),
            panel = panel,
        );
        (StatusCode::OK, Html(html)).into_response()
    }

    async fn tab_account(Extension(auth): Extension<AuthContext>) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        let html = render_account(user);
        (StatusCode::OK, Html(html)).into_response()
    }

    async fn tab_bookmarks(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        let bookmarks = svc
            .repository
            .get_bookmarks(user.id)
            .await
            .unwrap_or_default();
        let html = render_bookmarks_list(&bookmarks);
        (StatusCode::OK, Html(html)).into_response()
    }

    async fn tab_notes(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        let notes = svc.repository.get_notes(user.id).await.unwrap_or_default();
        let html = render_notes_list(&notes);
        (StatusCode::OK, Html(html)).into_response()
    }

    async fn add_bookmark(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Form(form): Form<AddBookmarkForm>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        let req = AddBookmarkRequest {
            user_id: user.id,
            content_type: form.content_type,
            content_id: form.content_id,
            content_title: form.content_title,
            content_url: form.content_url,
        };
        if let Err(e) = svc.repository.add_bookmark(&req).await {
            tracing::warn!("add_bookmark failed: {e}");
            publish_toast(ToastEvent {
                level: ToastLevel::Error,
                message: format!("Bookmark failed: {e}"),
                id: None,
            });
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Html(error_panel(&e.to_string())),
            )
                .into_response();
        }
        publish_toast(ToastEvent {
            level: ToastLevel::Success,
            message: "Bookmark saved".to_string(),
            id: None,
        });
        let bookmarks = svc
            .repository
            .get_bookmarks(user.id)
            .await
            .unwrap_or_default();
        (StatusCode::OK, Html(render_bookmarks_list(&bookmarks))).into_response()
    }

    async fn delete_bookmark(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Path(id): Path<uuid::Uuid>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        if let Err(e) = svc.repository.remove_bookmark(id, user.id).await {
            tracing::warn!("remove_bookmark failed: {e}");
            publish_toast(ToastEvent {
                level: ToastLevel::Error,
                message: format!("Bookmark removal failed: {e}"),
                id: None,
            });
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Html(error_panel(&e.to_string())),
            )
                .into_response();
        }
        publish_toast(ToastEvent {
            level: ToastLevel::Info,
            message: "Bookmark removed".to_string(),
            id: None,
        });
        // Empty body + `hx-swap="delete"` on the row removes the element.
        (StatusCode::OK, Html(String::new())).into_response()
    }

    async fn add_note(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Form(form): Form<NoteForm>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        if form.title.trim().is_empty() {
            return (
                StatusCode::BAD_REQUEST,
                Html(error_panel("Title is required")),
            )
                .into_response();
        }
        if let Err(e) = svc
            .repository
            .create_note(user.id, form.title.trim(), form.content.trim())
            .await
        {
            tracing::warn!("create_note failed: {e}");
            publish_toast(ToastEvent {
                level: ToastLevel::Error,
                message: format!("Note save failed: {e}"),
                id: None,
            });
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Html(error_panel(&e.to_string())),
            )
                .into_response();
        }
        publish_toast(ToastEvent {
            level: ToastLevel::Success,
            message: "Note saved".into(),
            id: None,
        });
        let notes = svc.repository.get_notes(user.id).await.unwrap_or_default();
        (StatusCode::OK, Html(render_notes_list(&notes))).into_response()
    }

    async fn update_note(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Path(id): Path<uuid::Uuid>,
        Form(form): Form<NoteForm>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        if let Err(e) = svc
            .repository
            .update_note(id, user.id, form.title.trim(), form.content.trim())
            .await
        {
            tracing::warn!("update_note failed: {e}");
            publish_toast(ToastEvent {
                level: ToastLevel::Error,
                message: format!("Note update failed: {e}"),
                id: None,
            });
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Html(error_panel(&e.to_string())),
            )
                .into_response();
        }
        publish_toast(ToastEvent {
            level: ToastLevel::Success,
            message: "Note updated".into(),
            id: None,
        });
        let notes = svc.repository.get_notes(user.id).await.unwrap_or_default();
        (StatusCode::OK, Html(render_notes_list(&notes))).into_response()
    }

    async fn delete_note(
        State(svc): State<Arc<AuthService>>,
        Extension(auth): Extension<AuthContext>,
        Path(id): Path<uuid::Uuid>,
    ) -> Response {
        let Some(user) = auth.user.as_ref() else {
            return unauthorised();
        };
        if let Err(e) = svc.repository.delete_note(id, user.id).await {
            tracing::warn!("delete_note failed: {e}");
            publish_toast(ToastEvent {
                level: ToastLevel::Error,
                message: format!("Note delete failed: {e}"),
                id: None,
            });
            return (
                StatusCode::INTERNAL_SERVER_ERROR,
                Html(error_panel(&e.to_string())),
            )
                .into_response();
        }
        publish_toast(ToastEvent {
            level: ToastLevel::Info,
            message: "Note removed".into(),
            id: None,
        });
        (StatusCode::OK, Html(String::new())).into_response()
    }

    async fn panel_for(
        svc: &AuthService,
        user: &rustelo_core_lib::auth::User,
        active: &str,
    ) -> String {
        match active {
            "bookmarks" => {
                let bookmarks = svc
                    .repository
                    .get_bookmarks(user.id)
                    .await
                    .unwrap_or_default();
                render_bookmarks_list(&bookmarks)
            }
            "notes" => {
                let notes = svc.repository.get_notes(user.id).await.unwrap_or_default();
                render_notes_list(&notes)
            }
            _ => render_account(user),
        }
    }

    fn render_tabs(active: &str) -> String {
        let tabs = [
            ("account", "Account"),
            ("bookmarks", "Bookmarks"),
            ("notes", "Notes"),
        ];
        let mut out = String::new();
        for (slug, label) in tabs {
            let is_active = slug == active;
            let aria = if is_active {
                r#" aria-selected="true""#
            } else {
                ""
            };
            let class = if is_active { " ds-tab-active" } else { "" };
            out.push_str(&format!(
                r##"<button type="button" role="tab" class="ds-tab{class}"{aria} hx-get="/api/htmx/user/dashboard/{slug}" hx-target="#dashboard-content" hx-swap="innerHTML" hx-push-url="/user?tab={slug}">{label}</button>"##,
            ));
        }
        out
    }

    fn render_account(user: &rustelo_core_lib::auth::User) -> String {
        let username = encode_text(&user.username);
        let email = encode_text(&user.email);
        let display = user
            .display_name
            .as_deref()
            .map(encode_text)
            .unwrap_or_default();
        format!(
            r#"<dl class="ds-list"><dt>Username</dt><dd>{username}</dd><dt>Email</dt><dd>{email}</dd><dt>Display name</dt><dd>{display}</dd></dl>"#
        )
    }

    fn render_bookmarks_list(rows: &[crate::database::auth::BookmarkRow]) -> String {
        if rows.is_empty() {
            return r#"<p class="ds-text-muted">No bookmarks yet.</p>"#.to_string();
        }
        let mut out = String::from(r#"<ul class="ds-list" id="bookmarks-list">"#);
        for row in rows {
            let title = row
                .content_title
                .as_deref()
                .map(encode_text)
                .unwrap_or_else(|| encode_text(&row.content_id));
            let href = row
                .content_url
                .as_deref()
                .map(encode_double_quoted_attribute)
                .unwrap_or_default();
            let id = row.id;
            out.push_str(&format!(
                r##"<li id="bookmark-{id}"><a href="{href}">{title}</a><button type="button" class="ds-btn-ghost-sm" hx-delete="/api/htmx/user/bookmarks/{id}" hx-target="#bookmark-{id}" hx-swap="outerHTML" aria-label="Remove bookmark">✕</button></li>"##,
            ));
        }
        out.push_str("</ul>");
        out
    }

    fn render_notes_list(rows: &[crate::database::auth::NoteRow]) -> String {
        let form = r##"<form id="note-create-form" hx-post="/api/htmx/user/notes" hx-target="#notes-panel" hx-swap="outerHTML" class="space-y-2 mb-4"><input type="text" name="title" placeholder="Title" required class="ds-input"><textarea name="content" placeholder="Content…" class="ds-input"></textarea><button type="submit" class="ds-btn-primary">Save note</button></form>"##;
        if rows.is_empty() {
            return format!(
                r##"<div id="notes-panel">{form}<p class="ds-text-muted">No notes yet.</p></div>"##
            );
        }
        let mut items = String::new();
        for row in rows {
            let id = row.id;
            let title = encode_text(&row.title);
            let content = encode_text(&row.content);
            items.push_str(&format!(
                r##"<li id="note-{id}" class="ds-card"><h4>{title}</h4><p>{content}</p><button type="button" class="ds-btn-ghost-sm" hx-delete="/api/htmx/user/notes/{id}" hx-target="#note-{id}" hx-swap="outerHTML" aria-label="Delete note">✕</button></li>"##,
            ));
        }
        format!(r##"<div id="notes-panel">{form}<ul class="ds-list">{items}</ul></div>"##)
    }

    fn error_panel(message: &str) -> String {
        format!(
            r#"<div class="ds-alert-error" role="alert">{}</div>"#,
            encode_text(message)
        )
    }

    fn unauthorised() -> Response {
        let html = r#"<div class="ds-alert-error" role="alert">You must sign in to view this section.</div>"#;
        (StatusCode::UNAUTHORIZED, Html(html)).into_response()
    }

    #[cfg(test)]
    mod tests {
        use chrono::Utc;
        use uuid::Uuid;

        use super::*;

        fn sample_user() -> rustelo_core_lib::auth::User {
            rustelo_core_lib::auth::User {
                id: Uuid::nil(),
                email: "j@x".into(),
                username: "j".into(),
                display_name: Some("Jay".into()),
                avatar_url: None,
                roles: vec![],
                is_active: true,
                email_verified: true,
                created_at: Utc::now(),
                updated_at: Utc::now(),
                last_login: None,
                profile: rustelo_core_lib::auth::UserProfile::default(),
                two_factor_enabled: false,
            }
        }

        #[test]
        fn account_panel_renders_user_fields() {
            let user = sample_user();
            let html = render_account(&user);
            assert!(html.contains("j@x"));
            assert!(html.contains(">j<"));
            assert!(html.contains("Jay"));
        }

        #[test]
        fn tabs_mark_active_with_aria_selected() {
            let html = render_tabs("bookmarks");
            assert!(html.contains(r#"aria-selected="true""#));
            // Three buttons; only one active.
            assert_eq!(html.matches(r#"aria-selected="true""#).count(), 1);
            assert_eq!(html.matches("<button").count(), 3);
        }

        #[test]
        fn empty_bookmarks_renders_placeholder() {
            assert!(render_bookmarks_list(&[]).contains("No bookmarks yet"));
        }

        #[test]
        fn bookmark_row_carries_delete_endpoint() {
            let row = crate::database::auth::BookmarkRow {
                id: Uuid::nil(),
                user_id: Uuid::nil(),
                content_type: "blog".into(),
                content_id: "post-1".into(),
                content_title: Some("Hello".into()),
                content_url: Some("/blog/post-1".into()),
                created_at: Utc::now(),
            };
            let html = render_bookmarks_list(&[row]);
            assert!(html.contains(r#"hx-delete="/api/htmx/user/bookmarks/"#));
            assert!(html.contains(r#"id="bookmark-"#));
        }

        #[test]
        fn empty_notes_renders_placeholder() {
            assert!(render_notes_list(&[]).contains("No notes yet"));
        }
    }
}

#[cfg(feature = "auth")]
pub use imp::htmx_user_routes;
