//! HTTP API server — `mps serve`.
//!
//! All Store I/O is synchronous; handlers wrap it in `spawn_blocking`.

use crate::api::{
    self, AppendRequest, ElementJson, ErrorResponse, StatsJson, UpdateRequest,
};
use crate::date_parse;
use crate::error::MpsError;
use crate::ref_resolver::RefResolver;
use crate::store::Store;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{delete as route_delete, get, patch};
use axum::{Json, Router};

const UI_HTML: &str = include_str!("../assets/index.html");
use chrono::{Local, NaiveDate};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tower_http::cors::CorsLayer;

use crate::config::Config;

// ── State ─────────────────────────────────────────────────────────────────────

pub struct AppState {
    pub config: Config,
    #[allow(dead_code)]
    pub config_path: PathBuf,
}

// ── Error helpers ─────────────────────────────────────────────────────────────

type ApiResult<T> = Result<T, (StatusCode, Json<ErrorResponse>)>;

fn api_err(code: u16, msg: impl ToString) -> (StatusCode, Json<ErrorResponse>) {
    let status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
    (status, Json(ErrorResponse { error: msg.to_string() }))
}

fn mps_err(e: MpsError) -> (StatusCode, Json<ErrorResponse>) {
    let code = match &e {
        MpsError::DateParseError(_) | MpsError::ConfigInvalid(_) | MpsError::TimeParse(_) => {
            StatusCode::BAD_REQUEST
        }
        MpsError::ConfigNotFound(_) => StatusCode::NOT_FOUND,
        _ => StatusCode::INTERNAL_SERVER_ERROR,
    };
    (code, Json(ErrorResponse { error: e.to_string() }))
}

fn today() -> NaiveDate {
    Local::now().date_naive()
}

fn parse_date_param(s: &str) -> Result<NaiveDate, (StatusCode, Json<ErrorResponse>)> {
    date_parse::parse_date(s).map_err(mps_err)
}

// ── Query param structs ───────────────────────────────────────────────────────

#[derive(Debug, Deserialize)]
pub struct ListParams {
    pub date: Option<String>,
    pub r#type: Option<String>,
    pub tag: Option<String>,
    pub since: Option<String>,
    #[serde(default)]
    pub all: bool,
}

#[derive(Debug, Deserialize)]
pub struct DeleteParams {
    pub date: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct SearchParams {
    pub q: Option<String>,
    pub r#type: Option<String>,
    pub tag: Option<String>,
    pub since: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct StatsParams {
    pub date: Option<String>,
    pub since: Option<String>,
    #[serde(default)]
    pub all: bool,
}

#[derive(Debug, Deserialize)]
pub struct ExportParams {
    pub format: Option<String>,
    pub date: Option<String>,
    pub since: Option<String>,
    pub r#type: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct TagsParams {
    pub date: Option<String>,
    #[serde(default)]
    pub all: bool,
    pub r#type: Option<String>,
}

// ── Bearer auth middleware (layer) ────────────────────────────────────────────

/// Axum middleware that checks Bearer auth when a token is configured.
/// Injects the checked header state via an extension.
async fn auth_middleware(
    State(state): State<Arc<AppState>>,
    headers: HeaderMap,
    mut req: axum::extract::Request,
    next: axum::middleware::Next,
) -> axum::response::Response {
    let token = &state.config.serve.token;
    if !token.is_empty() {
        let auth_header = headers
            .get("Authorization")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("");
        let expected = format!("Bearer {}", token);
        if auth_header != expected {
            let body = Json(ErrorResponse { error: "unauthorized".into() });
            return (StatusCode::UNAUTHORIZED, body).into_response();
        }
    }
    req.extensions_mut().insert(AuthChecked);
    next.run(req).await
}

#[derive(Clone)]
struct AuthChecked;

// ── Router ────────────────────────────────────────────────────────────────────

pub fn build_router(state: Arc<AppState>) -> Router {
    Router::new()
        .route("/", get(ui_handler))
        .route("/health", get(health_handler))
        .route("/elements", get(list_elements).post(append_element))
        .route(
            "/elements/:element_ref",
            get(get_element).patch(update_element).delete(delete_element),
        )
        .route("/search", get(search_handler))
        .route("/stats", get(stats_handler))
        .route("/export", get(export_handler))
        .route("/tags", get(tags_handler))
        .layer(axum::middleware::from_fn_with_state(
            state.clone(),
            auth_middleware,
        ))
        .layer(CorsLayer::permissive())
        .with_state(state)
}

pub async fn run(config: Config, config_path: PathBuf) -> anyhow::Result<()> {
    let addr = format!("{}:{}", config.serve.host, config.serve.port);
    let state = Arc::new(AppState { config, config_path });
    let listener = tokio::net::TcpListener::bind(&addr).await?;
    println!("  mps serve  http://{}", addr);
    axum::serve(listener, build_router(state)).await?;
    Ok(())
}

// ── Handlers ──────────────────────────────────────────────────────────────────

async fn ui_handler() -> impl IntoResponse {
    axum::response::Response::builder()
        .status(200)
        .header("Content-Type", "text/html; charset=utf-8")
        .body(axum::body::Body::from(UI_HTML))
        .unwrap()
}

async fn health_handler() -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "status": "ok",
        "version": env!("CARGO_PKG_VERSION"),
    }))
}

