mod analysis;
mod battery;
mod config;
mod crypto_provider;
mod diag;
mod display;
mod error;
mod gps;
mod key_input;
mod notifications;
mod pcap;
mod qmdl_store;
mod server;
mod stats;
mod update;
mod webdav;

use std::net::SocketAddr;
use std::sync::Arc;

use crate::battery::run_battery_notification_worker;
use crate::config::{GpsMode, parse_args, parse_config};
use crate::diag::run_diag_read_thread;
use crate::error::RayhunterError;
use crate::gps::{get_gps, post_gps};
use crate::notifications::{NotificationService, run_notification_worker};
use crate::pcap::get_pcap;
use crate::qmdl_store::RecordingStore;
use crate::server::{
    ServerState, debug_set_display_state, get_config, get_qmdl, get_time, get_wifi_status, get_zip,
    scan_wifi, serve_static, set_config, set_time_offset, test_notification,
};
use crate::stats::{get_qmdl_manifest, get_system_stats, get_update_status};
use crate::update::{UpdateStatus, run_update_check_worker};
use crate::webdav::run_webdav_upload_worker;
use wifi_station::WifiStatus;

use analysis::{
    AnalysisCtrlMessage, AnalysisStatus, get_analysis_status, run_analysis_thread, start_analysis,
};
use axum::Router;
use axum::response::Redirect;
use axum::routing::{get, post};
use diag::{
    DiagDeviceCtrlMessage, delete_all_recordings, delete_recording, get_analysis_report,
    start_recording, stop_recording,
};
use log::{error, info, warn};
use qmdl_store::RecordingStoreError;
use rayhunter::Device;
use stats::get_log;
use tokio::net::TcpListener;
use tokio::select;
use tokio::sync::RwLock;
use tokio::sync::mpsc::{self, Sender};
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;

type AppRouter = Router<Arc<ServerState>>;

fn get_router() -> AppRouter {
    Router::new()
        .route("/api/pcap/{name}", get(get_pcap))
        .route("/api/qmdl/{name}", get(get_qmdl))
        .route("/api/zip/{name}", get(get_zip))
        .route("/api/system-stats", get(get_system_stats))
        .route("/api/update-status", get(get_update_status))
        .route("/api/qmdl-manifest", get(get_qmdl_manifest))
        .route("/api/log", get(get_log))
        .route("/api/start-recording", post(start_recording))
        .route("/api/stop-recording", post(stop_recording))
        .route("/api/delete-recording/{name}", post(delete_recording))
        .route("/api/delete-all-recordings", post(delete_all_recordings))
        .route("/api/analysis-report/{name}", get(get_analysis_report))
        .route("/api/analysis", get(get_analysis_status))
        .route("/api/analysis/{name}", post(start_analysis))
        .route("/api/config", get(get_config))
        .route("/api/config", post(set_config))
        .route("/api/test-notification", post(test_notification))
        .route("/api/wifi-status", get(get_wifi_status))
        .route("/api/wifi-scan", post(scan_wifi))
        .route("/api/time", get(get_time))
        .route("/api/time-offset", post(set_time_offset))
        .route("/api/debug/display-state", post(debug_set_display_state))
        .route("/api/gps", get(get_gps))
        .route("/api/gps", post(post_gps))
        .route("/", get(|| async { Redirect::permanent("/index.html") }))
        .route("/{*path}", get(serve_static))
}

// Runs the axum server, taking all the elements needed to build up our
// ServerState and a oneshot Receiver that'll fire when it's time to shutdown
// (i.e. user hit ctrl+c)
async fn run_server(
    task_tracker: &TaskTracker,
    state: Arc<ServerState>,
    shutdown_token: CancellationToken,
) -> JoinHandle<()> {
    info!("spinning up server");
    let addr = SocketAddr::from(([0, 0, 0, 0], state.config.port));
    let listener = TcpListener::bind(&addr).await.unwrap();
    let app = get_router().with_state(state);

    task_tracker.spawn(async move {
        info!("The orca is hunting for stingrays...");
        axum::serve(listener, app)
            .with_graceful_shutdown(shutdown_token.cancelled_owned())
            .await
            .unwrap();
    })
}

