//! Huskies server — entry point, CLI argument parsing, and server startup.

// matrix-sdk-crypto's deeply nested types require a higher recursion limit
// when the `e2e-encryption` feature is enabled.
#![recursion_limit = "256"]
// Prevent panics from direct &str indexing that could split multi-byte UTF-8 codepoints.
#![deny(clippy::string_slice)]

mod agent_log;
mod agent_mode;
mod agents;
mod chat;
#[cfg(test)]
mod ci_publish_artifact;
mod config;
/// CRDT snapshot — serialisation and restore of the full pipeline CRDT state.
pub mod crdt_snapshot;
/// CRDT state — in-memory pipeline state machine backed by a distributed CRDT.
pub mod crdt_state;
/// CRDT sync — WebSocket-based peer synchronisation for distributed nodes.
pub mod crdt_sync;
/// CRDT wire format — on-wire message types for the crdt-sync protocol.
pub mod crdt_wire;
mod db;
/// Event log — CRDT-persisted append-only log of every pipeline stage transition.
pub(crate) mod event_log;
/// Gateway mode — multi-project reverse proxy that fronts multiple project containers.
pub mod gateway;
mod gateway_relay;
mod http;
mod io;
mod llm;
/// LLM session management — CRDT-backed context assembly for bot prompts.
pub(crate) mod llm_session;
/// Log buffer — in-memory ring buffer for recent server-side log lines.
pub mod log_buffer;
/// Mesh — peer discovery and multi-hop CRDT replication over WebSocket.
pub mod mesh;
/// Node identity — Ed25519 keypair generation and stable node ID management.
pub mod node_identity;
/// Gateway pidfile — exclusive flock on `$HOME/.huskies/gateway.pid`.
pub mod pidfile;
/// Pipeline event bus — real-time broadcast of pipeline-transition events to persona subscribers.
pub(crate) mod pipeline_event_bus;
pub(crate) mod pipeline_state;
/// Reliable process-termination primitives shared across the server.
pub mod process_kill;
/// Rebuild — process restart and shutdown coordination.
pub mod rebuild;
mod service;
/// Services — shared service bundle injected into HTTP handlers and bot tasks.
pub mod services;
/// Sled uplink — background task that forwards permission requests to an upstream gateway.
pub mod sled_uplink;
mod startup;
mod state;
mod store;
/// Detached trampoline — kills the running gateway and starts the new binary.
pub mod trampoline;
/// In-container binary self-update — fetch, atomic replace, and re-exec.
pub mod upgrade;
/// Validated input layer — transport-agnostic newtypes and request structs for all MCP write tools.
pub mod validation;
mod workflow;
mod worktree;

use crate::agents::AgentPool;
use crate::http::build_routes;
use crate::http::context::AppContext;
use crate::http::{remove_port_file, resolve_port, write_port_file};
use crate::rebuild::ShutdownReason;
use crate::state::SessionState;
use crate::store::JsonFileStore;
use crate::workflow::WorkflowState;
use poem::Server;
use poem::listener::TcpListener;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::broadcast;

mod cli;