async fn list_elements(
    State(state): State<Arc<AppState>>,
    Query(params): Query<ListParams>,
) -> ApiResult<Json<Vec<ElementJson>>> {
    let config = state.config.clone();

    let date = match &params.date {
        Some(d) => parse_date_param(d)?,
        None => today(),
    };
    let since = match &params.since {
        Some(s) => Some(parse_date_param(s)?),
        None => None,
    };

    tokio::task::spawn_blocking(move || {
        let store = Store::new(&config.storage_dir);
        let files = if params.all {
            store.all_files().map_err(mps_err)?
        } else if let Some(sd) = since {
            store.files_since(sd).map_err(mps_err)?
        } else {
            store.find_files(date)
        };

        let mut results = Vec::new();
        for file in &files {
            let date_str = file
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n[..8].to_string())
                .unwrap_or_default();

            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            let resolver = RefResolver::new(&elements);

            for (epoch_ref, el) in &elements {
                if el.is_mps_group() || el.is_unknown() {
                    continue;
                }
                if let Some(ref tf) = params.r#type {
                    if el.sign() != tf.as_str() {
                        continue;
                    }
                }
                if let Some(ref tag) = params.tag {
                    if !el.tags().iter().any(|t| t == tag) {
                        continue;
                    }
                }
                results.push(api::element_to_json(el, epoch_ref, &date_str, &resolver));
            }
        }
        Ok(Json(results))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn get_element(
    State(state): State<Arc<AppState>>,
    Path(element_ref): Path<String>,
    Query(params): Query<DeleteParams>,
) -> ApiResult<Json<ElementJson>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let date = match &params.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };

        let store = Store::new(&config.storage_dir);
        // Resolve the ref to find its file
        let files = store.find_files(date);
        for file in &files {
            let date_str = file
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n[..8].to_string())
                .unwrap_or_default();
            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            let resolver = RefResolver::new(&elements);
            // Try to match by epoch ref or human ref
            for (epoch_ref, el) in &elements {
                if el.is_mps_group() || el.is_unknown() {
                    continue;
                }
                let human = resolver.to_human(epoch_ref).map(|s| s.to_string());
                if epoch_ref == &element_ref || human.as_deref() == Some(element_ref.as_str()) {
                    return Ok(Json(api::element_to_json(el, epoch_ref, &date_str, &resolver)));
                }
            }
        }
        // Also check all files for epoch refs that encode their date
        let all_files = store.all_files().map_err(mps_err)?;
        for file in &all_files {
            let date_str = file
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n[..8].to_string())
                .unwrap_or_default();
            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            let resolver = RefResolver::new(&elements);
            for (epoch_ref, el) in &elements {
                if el.is_mps_group() || el.is_unknown() {
                    continue;
                }
                if epoch_ref == &element_ref {
                    return Ok(Json(api::element_to_json(el, epoch_ref, &date_str, &resolver)));
                }
            }
        }
        Err(api_err(404, format!("element '{}' not found", element_ref)))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn append_element(
    State(state): State<Arc<AppState>>,
    Json(body): Json<AppendRequest>,
) -> ApiResult<(StatusCode, Json<serde_json::Value>)> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let kind = body.element_type.to_lowercase();
        const VALID: &[&str] = &["task", "note", "log", "reminder", "character"];
        if !VALID.contains(&kind.as_str()) {
            return Err(api_err(
                400,
                format!("unknown type '{}'; valid: {}", kind, VALID.join(", ")),
            ));
        }

        let date = match &body.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };

        let mut attrs: Vec<(&str, String)> = Vec::new();
        if let Some(ref s) = body.status {
            attrs.push(("status", s.clone()));
        }
        if let Some(ref a) = body.at {
            attrs.push(("at", a.clone()));
        }
        if let Some(ref s) = body.start {
            attrs.push(("start", s.clone()));
        }
        if let Some(ref e) = body.end {
            attrs.push(("end", e.clone()));
        }
        if let Some(ref n) = body.name {
            attrs.push(("name", n.clone()));
        }
        let attrs_ref: Vec<(&str, &str)> =
            attrs.iter().map(|(k, v)| (*k, v.as_str())).collect();

        let store = Store::new(&config.storage_dir);
        store
            .append(&kind, &body.body, &body.tags, &attrs_ref, date)
            .map_err(mps_err)?;

        // Re-parse to find the new epoch ref (last element matching kind+body).
        let path = store.find_or_create_path(date);
        let elements = crate::parser::parse_file(&path).map_err(mps_err)?;
        let resolver = RefResolver::new(&elements);

        let epoch_ref = elements
            .iter()
            .filter(|(_, el)| el.sign() == kind && el.body_str().trim() == body.body.trim())
            .map(|(k, _)| k.clone())
            .last()
            .unwrap_or_default();

        let human_ref = resolver.to_human(&epoch_ref).map(|s| s.to_string());

        Ok((
            StatusCode::CREATED,
            Json(serde_json::json!({ "ref": epoch_ref, "human_ref": human_ref })),
        ))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn update_element(
    State(state): State<Arc<AppState>>,
    Path(element_ref): Path<String>,
    Json(body): Json<UpdateRequest>,
) -> ApiResult<Json<serde_json::Value>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let date = match &body.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };

        let mut new_attrs: HashMap<String, String> = HashMap::new();
        if let Some(ref s) = body.status {
            new_attrs.insert("status".into(), s.clone());
        }
        if let Some(ref a) = body.at {
            new_attrs.insert("at".into(), a.clone());
        }
        if let Some(ref s) = body.start {
            new_attrs.insert("start".into(), s.clone());
        }
        if let Some(ref e) = body.end {
            new_attrs.insert("end".into(), e.clone());
        }
        if let Some(ref n) = body.name {
            new_attrs.insert("name".into(), n.clone());
        }

        let store = Store::new(&config.storage_dir);

        // Update body if provided
        if let Some(ref new_body) = body.body {
            let ok = store
                .replace_element_body(&element_ref, new_body, date)
                .map_err(mps_err)?;
            if !ok {
                return Err(api_err(404, format!("element '{}' not found", element_ref)));
            }
        }

        // Update attrs if any provided
        if !new_attrs.is_empty() {
            let ok = store
                .rewrite_element(&element_ref, &new_attrs, date)
                .map_err(|e| match &e {
                    MpsError::ConfigNotFound(_) => api_err(404, e.to_string()),
                    _ => mps_err(e),
                })?;
            if !ok && body.body.is_none() {
                return Err(api_err(404, format!("element '{}' not found", element_ref)));
            }
        }

        // If neither body nor attrs provided, still return success (no-op)
        if body.body.is_none() && new_attrs.is_empty() {
            return Err(api_err(400, "no fields to update"));
        }

        Ok(Json(serde_json::json!({ "updated": true })))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn delete_element(
    State(state): State<Arc<AppState>>,
    Path(element_ref): Path<String>,
    Query(params): Query<DeleteParams>,
) -> ApiResult<Json<serde_json::Value>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let date = match &params.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };

        let store = Store::new(&config.storage_dir);
        let deleted = store
            .delete_element(&element_ref, date)
            .map_err(mps_err)?;

        if !deleted {
            return Err(api_err(404, format!("element '{}' not found", element_ref)));
        }

        Ok(Json(serde_json::json!({ "deleted": true })))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn search_handler(
    State(state): State<Arc<AppState>>,
    Query(params): Query<SearchParams>,
) -> ApiResult<Json<Vec<ElementJson>>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let since = match &params.since {
            Some(s) => Some(parse_date_param(s)?),
            None => None,
        };
        let query = params.q.as_deref().unwrap_or("");

        let store = Store::new(&config.storage_dir);
        let results = store
            .search(
                query,
                params.r#type.as_deref(),
                params.tag.as_deref(),
                since,
            )
            .map_err(mps_err)?;

        let mut out = Vec::new();
        for sr in &results {
            let elements = crate::parser::parse_file(&sr.file).map_err(mps_err)?;
            let resolver = RefResolver::new(&elements);
            out.push(api::element_to_json(
                &sr.element,
                &sr.epoch_ref,
                &sr.date_str,
                &resolver,
            ));
        }
        Ok(Json(out))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn stats_handler(
    State(state): State<Arc<AppState>>,
    Query(params): Query<StatsParams>,
) -> ApiResult<Json<StatsJson>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let store = Store::new(&config.storage_dir);

        let date = match &params.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };
        let since = match &params.since {
            Some(s) => Some(parse_date_param(s)?),
            None => None,
        };

        let files = if params.all {
            store.all_files().map_err(mps_err)?
        } else if let Some(sd) = since {
            store.files_since(sd).map_err(mps_err)?
        } else {
            store.find_files(date)
        };

        let mut date_elements = Vec::new();
        for file in &files {
            let date_str = file
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n[..8].to_string())
                .unwrap_or_default();
            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            date_elements.push((date_str, elements));
        }

        Ok(Json(api::compute_stats(date_elements)))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}

