// Hide console window on Windows in release builds (production mode)
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;

use axum::{routing::get, Router};
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tracing::{info, warn};

pub mod config;
mod models;
mod routes;
mod services;
mod watcher;
mod websocket;

/// Find an available port and return the bound listener.
/// Avoids TOCTOU race by keeping the listener alive.
async fn find_available_port() -> (TcpListener, u16) {
    let host = std::env::var("IRONPAD_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
    let bind_ip = match host.parse::<IpAddr>() {
        Ok(ip) => ip,
        Err(_) => {
            warn!("Invalid IRONPAD_HOST '{}', falling back to 127.0.0.1", host);
            IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
        }
    };

    if let Ok(port_str) = std::env::var("IRONPAD_PORT") {
        match port_str.parse::<u16>() {
            Ok(port) => {
                let addr = SocketAddr::new(bind_ip, port);
                let listener = TcpListener::bind(addr)
                    .await
                    .unwrap_or_else(|e| panic!("Failed to bind to {addr}: {e}"));
                return (listener, port);
            }
            Err(_) => {
                warn!("Invalid IRONPAD_PORT '{}', falling back to 3000-3010", port_str);
            }
        }
    }

    for port in 3000..=3010 {
        let addr = SocketAddr::new(bind_ip, port);
        if let Ok(listener) = TcpListener::bind(addr).await {
            return (listener, port);
        }
    }
    panic!("No available ports in range 3000-3010");
}

fn env_flag(name: &str) -> bool {
    std::env::var(name)
        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
        .unwrap_or(false)
}

fn main() {
    tracing_subscriber::fmt().init();
    config::init_data_dir();

    let production = config::is_production();
    let disable_tray = env_flag("IRONPAD_DISABLE_TRAY");

    if production && !disable_tray {
        run_with_tray();
    } else {
        if production && disable_tray {
            info!("Production static mode detected; running headless (IRONPAD_DISABLE_TRAY=1)");
        }
        // Development mode: normal tokio runtime, no tray
        let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
        rt.block_on(run_server(None));
    }
}

/// Start the Axum server. In tray mode, sends the bound port through `port_tx`
/// before entering the serve loop.
async fn run_server(port_tx: Option<std::sync::mpsc::Sender<u16>>) {
    let (listener, port) = find_available_port().await;

    // Notify tray thread of the bound port
    if let Some(tx) = port_tx {
        let _ = tx.send(port);
    }

    // WebSocket state (shared across handlers)
    let ws_state = Arc::new(websocket::WsState::new());

    // Start file watcher
    let ws_state_clone = ws_state.clone();
    if let Err(e) = watcher::start_watcher(ws_state_clone).await {
        warn!("File watcher failed to start: {}", e);
    }

    // Initialize git repo if needed
    if let Err(e) = services::git::init_repo() {
        warn!("Git init skipped: {}", e);
    }

    // Start auto-commit background task (tries to commit every 60s)
    services::git::start_auto_commit();

    // CORS layer (permissive for local-only app)
    let cors = CorsLayer::permissive();

    // API router
    let api_router = Router::new()
        // Notes CRUD
        .route(
            "/notes",
            get(routes::notes::list_notes).post(routes::notes::create_note),
        )
        .nest("/notes", routes::notes::router())
        // Tasks
        .nest("/tasks", routes::tasks::router())
        // Search
        .nest("/search", routes::search::router())
        // Git
        .nest("/git", routes::git::router())
        // Projects
        .nest("/projects", routes::projects::router())
        // Daily notes
        .nest("/daily", routes::daily::router())
        // Assets
        .nest("/assets", routes::assets::router());

    // App router with WebSocket state
    let mut app = Router::new()
        .route("/health", get(|| async { "ok" }))
        .route(
            "/ws",
            get({
                let ws = ws_state.clone();
                move |upgrade: axum::extract::WebSocketUpgrade| {
                    websocket::ws_handler(upgrade, axum::extract::State(ws))
                }
            }),
        )
        .nest("/api", api_router)
        .layer(cors);

    // Check for embedded frontend (production mode)
    let has_frontend = config::is_production();

    if has_frontend {
        // Production mode: serve frontend from static/ next to the exe
        let static_path = config::exe_dir().join("static");
        let index_path = static_path.join("index.html");
        info!(
            "Production mode: serving frontend from {}",
            static_path.display()
        );
        let serve_dir =
            ServeDir::new(&static_path).fallback(tower_http::services::ServeFile::new(index_path));
        app = app.fallback_service(serve_dir);
    } else {
        // Development mode: API-only
        app = app.fallback(|| async {
            "Ironpad API server running. Use 'npm run dev' in frontend/ for the GUI."
        });
    }

    // Start server
    let bound_addr = listener
        .local_addr()
        .map(|a| a.to_string())
        .unwrap_or_else(|_| format!("127.0.0.1:{port}"));
    info!("Ironpad running on http://{bound_addr}");

    axum::serve(listener, app).await.expect("Server failed");
}

// ---------------------------------------------------------------------------
// System tray (production mode)
// ---------------------------------------------------------------------------

/// Build a platform-appropriate tray icon.
///
/// On Windows the Ironpad icon is embedded in the .exe via winresource (build.rs).
/// We load it with LoadIconW using the resource ID assigned by winresource.
#[cfg(target_os = "windows")]
fn tray_icon() -> tray_item::IconSource {
    let hicon = unsafe {
        // winresource embeds the icon at resource ID 1.
        // GetModuleHandleW(null) = current exe, MAKEINTRESOURCE(1) = 1 as PCWSTR.
        let hinstance =
            windows_sys::Win32::System::LibraryLoader::GetModuleHandleW(std::ptr::null());
        windows_sys::Win32::UI::WindowsAndMessaging::LoadIconW(hinstance, 1 as *const u16)
    };
    tray_item::IconSource::RawIcon(hicon)
}

#[cfg(target_os = "macos")]
fn tray_icon() -> tray_item::IconSource {
    tray_item::IconSource::Resource("")
}

#[cfg(target_os = "linux")]
fn tray_icon() -> tray_item::IconSource {
    tray_item::IconSource::Resource("application-x-executable")
}

#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
fn tray_icon() -> tray_item::IconSource {
    tray_item::IconSource::Resource("")
}

/// Production mode: run the server on a background thread, tray on main thread.
/// The main thread drives the tray event loop (required on macOS; safe everywhere).
fn run_with_tray() {
    use std::sync::mpsc;

    enum TrayMessage {
        OpenBrowser,
        Quit,
    }

    // Channel to receive the dynamically-bound port from the server thread
    let (port_tx, port_rx) = mpsc::channel::<u16>();

    // Start the Axum server on a background thread with its own tokio runtime
    std::thread::spawn(move || {
        let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
        rt.block_on(run_server(Some(port_tx)));
    });

    // Wait for the server to report its port
    let port = port_rx.recv().expect("Server failed to start");
    let url = format!("http://localhost:{}", port);

    // Auto-open browser after a short delay (non-blocking)
    let url_for_open = url.clone();
    std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_millis(400));
        let _ = webbrowser::open(&url_for_open);
    });

    // Set up system tray icon and menu
    let (tx, rx) = mpsc::sync_channel::<TrayMessage>(2);

    let mut tray = match tray_item::TrayItem::new("Ironpad", tray_icon()) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("Failed to create system tray: {}. Running headless.", e);
            // Keep the process alive so the server thread continues
            loop {
                std::thread::park();
            }
        }
    };

    let tx_open = tx.clone();
    let _ = tray.add_menu_item("Open in Browser", move || {
        let _ = tx_open.send(TrayMessage::OpenBrowser);
    });

    let tx_quit = tx;
    let _ = tray.add_menu_item("Quit", move || {
        let _ = tx_quit.send(TrayMessage::Quit);
    });

    // Main-thread event loop — processes tray menu actions
    for msg in rx {
        match msg {
            TrayMessage::OpenBrowser => {
                let _ = webbrowser::open(&url);
            }
            TrayMessage::Quit => {
                info!("Quit requested from system tray");
                std::process::exit(0);
            }
        }
    }
}
