mod db;
pub(crate) mod routes;

use axum::{
    body::Body,
    http::StatusCode,
    middleware,
    response::{Html, IntoResponse, Response},
    routing::get,
    Json, Router,
};
use tokio::sync::broadcast;
use tower_http::cors::{Any, CorsLayer};
use tower_http::services::ServeDir;
use tower_http::trace::{self, TraceLayer};
use tracing::Level;
use tracing_subscriber;
use uuid::Uuid;

use db::Database;
use theseus_core::*;

#[derive(serde::Deserialize, Clone)]
pub(crate) struct Config {
    pub(crate) deepseek_api_key: Option<String>,
}

fn load_config(cli_key: Option<String>) -> Config {
    // CLI flag overrides everything
    if let Some(key) = cli_key {
        return Config {
            deepseek_api_key: Some(key),
        };
    }

    // Search config files in priority order: ./theseus.toml > ~/.config/theseus/config.toml
    let candidates = [std::path::PathBuf::from("theseus.toml"), dirs_config_path()];
    for path in &candidates {
        if path.exists() {
            let content = std::fs::read_to_string(path).unwrap_or_default();
            if let Ok(config) = toml::from_str(&content) {
                return config;
            }
        }
    }

    Config {
        deepseek_api_key: None,
    }
}

fn dirs_config_path() -> std::path::PathBuf {
    // XDG_CONFIG_HOME or ~/.config/theseus/config.toml
    let base = std::env::var("XDG_CONFIG_HOME")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| dirs_home().join(".config"));
    base.join("theseus").join("config.toml")
}

fn dirs_home() -> std::path::PathBuf {
    std::env::var("HOME")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from("/"))
}

#[derive(Clone)]
pub(crate) struct AppState {
    pub(crate) db: &'static Database,
    pub(crate) tx: broadcast::Sender<String>,
    pub(crate) config: Config,
}

async fn json_error_middleware(
    request: axum::extract::Request,
    next: middleware::Next,
) -> Response {
    let response = next.run(request).await;
    let status = response.status();
    if status.is_server_error() || status.is_client_error() {
        let (parts, body) = response.into_parts();
        let bytes = axum::body::to_bytes(body, 1024 * 1024)
            .await
            .unwrap_or_default();
        if bytes.is_empty() {
            let msg = format!(
                "HTTP {} {}",
                status.as_u16(),
                status.canonical_reason().unwrap_or("Unknown")
            );
            let json = Json(ApiResponse::<()>::err(msg));
            return (status, json).into_response();
        }
        return Response::from_parts(parts, Body::from(bytes));
    }
    response
}

pub(crate) fn parse_uuid(s: &str) -> Result<uuid::Uuid, StatusCode> {
    Uuid::parse_str(s).map_err(|_| StatusCode::BAD_REQUEST)
}