use cli::{parse_cli_args, resolve_path_arg};

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    // Reap zombie grandchildren on Unix (for native deployments without tini/init).
    // Docker containers with `init: true` in docker-compose.yml already have tini
    // as PID 1 for this. For native macOS/Linux, poll waitpid(-1, WNOHANG) in a
    // background thread so orphaned grandchildren don't accumulate as zombies.
    #[cfg(unix)]
    std::thread::spawn(|| {
        loop {
            // SAFETY: waitpid(-1, ...) with WNOHANG is always safe to call.
            unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
            std::thread::sleep(std::time::Duration::from_secs(5));
        }
    });

    // Log version and build hash so we can verify what's running.
    slog!(
        "[startup] huskies v{} (build {})",
        env!("CARGO_PKG_VERSION"),
        option_env!("BUILD_GIT_HASH").unwrap_or("unknown")
    );

    let app_state = Arc::new(SessionState::default());
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    // Migrate legacy root-level store.json into .huskies/ if the new path does
    // not yet exist.
    let legacy_store_path = cwd.join("store.json");
    let store_path = cwd.join(".huskies").join("store.json");
    if legacy_store_path.exists() && !store_path.exists() {
        if let Some(parent) = store_path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        let _ = std::fs::rename(&legacy_store_path, &store_path);
    }
    let store = Arc::new(JsonFileStore::from_path(store_path).map_err(std::io::Error::other)?);

    // Shared workflow state — same instance is reused for HTTP handlers below.
    let workflow = Arc::new(std::sync::Mutex::new(WorkflowState::default()));
    crate::crdt_sync::init_rpc_context(app_state.clone(), store.clone(), Arc::clone(&workflow));

    // Collect CLI args, skipping the binary name (argv[0]).
    let raw_args: Vec<String> = std::env::args().skip(1).collect();

    let cli = match parse_cli_args(&raw_args) {
        Ok(args) => args,
        Err(msg) => {
            eprintln!("error: {msg}");
            eprintln!("Run 'huskies --help' for usage.");
            std::process::exit(1);
        }
    };

    let is_init = cli.init;
    let is_agent = cli.agent;
    let is_gateway = cli.gateway;
    let agent_rendezvous = cli.rendezvous.clone();
    let explicit_path = resolve_path_arg(cli.path.as_deref(), &cwd);

    // Port resolution: CLI flag > project.toml (loaded later) > default.
    let port = cli.port.unwrap_or_else(resolve_port);

    // When a path is given explicitly on the CLI, it must already exist as a directory.
    if let Some(ref path) = explicit_path {
        if !path.exists() {
            eprintln!("error: path does not exist: {}", path.display());
            std::process::exit(1);
        }
        if !path.is_dir() {
            eprintln!("error: path is not a directory: {}", path.display());
            std::process::exit(1);
        }
    }

    // ── Trampoline mode: kill old gateway, start new one ─────────────────────
    if let Some(ref job_path) = cli.trampoline {
        trampoline::run_trampoline(std::path::Path::new(job_path)).await;
    }

    // ── Gateway mode: multi-project proxy ────────────────────────────────────
    if is_gateway {
        let config_dir = explicit_path.unwrap_or_else(|| cwd.clone());
        let config_path = config_dir.join("projects.toml");
        return gateway::run(&config_path, port).await;
    }

    startup::project::open_project_root(is_init, explicit_path, &cwd, &app_state, &store, port)
        .await;

    startup::project::init_subsystems(&app_state, &cwd, is_agent).await;

    let crdt_join_token = cli
        .join_token
        .clone()
        .or_else(|| std::env::var("HUSKIES_JOIN_TOKEN").ok());

    startup::project::configure_crdt_sync(
        &app_state,
        is_agent,
        agent_rendezvous.clone(),
        crdt_join_token,
    );

    // ── Agent mode: headless build agent ─────────────────────────────────────
    if is_agent {
        let agent_root = app_state.project_root.lock().unwrap().clone();
        let rendezvous = agent_rendezvous.expect("agent mode requires --rendezvous");
        let join_token = cli
            .join_token
            .clone()
            .or_else(|| std::env::var("HUSKIES_JOIN_TOKEN").ok());
        let agent_gateway_url = cli
            .gateway_url
            .clone()
            .or_else(|| std::env::var("HUSKIES_GATEWAY_URL").ok());
        return agent_mode::run(agent_root, rendezvous, port, join_token, agent_gateway_url).await;
    }

    // Event bus: broadcast channel for pipeline lifecycle events.
    let (watcher_tx, _) = broadcast::channel::<io::watcher::WatcherEvent>(1024);
    let agents = Arc::new(AgentPool::new(port, watcher_tx.clone()));
    crate::crdt_sync::init_rpc_agents(Arc::clone(&agents));

    // Filesystem watcher: watches config files for hot-reload.
    if let Some(ref root) = *app_state.project_root.lock().unwrap() {
        io::watcher::start_watcher(root.clone(), watcher_tx.clone());
    }

    // Spawn CRDT→watcher bridge and auto-assign subscriber.
    startup::tick_loop::spawn_event_bridges(
        watcher_tx.clone(),
        app_state.project_root.lock().unwrap().clone(),
        Arc::clone(&agents),
    );

    // Reconciliation progress channel and permission channel.
    let (reconciliation_tx, _) = broadcast::channel::<agents::ReconciliationEvent>(64);
    let (perm_tx, perm_rx) = tokio::sync::mpsc::unbounded_channel();

    let watcher_tx_for_bot = watcher_tx.clone();
    let watcher_rx_for_whatsapp = watcher_tx.subscribe();
    let watcher_rx_for_slack = watcher_tx.subscribe();
    let watcher_rx_for_discord = watcher_tx.subscribe();

    let permission_registry = service::permission_router::ResponderRegistry::new();
    service::permission_router::spawn_permission_router(perm_rx, Arc::clone(&permission_registry));
    let startup_root: Option<PathBuf> = app_state.project_root.lock().unwrap().clone();
    let startup_agents = Arc::clone(&agents);
    let startup_reconciliation_tx = reconciliation_tx.clone();
    let agents_for_shutdown = Arc::clone(&agents);

    // ── Construct the shared Services bundle ──────────────────────────────────
    let bot_cfg = startup_root
        .as_ref()
        .and_then(|root| chat::transport::matrix::BotConfig::load(root));
    let services = Arc::new(services::Services {
        project_root: startup_root.clone().unwrap_or_default(),
        agents: Arc::clone(&agents),
        bot_name: bot_cfg
            .as_ref()
            .and_then(|c| c.display_name.clone())
            .unwrap_or_else(|| "Assistant".to_string()),
        bot_user_id: String::new(),
        ambient_rooms: Arc::new(std::sync::Mutex::new(
            bot_cfg
                .as_ref()
                .map(|c| c.ambient_rooms.iter().cloned().collect())
                .unwrap_or_default(),
        )),
        permission_registry: Arc::clone(&permission_registry),
        pending_perm_replies: service::permission_router::PendingPermReplies::new(),
        permission_timeout_secs: bot_cfg
            .as_ref()
            .map(|c| c.permission_timeout_secs)
            .unwrap_or(120),
        status: agents.status_broadcaster(),
        chat_dispatcher: std::sync::Arc::new(chat::dispatcher::ChatDispatcher::new(
            bot_cfg
                .as_ref()
                .map(|c| c.coalesce_window_ms)
                .unwrap_or(1_500),
        )),
    });

    // Register the bot's persona in the CRDT so all transports share a single
    // event-log high-water mark keyed by name rather than transport ids.
    // scope="all" gives the gateway persona a cross-sled view of pipeline events.
    crate::crdt_state::write_llm_session(&services.bot_name.to_lowercase(), "all");

    // Sled uplink: forward permission requests to an upstream gateway when configured.
    // Project name for the identity frame (story 899 AC 5) and for the
    // derived uplink URL below (story 1179 AC 4). Env-var override wins;
    // otherwise derive from the project_root basename.
    let project_name = std::env::var("HUSKIES_PROJECT_NAME").unwrap_or_else(|_| {
        services
            .project_root
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown")
            .to_string()
    });
    // Explicit `--upstream-gateway`/`HUSKIES_UPSTREAM_GATEWAY` (a full WS URL,
    // possibly with a legacy `?token=`) takes precedence; otherwise derive the
    // uplink URL from `HUSKIES_GATEWAY_URL` (story 1179 AC 4).
    let upstream_gateway = cli
        .upstream_gateway
        .clone()
        .or_else(|| std::env::var("HUSKIES_UPSTREAM_GATEWAY").ok())
        .or_else(|| {
            std::env::var("HUSKIES_GATEWAY_URL")
                .ok()
                .map(|gateway_url| sled_uplink::build_uplink_url(&gateway_url, &project_name))
        })
        .unwrap_or_default();
    // Shared-secret sent as `Authorization: Bearer <token>` on the uplink
    // upgrade request (story 1179 AC 1).
    let sled_auth_token = std::env::var("HUSKIES_SLED_TOKEN").ok();
    let local_mcp_url = format!("http://127.0.0.1:{port}/mcp");
    sled_uplink::spawn_uplink_task(
        sled_uplink::UplinkConfig {
            upstream_url: upstream_gateway,
            project_name,
            local_mcp_url,
            auth_token: sled_auth_token,
        },
        Arc::clone(&services),
    );

    // ── Build bot contexts (WhatsApp / Slack / Discord) ───────────────────────
    let (bot_ctxs, matrix_shutdown_rx) =
        startup::bots::build_bot_contexts(&startup_root, &services);
    startup::bots::spawn_startup_announcements(&bot_ctxs);

    let matrix_shutdown_tx_for_rebuild = Arc::clone(&bot_ctxs.matrix_shutdown_tx);

    // Shared rate-limit retry timer store.
    let timer_store = {
        let pool = crate::db::get_shared_pool().expect("db pool must be initialised before stores");
        std::sync::Arc::new(
            crate::service::timer::TimerStore::from_pool(pool.clone())
                .await
                .expect("failed to load timer store from db"),
        )
    };
    let timer_store_for_tick = Arc::clone(&timer_store);
    let timer_store_for_bot = Arc::clone(&timer_store);

    // Event-based pipeline trigger store.
    let event_trigger_store = {
        let pool = crate::db::get_shared_pool().expect("db pool must be initialised before stores");
        std::sync::Arc::new(
            crate::service::event_triggers::store::EventTriggerStore::from_pool(pool.clone())
                .await
                .expect("failed to load event trigger store from db"),
        )
    };
    let event_trigger_store_for_subscriber = Arc::clone(&event_trigger_store);

    // Generic scheduled-timer store for the `schedule_timer` MCP tool.
    let scheduled_timer_store = {
        let pool = crate::db::get_shared_pool().expect("db pool must be initialised before stores");
        std::sync::Arc::new(
            crate::service::timer::ScheduledTimerStore::from_pool(pool.clone())
                .await
                .expect("failed to load scheduled timer store from db"),
        )
    };
    let scheduled_timer_store_for_tick = Arc::clone(&scheduled_timer_store);

    let ctx = AppContext {
        state: app_state,
        store,
        workflow,
        services: Arc::clone(&services),
        watcher_tx,
        reconciliation_tx,
        perm_tx,
        qa_app_process: Arc::new(std::sync::Mutex::new(None)),
        bot_shutdown: bot_ctxs.shutdown_notifier.clone(),
        matrix_shutdown_tx: Some(Arc::clone(&bot_ctxs.matrix_shutdown_tx)),
        timer_store,
        scheduled_timer_store,
        event_trigger_store,
    };

    // Gateway relay task (pushes StatusEvents to a configured gateway).
    startup::tick_loop::spawn_gateway_relay(&startup_root, Arc::clone(&services.status));

    // Clone ctx before it is consumed by build_routes; AppContext is cheap to clone (all Arcs).
    let ctx_for_triggers = ctx.clone();

    let app = build_routes(
        ctx.clone(),
        bot_ctxs.whatsapp_ctx.clone(),
        bot_ctxs.slack_ctx.clone(),
        port,
    );

    // Permanent liveness heartbeat — stops when the tokio runtime freezes,
    // giving a precise timestamp of the freeze from the last [liveness] line.
    let liveness_tick_secs = startup_root
        .as_ref()
        .and_then(|r| config::ProjectConfig::load(r).ok())
        .map(|c| c.watcher.liveness_tick_secs)
        .unwrap_or(5);
    startup::tick_loop::spawn_liveness_tick(liveness_tick_secs);

    // Unified 1-second background tick loop.
    startup::tick_loop::spawn_tick_loop(
        Arc::clone(&startup_agents),
        timer_store_for_tick,
        scheduled_timer_store_for_tick,
        startup_root.clone(),
        ctx,
    );

    // Optional Matrix bot.
    if let Some(ref root) = startup_root {
        let _ = chat::transport::matrix::spawn_bot(
            root,
            watcher_tx_for_bot,
            Arc::clone(&services),
            matrix_shutdown_rx,
            None,
            None,
            timer_store_for_bot,
            None,
            None,
            None,
        );
    } else {
        drop(matrix_shutdown_rx);
    }

    // Notification listeners for WhatsApp, Slack, Discord.
    startup::bots::spawn_notification_listeners(
        &bot_ctxs,
        &startup_root,
        watcher_rx_for_whatsapp,
        watcher_rx_for_slack,
        watcher_rx_for_discord,
    );

    // Event trigger subscriber: listens on TransitionFired and executes matching triggers.
    startup::tick_loop::spawn_event_trigger_subscriber(
        event_trigger_store_for_subscriber,
        Arc::clone(&startup_agents),
        startup_root.clone(),
        ctx_for_triggers,
    );

    // Reconcile completed worktrees and auto-assign free agents.
    startup::tick_loop::spawn_startup_reconciliation(
        startup_root.clone(),
        startup_agents,
        startup_reconciliation_tx,
    );

    let host = std::env::var("HUSKIES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
    let addr = format!("{host}:{port}");

    println!("\x1b[97;1m");
    println!("       /\\_/\\       \x1b[96;1m _   _           _    _           \x1b[97;1m");
    println!("      / o o \\      \x1b[96;1m| | | |_   _ ___| | _(_) ___  ___\x1b[97;1m");
    println!("     (   Y   )     \x1b[96;1m| |_| | | | / __| |/ / |/ _ \\/ __|\x1b[97;1m");
    println!("      \\  ^  /      \x1b[96;1m|  _  | |_| \\__ \\   <| |  __/\\__ \\\x1b[97;1m");
    println!("       )===( \\     \x1b[96;1m|_| |_|\\__,_|___/_|\\_\\_|\\___||___/\x1b[97;1m");
    println!("      /     \\ \\    \x1b[90mStory-driven development, powered by AI\x1b[97;1m");
    println!("      |  |  | |");
    println!("     /|  |  |\\|");
    println!("     \\|__|__|/\x1b[0m");
    println!();
    println!("HUSKIES_PORT={port}");
    println!("\x1b[96;1mFrontend:\x1b[0m \x1b[94mhttp://{addr}\x1b[0m");
    println!("\x1b[92;1mOpenAPI Docs:\x1b[0m \x1b[94mhttp://{addr}/docs\x1b[0m");

    let port_file = write_port_file(&cwd, port);

    let result = Server::new(TcpListener::bind(&addr)).run(app).await;

    // ── Shutdown notifications (best-effort) ──────────────────────────────────
    startup::bots::notify_shutdown(&bot_ctxs).await;

    // Matrix: signal the bot task and give it a short window to send its message.
    let _ = matrix_shutdown_tx_for_rebuild.send(Some(ShutdownReason::Manual));
    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;

    // Kill all active PTY child processes before exiting.
    agents_for_shutdown.kill_all_children().await;

    if let Some(ref path) = port_file {
        remove_port_file(path);
    }

    result
}

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

    #[test]
    #[should_panic(expected = "Invalid project.toml: Duplicate agent name")]
    fn panics_on_duplicate_agent_names() {
        let tmp = tempfile::tempdir().unwrap();
        let sk = tmp.path().join(".huskies");
        std::fs::create_dir_all(&sk).unwrap();
        std::fs::write(
            sk.join("project.toml"),
            r#"
[[agent]]
name = "coder"

[[agent]]
name = "coder"
"#,
        )
        .unwrap();

        config::ProjectConfig::load(tmp.path())
            .unwrap_or_else(|e| panic!("Invalid project.toml: {e}"));
    }
}