// Loads a RecordingStore if one exists, and if not, only create one if we're
// not in debug mode. If we fail to parse the manifest AND we're not in debug
// mode, try to recover the manifest from the existing QMDL files
async fn init_qmdl_store(config: &config::Config) -> Result<RecordingStore, RayhunterError> {
    let store_exists = RecordingStore::exists(&config.qmdl_store_path).await?;
    if config.debug_mode {
        if store_exists {
            Ok(RecordingStore::load(&config.qmdl_store_path).await?)
        } else {
            Err(RayhunterError::NoStoreDebugMode(
                config.qmdl_store_path.clone(),
            ))
        }
    } else if store_exists {
        match RecordingStore::load(&config.qmdl_store_path).await {
            Ok(store) => Ok(store),
            Err(RecordingStoreError::ParseManifestError(err)) => {
                error!("failed to parse QMDL manifest: {err}");
                info!("recovering manifest from existing QMDL files...");
                Ok(RecordingStore::recover(&config.qmdl_store_path).await?)
            }
            Err(err) => Err(err.into()),
        }
    } else {
        Ok(RecordingStore::create(&config.qmdl_store_path).await?)
    }
}

// Start a thread that'll track when user hits ctrl+c. When that happens,
// trigger various cleanup tasks, including sending signals to other threads to
// shutdown
fn run_shutdown_thread(
    task_tracker: &TaskTracker,
    diag_device_sender: Sender<DiagDeviceCtrlMessage>,
    shutdown_token: CancellationToken,
    qmdl_store_lock: Arc<RwLock<RecordingStore>>,
    analysis_tx: Sender<AnalysisCtrlMessage>,
) -> JoinHandle<Result<(), RayhunterError>> {
    info!("create shutdown thread");

    task_tracker.spawn(async move {
        select! {
            res = tokio::signal::ctrl_c() => {
                if let Err(err) = res {
                    error!("Unable to listen for shutdown signal: {err}");
                }
            }
            _ = shutdown_token.cancelled() => {}
        }

        let mut qmdl_store = qmdl_store_lock.write().await;
        if qmdl_store.current_entry.is_some() {
            info!("Closing current QMDL entry...");
            qmdl_store.close_current_entry().await?;
            info!("Done!");
        }

        shutdown_token.cancel();
        diag_device_sender
            .send(DiagDeviceCtrlMessage::Exit)
            .await
            .expect("couldn't send Exit message to diag thread");
        analysis_tx
            .send(AnalysisCtrlMessage::Exit)
            .await
            .expect("couldn't send Exit message to analysis thread");
        Ok(())
    })
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), RayhunterError> {
    rayhunter::init_logging(log::LevelFilter::Info);

    crate::crypto_provider::install_default();

    let args = parse_args();

    loop {
        let config = parse_config(&args.config_path).await?;
        if !run_with_config(&args, config).await? {
            return Ok(());
        }
    }
}