/// Call DeepSeek chat API. Returns response content or an HTTP error with a meaningful message.
pub(crate) async fn deepseek_chat(
    api_key: &str,
    system_prompt: &str,
    user_content: &str,
    temperature: f64,
    max_tokens: u32,
    json_mode: bool,
) -> Result<String, StatusCode> {
    let mut body = serde_json::json!({
        "model": "deepseek-v4-flash",
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_content}
        ],
        "temperature": temperature,
        "max_tokens": max_tokens
    });
    if json_mode {
        body.as_object_mut().unwrap().insert(
            "response_format".to_string(),
            serde_json::json!({"type": "json_object"}),
        );
    }

    let client = reqwest::Client::new();
    let res = client
        .post("https://api.deepseek.com/chat/completions")
        .header("Authorization", format!("Bearer {}", api_key))
        .json(&body)
        .send()
        .await
        .map_err(|e| {
            tracing::error!("DeepSeek API request failed: {}", e);
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

    let status = res.status();
    if !status.is_success() {
        let body_text = res.text().await.unwrap_or_default();
        tracing::error!("DeepSeek API error {}: {}", status, body_text);
        return Err(match status.as_u16() {
            401 => StatusCode::INTERNAL_SERVER_ERROR, // bad API key — don't expose to client
            402 => StatusCode::INTERNAL_SERVER_ERROR, // insufficient balance
            429 => StatusCode::INTERNAL_SERVER_ERROR, // rate limited
            _ => StatusCode::INTERNAL_SERVER_ERROR,
        });
    }

    let body: serde_json::Value = res.json().await.map_err(|e| {
        tracing::error!("DeepSeek API response parse error: {}", e);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    body["choices"][0]["message"]["content"]
        .as_str()
        .map(String::from)
        .ok_or_else(|| {
            tracing::error!("DeepSeek API response missing content");
            StatusCode::INTERNAL_SERVER_ERROR
        })
}

/// Resolve a project ID — accepts full UUID or a prefix (≥6 chars).
pub(crate) async fn resolve_project_id(db: &Database, raw: &str) -> Result<uuid::Uuid, StatusCode> {
    // Try full UUID first
    if let Ok(id) = Uuid::parse_str(raw) {
        return Ok(id);
    }
    // Short prefix matching
    if raw.len() < 6 {
        return Err(StatusCode::BAD_REQUEST);
    }
    let projects = db
        .list_projects()
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    let matches: Vec<&Project> = projects
        .iter()
        .filter(|p| p.id.to_string().starts_with(raw))
        .collect();
    match matches.len() {
        0 => Err(StatusCode::NOT_FOUND),
        1 => Ok(matches[0].id),
        _ => Err(StatusCode::BAD_REQUEST),
    }
}

/// Resolve an edge ID within a project — accepts full UUID or a prefix (≥6 chars).
pub(crate) fn resolve_edge_id(
    db: &Database,
    project_id: Uuid,
    raw: &str,
) -> Result<uuid::Uuid, StatusCode> {
    // Try full UUID first
    if let Ok(id) = Uuid::parse_str(raw) {
        return Ok(id);
    }
    // Short prefix matching
    if raw.len() < 6 {
        return Err(StatusCode::BAD_REQUEST);
    }
    let edges = db
        .list_edges(project_id)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    let matches: Vec<&Edge> = edges
        .iter()
        .filter(|e| e.id.to_string().starts_with(raw))
        .collect();
    match matches.len() {
        0 => Err(StatusCode::NOT_FOUND),
        1 => Ok(matches[0].id),
        _ => Err(StatusCode::BAD_REQUEST),
    }
}

/// Resolve a node ID within a project — accepts full UUID or a prefix (≥6 chars).
pub(crate) fn resolve_node_id(
    db: &Database,
    project_id: Uuid,
    raw: &str,
) -> Result<uuid::Uuid, StatusCode> {
    // Try full UUID first
    if let Ok(id) = Uuid::parse_str(raw) {
        return Ok(id);
    }
    // Short prefix matching
    if raw.len() < 6 {
        return Err(StatusCode::BAD_REQUEST);
    }
    let nodes = db
        .list_nodes(project_id)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    let matches: Vec<&Node> = nodes
        .iter()
        .filter(|n| n.id.to_string().starts_with(raw))
        .collect();
    match matches.len() {
        0 => Err(StatusCode::NOT_FOUND),
        1 => Ok(matches[0].id),
        _ => Err(StatusCode::BAD_REQUEST),
    }
}

pub async fn serve(port: u16, api_key: Option<String>) {
    tracing_subscriber::fmt()
        .with_env_filter("theseus_server=debug,tower_http=debug")
        .try_init()
        .ok();

    // Open (or create) SQLite database in project root
    let db_path = concat!(env!("CARGO_MANIFEST_DIR"), "/../theseus.db");
    let db = Box::leak(Box::new(
        Database::open(db_path).expect("Failed to open database"),
    ));

    let (tx, _rx) = broadcast::channel(256);
    let config = load_config(api_key);
    if config.deepseek_api_key.is_some() {
        tracing::info!("AI features enabled (DeepSeek API key configured)");
    } else {
        tracing::info!("AI features disabled (no API key — use --api-key, THESEUS_DEEPSEEK_API_KEY, or theseus.toml)");
    }
    let state = AppState {
        db,
        tx: tx.clone(),
        config,
    };

    let cors = CorsLayer::new()
        .allow_origin(Any)
        .allow_methods(Any)
        .allow_headers(Any);

    let app = Router::new()
        .route("/", get(index))
        .merge(routes::ws::router())
        .merge(routes::projects::router())
        .merge(routes::nodes::router())
        .merge(routes::edges::router())
        .merge(routes::credentials::router())
        .merge(routes::timeline::router())
        .merge(routes::search::router())
        .merge(routes::evidence::router())
        .merge(routes::import::router())
        .merge(routes::analysis::router())
        .merge(routes::check::router())
        .merge(routes::report::router())
        .merge(routes::chat::router())
        .merge(routes::daily_report::router())
        .nest_service(
            "/static",
            ServeDir::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")),
        )
        .layer(axum::middleware::from_fn(json_error_middleware))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
        )
        .layer(cors)
        .with_state(state);

    let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{port}"))
        .await
        .unwrap();
    tracing::info!("Theseus server listening on http://0.0.0.0:{port}");
    axum::serve(listener, app).await.unwrap();
}

// ── index ──

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