use std::convert::Infallible;
use std::path::PathBuf;
use std::sync::Arc;

use axum::{
    extract::{Query, State},
    http::{header, StatusCode},
    response::{
        sse::{Event as SseEvent, KeepAlive},
        Html, IntoResponse, Response, Sse,
    },
};
use futures::Stream;
use serde::Deserialize;
use tokio::sync::broadcast;
use tokio_stream::{wrappers::BroadcastStream, StreamExt};

use crate::files::{list_puml_files, resolve_safe, PathError};
use crate::renderer::render_svg;
use crate::watcher::FileEvent;

#[derive(Clone)]
pub struct AppState {
    pub root: Arc<PathBuf>,
    pub plantuml_jar: Arc<PathBuf>,
    pub event_tx: broadcast::Sender<FileEvent>,
}

#[derive(Deserialize)]
pub struct FileQuery {
    pub f: String,
}

// ── Static assets ────────────────────────────────────────────────────────────

pub async fn index_handler() -> Html<&'static str> {
    Html(include_str!("../static/index.html"))
}

pub async fn viewer_js_handler() -> impl IntoResponse {
    (
        [(header::CONTENT_TYPE, "application/javascript; charset=utf-8")],
        include_str!("../static/js/viewer.js"),
    )
}

pub async fn style_css_handler() -> impl IntoResponse {
    (
        [(header::CONTENT_TYPE, "text/css; charset=utf-8")],
        include_str!("../static/css/style.css"),
    )
}

pub async fn htmx_js_handler() -> impl IntoResponse {
    (
        [(header::CONTENT_TYPE, "application/javascript; charset=utf-8")],
        include_str!("../static/js/htmx.min.js"),
    )
}

// ── API ───────────────────────────────────────────────────────────────────────

/// Returns an HTML fragment listing all .puml files as clickable items.
pub async fn files_handler(State(state): State<AppState>) -> Html<String> {
    let mut files = list_puml_files(&state.root);
    files.sort();
    let html = files
        .iter()
        .map(|f| {
            format!(
                r#"<a class="file-item" data-path="{f}" title="{f}">{f}</a>"#,
                f = html_escape(f)
            )
        })
        .collect::<Vec<_>>()
        .join("\n");
    Html(html)
}

/// Render a .puml file and return the SVG as text/html so HTMX can swap it.
pub async fn render_handler(
    State(state): State<AppState>,
    Query(q): Query<FileQuery>,
) -> Response {
    let canonical = match resolve_safe(&state.root, &q.f) {
        Ok(p) => p,
        Err(PathError::Traversal) => {
            return (StatusCode::FORBIDDEN, "path traversal denied").into_response()
        }
        Err(e) => return (StatusCode::BAD_REQUEST, e.to_string()).into_response(),
    };

    match render_svg(&state.plantuml_jar, &canonical).await {
        Ok(svg) => (
            [
                (header::CONTENT_TYPE, "text/html; charset=utf-8"),
                (header::CACHE_CONTROL, "no-store"),
            ],
            svg,
        )
            .into_response(),
        Err(e) => {
            tracing::warn!("render failed for {:?}: {e}", canonical);
            (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
        }
    }
}

/// Server-Sent Events stream; fires a `change` event with the relative path
/// whenever a .puml file under root is created, modified, or removed.
pub async fn events_handler(
    State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<SseEvent, Infallible>>> {
    let rx = state.event_tx.subscribe();
    let stream = BroadcastStream::new(rx)
        .filter_map(|msg| msg.ok())
        .map(|ev| {
            Ok::<SseEvent, Infallible>(
                SseEvent::default()
                    .event("change")
                    .data(ev.relative_path),
            )
        });
    Sse::new(stream).keep_alive(KeepAlive::default())
}

// ── Helpers ───────────────────────────────────────────────────────────────────

fn html_escape(s: &str) -> String {
    s.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
}

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

    #[test]
    fn html_escape_replaces_special_chars() {
        assert_eq!(html_escape(r#"a<b>&"c""#), "a&lt;b&gt;&amp;&quot;c&quot;");
    }

    #[test]
    fn html_escape_leaves_plain_text() {
        assert_eq!(html_escape("sub/diagram.puml"), "sub/diagram.puml");
    }
}
