//! write.famzheng.me — voice/text → claude → markdown doc.
//!
//! Auth: `Authorization: token <PASSPHRASE>` header. WS endpoints also accept
//! `?token=...` query (since browser ws can't set headers).
//! Config: env vars (see write.service for the full list).

use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use axum::{
    extract::{Request, State},
    http::{header, StatusCode},
    middleware::{from_fn_with_state, Next},
    response::{IntoResponse, Response},
    routing::get,
    Router,
};
use rusqlite::Connection;
use tracing::warn;

mod asr;
mod docs;
mod state;
use state::AppState;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    cube_core::init_tracing();

    let docs_dir = PathBuf::from(
        std::env::var("WRITE_DOCS_DIR")
            .unwrap_or_else(|_| "/home/fam/.local/share/write/docs".into()),
    );
    let db_path = std::env::var("WRITE_DB_PATH")
        .unwrap_or_else(|_| "/home/fam/.local/share/write/app.db".into());
    let dist = std::env::var("WRITE_DIST_DIR")
        .unwrap_or_else(|_| "/home/fam/src/cube/apps/write/frontend/dist".into());

    let passphrase = std::env::var("WRITE_PASSPHRASE").unwrap_or_default();
    if passphrase.is_empty() {
        warn!("WRITE_PASSPHRASE not set — all /api/* and /asr will return 401");
    }

    let asr_upstream = std::env::var("WRITE_ASR_UPSTREAM")
        .unwrap_or_else(|_| "ws://cpc-i7:9000".into());
    let asr_language =
        std::env::var("WRITE_ASR_LANGUAGE").unwrap_or_else(|_| "Chinese".into());
    let asr_chunk_size_sec: f64 = std::env::var("WRITE_ASR_CHUNK_SIZE_SEC")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(1.0);

    let claude_bin = std::env::var("WRITE_CLAUDE_BIN")
        .unwrap_or_else(|_| "/home/fam/.local/bin/claude".into());
    let claude_model =
        std::env::var("WRITE_CLAUDE_MODEL").unwrap_or_else(|_| "sonnet".into());
    let claude_max_budget_usd: f64 = std::env::var("WRITE_CLAUDE_MAX_BUDGET_USD")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(2.0);

    std::fs::create_dir_all(&docs_dir).expect("mkdir docs_dir");
    if let Some(parent) = PathBuf::from(&db_path).parent() {
        std::fs::create_dir_all(parent).ok();
    }

    let conn = Connection::open(&db_path).expect("open sqlite");
    conn.execute_batch(
        "PRAGMA journal_mode=WAL;
         CREATE TABLE IF NOT EXISTS docs (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL DEFAULT '未命名文档',
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
         );",
    )
    .expect("init schema");

    tracing::info!(
        %db_path,
        docs = %docs_dir.display(),
        %asr_upstream,
        %claude_model,
        "write ready"
    );

    let state = AppState {
        db: Arc::new(Mutex::new(conn)),
        docs_dir,
        passphrase,
        asr_upstream,
        asr_language,
        asr_chunk_size_sec,
        claude_bin,
        claude_model,
        claude_max_budget_usd,
    };

    let api = docs::router().route_layer(from_fn_with_state(state.clone(), auth_header));
    let asr_router = Router::new()
        .route("/asr", get(asr::ws_handler))
        .route_layer(from_fn_with_state(state.clone(), auth_query_or_header));

    let app: Router = Router::new()
        .nest("/api", api)
        .merge(asr_router)
        .with_state(state)
        .merge(cube_core::base(&dist));

    let port: u16 = std::env::var("PORT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(31391);
    cube_core::serve(app, port).await
}

/// Standard `Authorization: token <PASSPHRASE>` middleware.
async fn auth_header(State(state): State<AppState>, req: Request, next: Next) -> Response {
    if state.passphrase.is_empty() {
        return (StatusCode::UNAUTHORIZED, "auth not configured").into_response();
    }
    let want = format!("token {}", state.passphrase);
    let got = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .unwrap_or("");
    if got != want {
        return (StatusCode::UNAUTHORIZED, "invalid token").into_response();
    }
    next.run(req).await
}

/// Same as `auth_header` but also accepts `?token=<PASSPHRASE>` query — needed
/// for WebSocket endpoints since the browser can't attach custom headers to ws.
async fn auth_query_or_header(
    State(state): State<AppState>,
    req: Request,
    next: Next,
) -> Response {
    if state.passphrase.is_empty() {
        return (StatusCode::UNAUTHORIZED, "auth not configured").into_response();
    }
    let want_header = format!("token {}", state.passphrase);
    let header_match = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .map(|got| got == want_header)
        .unwrap_or(false);
    let query_match = req.uri().query().map(|q| {
        q.split('&').any(|kv| {
            let mut parts = kv.splitn(2, '=');
            matches!(
                (parts.next(), parts.next()),
                (Some("token"), Some(v)) if v == state.passphrase
            )
        })
    }).unwrap_or(false);

    if !header_match && !query_match {
        return (StatusCode::UNAUTHORIZED, "invalid token").into_response();
    }
    next.run(req).await
}