async fn run_with_config(
    args: &config::Args,
    config: config::Config,
) -> Result<bool, RayhunterError> {
    // TaskTrackers give us an interface to spawn tokio threads, and then
    // eventually await all of them ending
    let task_tracker = TaskTracker::new();
    println!("R A Y H U N T E R 🐳");

    let store = init_qmdl_store(&config).await?;
    let analysis_status = AnalysisStatus::new(&store);
    let qmdl_store_lock = Arc::new(RwLock::new(store));
    let (diag_tx, diag_rx) = mpsc::channel::<DiagDeviceCtrlMessage>(1);
    let (ui_update_tx, ui_update_rx) = mpsc::channel::<display::DisplayState>(1);
    let (analysis_tx, analysis_rx) = mpsc::channel::<AnalysisCtrlMessage>(5);
    let restart_token = CancellationToken::new();
    let shutdown_token = restart_token.child_token();
    // Ensure shutdown_token is cancelled when this function exits for any
    // reason (e.g. diag device init failure), so all spawned tasks get
    // signaled to stop.
    let _shutdown_guard = shutdown_token.clone().drop_guard();

    let notification_service = NotificationService::new(config.ntfy_url.clone());
    let update_status_lock = Arc::new(RwLock::new(UpdateStatus::default()));

    if !config.debug_mode {
        info!("Starting Diag Thread");
        let gps_fixed_coords = match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
            (Some(lat), Some(lon)) => Some((lat, lon)),
            _ => None,
        };
        run_diag_read_thread(
            &task_tracker,
            config.device.clone(),
            diag_rx,
            diag_tx.clone(),
            ui_update_tx.clone(),
            qmdl_store_lock.clone(),
            analysis_tx.clone(),
            config.analyzers.clone(),
            notification_service.new_handler(),
            config.min_space_to_start_recording_mb,
            config.min_space_to_continue_recording_mb,
            config.gps_mode,
            gps_fixed_coords,
        );
        info!("Starting UI");

        let update_ui = match &config.device {
            Device::Orbic | Device::Moxee => display::orbic::update_ui,
            Device::Tplink => display::tplink::update_ui,
            Device::Tmobile => display::tmobile::update_ui,
            Device::Wingtech => display::wingtech::update_ui,
            Device::Pinephone => display::headless::update_ui,
            Device::Uz801 => display::uz801::update_ui,
        };
        update_ui(&task_tracker, &config, shutdown_token.clone(), ui_update_rx);

        info!("Starting Key Input service");
        key_input::run_key_input_thread(
            &task_tracker,
            &config,
            diag_tx.clone(),
            shutdown_token.clone(),
        );

        if config.auto_check_updates {
            run_update_check_worker(
                &task_tracker,
                shutdown_token.clone(),
                update_status_lock.clone(),
                notification_service.new_handler(),
                config.enabled_notifications.clone(),
            );
        }
    }

    let analysis_status_lock = Arc::new(RwLock::new(analysis_status));
    run_analysis_thread(
        &task_tracker,
        analysis_rx,
        qmdl_store_lock.clone(),
        analysis_status_lock.clone(),
        config.analyzers.clone(),
    );

    run_shutdown_thread(
        &task_tracker,
        diag_tx.clone(),
        shutdown_token.clone(),
        qmdl_store_lock.clone(),
        analysis_tx.clone(),
    );

    if !config.debug_mode {
        run_battery_notification_worker(
            &task_tracker,
            config.device.clone(),
            notification_service.new_handler(),
            shutdown_token.clone(),
        );
    }

    run_notification_worker(
        &task_tracker,
        notification_service,
        config.enabled_notifications.clone(),
    );

    let wifi_status = Arc::new(RwLock::new(WifiStatus::default()));
    if !config.debug_mode {
        wifi_station::run_wifi_client(
            &task_tracker,
            &config.wifi_config(),
            shutdown_token.clone(),
            wifi_status.clone(),
        );
    }

    if !config.webdav.url.trim().is_empty() {
        run_webdav_upload_worker(
            &task_tracker,
            shutdown_token.clone(),
            qmdl_store_lock.clone(),
            config.webdav.clone().into(),
        );
    }
    let initial_gps = if config.gps_mode == GpsMode::Fixed {
        match (config.gps_fixed_latitude, config.gps_fixed_longitude) {
            (Some(lat), Some(lon)) => Some(gps::GpsData {
                latitude: lat,
                longitude: lon,
            }),
            _ => {
                warn!(
                    "gps_mode is Fixed but gps_fixed_latitude or gps_fixed_longitude is missing from config — no GPS coordinates will be recorded"
                );
                None
            }
        }
    } else {
        None
    };

    let state = Arc::new(ServerState {
        config_path: args.config_path.clone(),
        config,
        qmdl_store_lock: qmdl_store_lock.clone(),
        diag_device_ctrl_sender: diag_tx,
        analysis_status_lock,
        analysis_sender: analysis_tx,
        daemon_restart_token: restart_token.clone(),
        ui_update_sender: Some(ui_update_tx),
        wifi_status,
        wifi_scan_lock: tokio::sync::Mutex::new(()),
        gps_state: Arc::new(tokio::sync::RwLock::new(initial_gps)),
        update_status_lock: update_status_lock.clone(),
    });
    run_server(&task_tracker, state, shutdown_token.clone()).await;

    task_tracker.close();
    task_tracker.wait().await;

    info!("see you space cowboy...");
    Ok(restart_token.is_cancelled())
}

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

    #[test]
    fn test_get_router() {
        // assert that creating the router does not panic from invalid route patterns.
        let _ = get_router();
    }
}