async fn export_handler(
    State(state): State<Arc<AppState>>,
    Query(params): Query<ExportParams>,
) -> impl IntoResponse {
    let config = state.config.clone();

    let result = tokio::task::spawn_blocking(move || -> Result<(String, String), (StatusCode, Json<ErrorResponse>)> {
        let date = match &params.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };
        let since = match &params.since {
            Some(s) => Some(parse_date_param(s)?),
            None => None,
        };
        let fmt = params.format.as_deref().unwrap_or("json");

        let store = Store::new(&config.storage_dir);
        let files = if let Some(sd) = since {
            store.files_since(sd).map_err(mps_err)?
        } else {
            store.find_files(date)
        };

        let mut all_json: Vec<serde_json::Value> = Vec::new();

        for file in &files {
            let date_str = file
                .file_name()
                .and_then(|n| n.to_str())
                .map(|n| n[..8].to_string())
                .unwrap_or_default();
            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            let resolver = RefResolver::new(&elements);

            for (epoch_ref, el) in &elements {
                if el.is_mps_group() || el.is_unknown() {
                    continue;
                }
                if let Some(ref tf) = params.r#type {
                    if el.sign() != tf.as_str() {
                        continue;
                    }
                }
                let ej = api::element_to_json(el, epoch_ref, &date_str, &resolver);
                all_json.push(
                    serde_json::to_value(&ej).map_err(|e| api_err(500, e))?,
                );
            }
        }

        if fmt == "csv" {
            let csv_out = export_to_csv(&all_json).map_err(|e| api_err(500, e))?;
            Ok(("text/csv".into(), csv_out))
        } else {
            let json_str =
                serde_json::to_string(&all_json).map_err(|e| api_err(500, e))?;
            Ok(("application/json".into(), json_str))
        }
    })
    .await;

    match result {
        Ok(Ok((content_type, body))) => axum::response::Response::builder()
            .status(200)
            .header("Content-Type", content_type)
            .body(axum::body::Body::from(body))
            .unwrap(),
        Ok(Err((status, json))) => {
            (status, json).into_response()
        }
        Err(_) => api_err(500, "internal error").into_response(),
    }
}

