// SPDX-FileCopyrightText: 2025 Stanislaw Grams <stanislawgrams@gmail.com>
//
// SPDX-License-Identifier: BSD-2-Clause

#[path = "api.rs"]
mod api;
#[path = "audio.rs"]
pub mod audio;
#[path = "auth.rs"]
pub mod auth;
#[path = "bookmarks.rs"]
pub mod bookmarks;
#[path = "status.rs"]
pub mod status;

use std::net::SocketAddr;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use std::time::Duration;

use actix_web::dev::Server;
use actix_web::{
    middleware::{DefaultHeaders, Logger},
    web, App, HttpServer,
};
use tokio::signal;
use tokio::sync::{mpsc, watch};
use tokio::task::JoinHandle;
use tracing::{error, info};

use trx_core::RigRequest;
use trx_core::RigState;
use trx_frontend::{FrontendRuntimeContext, FrontendSpawner};

use auth::{AuthConfig, AuthState, SameSite};

/// HTTP frontend implementation.
pub struct HttpFrontend;

impl FrontendSpawner for HttpFrontend {
    fn spawn_frontend(
        state_rx: watch::Receiver<RigState>,
        rig_tx: mpsc::Sender<RigRequest>,
        callsign: Option<String>,
        listen_addr: SocketAddr,
        context: Arc<FrontendRuntimeContext>,
    ) -> JoinHandle<()> {
        tokio::spawn(async move {
            if let Err(e) = serve(listen_addr, state_rx, rig_tx, callsign, context).await {
                error!("HTTP status server error: {:?}", e);
            }
        })
    }
}

async fn serve(
    addr: SocketAddr,
    state_rx: watch::Receiver<RigState>,
    rig_tx: mpsc::Sender<RigRequest>,
    callsign: Option<String>,
    context: Arc<FrontendRuntimeContext>,
) -> Result<(), actix_web::Error> {
    audio::start_decode_history_collector(context.clone());
    let server = build_server(addr, state_rx, rig_tx, callsign, context)?;
    let handle = server.handle();
    tokio::spawn(async move {
        let _ = signal::ctrl_c().await;
        handle.stop(false).await;
    });
    info!("http frontend listening on {}", addr);
    info!("http frontend ready (status/control)");
    server.await?;
    Ok(())
}

fn build_server(
    addr: SocketAddr,
    state_rx: watch::Receiver<RigState>,
    rig_tx: mpsc::Sender<RigRequest>,
    _callsign: Option<String>,
    context: Arc<FrontendRuntimeContext>,
) -> Result<Server, actix_web::Error> {
    let state_data = web::Data::new(state_rx);
    let rig_tx = web::Data::new(rig_tx);
    let clients = web::Data::new(Arc::new(AtomicUsize::new(0)));

    let bookmark_path = bookmarks::BookmarkStore::default_path();
    let bookmark_store = web::Data::new(Arc::new(bookmarks::BookmarkStore::open(&bookmark_path)));

    // Extract auth config values before moving context
    let same_site = match context.http_auth_cookie_same_site.as_str() {
        "Strict" => SameSite::Strict,
        "None" => SameSite::None,
        _ => SameSite::Lax, // default
    };
    let auth_config = AuthConfig::new(
        context.http_auth_enabled,
        context.http_auth_rx_passphrase.clone(),
        context.http_auth_control_passphrase.clone(),
        context.http_auth_tx_access_control_enabled,
        Duration::from_secs(context.http_auth_session_ttl_secs),
        context.http_auth_cookie_secure,
        same_site,
    );

    let context_data = web::Data::new(context);
    let auth_state = web::Data::new(AuthState::new(auth_config.clone()));

    // Spawn session cleanup task if auth is enabled
    if auth_config.enabled {
        let store_cleanup = auth_state.store.clone();
        tokio::spawn(async move {
            let mut interval = tokio::time::interval(Duration::from_secs(300)); // 5 minutes
            loop {
                interval.tick().await;
                store_cleanup.cleanup_expired();
            }
        });
    }

    let server = HttpServer::new(move || {
        App::new()
            .app_data(state_data.clone())
            .app_data(rig_tx.clone())
            .app_data(clients.clone())
            .app_data(context_data.clone())
            .app_data(auth_state.clone())
            .app_data(bookmark_store.clone())
            .wrap(
                DefaultHeaders::new()
                    .add(("Referrer-Policy", "same-origin"))
                    .add(("Cross-Origin-Resource-Policy", "same-origin"))
                    .add(("Cross-Origin-Opener-Policy", "same-origin"))
                    .add(("X-Content-Type-Options", "nosniff")),
            )
            // Use "real IP" so reverse-proxy setups can pass client address
            // via Forwarded / X-Forwarded-For / X-Real-IP headers.
            .wrap(Logger::new(
                r#"%{r}a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#,
            ))
            .wrap(auth::AuthMiddleware)
            .configure(api::configure)
    })
    .shutdown_timeout(1)
    .disable_signals()
    .bind(addr)?
    .run();
    Ok(server)
}

pub fn configure(cfg: &mut web::ServiceConfig) {
    cfg.configure(api::configure);
}