fn export_to_csv(records: &[serde_json::Value]) -> Result<String, String> {
    let mut wtr = csv::Writer::from_writer(vec![]);
    let headers = ["ref", "human_ref", "date", "type", "tags", "body", "status", "at", "start", "end", "name"];
    wtr.write_record(&headers).map_err(|e| e.to_string())?;

    for rec in records {
        let get = |key: &str| -> String {
            match rec.get(key) {
                Some(serde_json::Value::String(s)) => s.clone(),
                Some(serde_json::Value::Array(a)) => {
                    a.iter()
                        .filter_map(|v| v.as_str())
                        .collect::<Vec<_>>()
                        .join(", ")
                }
                _ => String::new(),
            }
        };
        wtr.write_record(&[
            get("ref"),
            get("human_ref"),
            get("date"),
            get("type"),
            get("tags"),
            get("body"),
            get("status"),
            get("at"),
            get("start"),
            get("end"),
            get("name"),
        ])
        .map_err(|e| e.to_string())?;
    }

    wtr.into_inner()
        .map(|v| String::from_utf8(v).unwrap_or_default())
        .map_err(|e| e.to_string())
}

async fn tags_handler(
    State(state): State<Arc<AppState>>,
    Query(params): Query<TagsParams>,
) -> ApiResult<Json<HashMap<String, usize>>> {
    let config = state.config.clone();

    tokio::task::spawn_blocking(move || {
        let date = match &params.date {
            Some(d) => parse_date_param(d)?,
            None => today(),
        };

        let store = Store::new(&config.storage_dir);
        let files = if params.all {
            store.all_files().map_err(mps_err)?
        } else {
            store.find_files(date)
        };

        let mut freq: HashMap<String, usize> = HashMap::new();
        for file in &files {
            let elements = crate::parser::parse_file(file).map_err(mps_err)?;
            for (_, el) in &elements {
                if el.is_mps_group() || el.is_unknown() {
                    continue;
                }
                if let Some(ref tf) = params.r#type {
                    if el.sign() != tf.as_str() {
                        continue;
                    }
                }
                for tag in el.tags() {
                    *freq.entry(tag.clone()).or_insert(0) += 1;
                }
            }
        }
        Ok(Json(freq))
    })
    .await
    .map_err(|_| api_err(500, "internal error"))?
}
