mod agents_cleanup;
mod agents_mcp;
mod agents_remove;
mod agents_setup;
mod agents_tui;
mod auth;
mod cli;
mod config;
mod datadir_readme;
mod dkim;
mod doctor;
mod frontmatter;
mod hook;
mod hook_handler;
mod hook_list_handler;
mod hooks;
mod ingest;
mod logging;
mod logs;
mod mailbox;
mod mailbox_handler;
mod mailbox_list_handler;
mod mailbox_locks;
mod markdown_render;
mod mcp;
mod mx;
mod ownership;
mod platform;
mod portcheck;
mod send;
mod send_handler;
mod send_protocol;
mod serve;
mod setup;
mod slug;
mod smtp;
mod state_handler;
mod term;
mod transport;
mod trust;
mod uds_authz;
mod wire_assembly;
// `release` lands the release-metadata + ReleaseOps used by
// `aimx upgrade`. Per-item `#[allow(dead_code)]` attributes inside the
// module scope the lint narrowly so any future genuinely-unused item
// still surfaces.
mod release;
mod uninstall;
mod upgrade;
mod user_resolver;
mod version;
mod version_handler;

use clap::Parser;
use cli::{AgentsCommand, Cli, Command};

fn main() {
    // Handle `aimx --version` / `aimx -V` manually so the output is
    // exactly the version banner (`aimx <tag> (<sha>) <target> built <date>`)
    // without clap's binary-name prefix.
    if cli::handle_version_flag(std::env::args_os()) {
        return;
    }
    let cli = Cli::parse();
    if let Err(e) = dispatch(cli) {
        eprintln!("{} {e}", term::error("Error:"));
        std::process::exit(1);
    }
}

fn dispatch(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
    match cli.command {
        // Setup runs pre-install: config may not exist yet.
        Command::Setup {
            domain,
            verify_host,
        } => {
            let sys = setup::RealSystemOps;
            let net = build_network_ops(verify_host.as_deref())?;
            setup::run_setup(domain.as_deref(), cli.data_dir.as_deref(), &sys, &net)
        }
        // Uninstall also runs pre-config: config may be missing or unreadable.
        Command::Uninstall { yes } => {
            let sys = setup::RealSystemOps;
            uninstall::run(yes, &sys)
        }
        // Portcheck does not read config for storage, only `verify_host`.
        Command::Portcheck { verify_host } => portcheck::run(verify_host.as_deref()),
        // MCP tools route through the daemon UDS; no config or data-dir
        // override is read in-process.
        Command::Mcp => {
            let rt = tokio::runtime::Runtime::new()
                .map_err(|e| format!("Failed to create runtime: {e}"))?;
            rt.block_on(mcp::run())
        }
        // `aimx agents <command>`: the canonical plural-noun-with-verb
        // shape. Setup / Remove / List dispatch to their dedicated
        // modules.
        Command::Agents(cmd) => match cmd {
            AgentsCommand::Setup {
                agent,
                list,
                force,
                print,
                no_interactive,
                dangerously_allow_root,
            } => agents_setup::run(agents_setup::RunOpts {
                agent,
                list,
                force,
                print,
                no_interactive,
                dangerously_allow_root,
                data_dir: cli.data_dir.as_deref(),
            }),
            AgentsCommand::Remove {
                agent,
                dangerously_allow_root,
            } => agents_remove::run(agents_remove::RunOpts {
                agent,
                dangerously_allow_root,
            }),
            AgentsCommand::List => agents_setup::run(agents_setup::RunOpts {
                agent: None,
                list: true,
                force: false,
                print: false,
                no_interactive: true,
                dangerously_allow_root: false,
                data_dir: cli.data_dir.as_deref(),
            }),
        },
        // `aimx send` is a pure UDS client. It never reads config.toml.
        // The daemon parses the `From:` header itself and resolves the
        // sender mailbox against its in-memory Config.
        Command::Send(args) => send::run(args),
        // `aimx logs` is a thin wrapper around journalctl; it does not
        // read config.toml.
        Command::Logs { lines, follow } => logs::run(lines, follow),
        // `aimx mailboxes` is a thin UDS client (mirrors `aimx send`).
        // It must NOT pre-load config here — `/etc/aimx/config.toml`
        // is `0640 root:root` in production, so an eager
        // `Config::load_resolved_with_data_dir(...)?` would surface
        // `Permission denied (os error 13)` for non-root callers
        // before `mailbox::run` ever sees the request, breaking the
        // S2-1 acceptance criterion ("solo operator runs `aimx
        // mailboxes create my-agent` as themselves and it works").
        // `mailbox::run` loads the config itself when it can, and
        // each subcommand decides whether the missing-config case is
        // recoverable (CREATE/DELETE go through the daemon UDS;
        // LIST falls back to `MAILBOX-LIST`; SHOW errors with an
        // actionable hint).
        Command::Mailboxes(cmd) => mailbox::run(cmd, cli.data_dir.as_deref()),
        // `aimx upgrade` reads the release-manifest URL from Config
        // (optional `[upgrade] release_manifest_url`) but tolerates a
        // missing / unloadable config — a freshly-installed machine
        // that never ran `aimx setup` should still be able to upgrade.
        Command::Upgrade(args) => {
            let config = config::Config::load_resolved_ignore_warnings().ok();
            let manifest_url = config
                .as_ref()
                .and_then(|c| c.upgrade.as_ref())
                .and_then(|u| u.release_manifest_url.clone());
            let release = release::RealReleaseOps::new(manifest_url);
            let sys = setup::RealSystemOps;
            upgrade::run(args, &release, &sys)
        }
        // Everything else loads Config once here and takes it by value.
        // For long-lived processes (`aimx serve`, `aimx doctor`) we log
        // startup warnings via `tracing` so orphan mailboxes / templates
        // surface in journalctl. Short-lived CLI commands inherit the
        // same logging but only a tracing subscriber is installed by
        // `aimx serve` / `aimx doctor` themselves.
        other => {
            let (config, warnings) =
                config::Config::load_resolved_with_data_dir(cli.data_dir.as_deref())?;
            emit_load_warnings(&warnings);
            dispatch_with_config(other, config)
        }
    }
}

fn emit_load_warnings(warnings: &[config::LoadWarning]) {
    for w in warnings {
        tracing::warn!(
            target: "aimx::config",
            "{}",
            w.message(),
        );
    }
}

fn dispatch_with_config(
    cmd: Command,
    config: config::Config,
) -> Result<(), Box<dyn std::error::Error>> {
    match cmd {
        Command::Ingest { rcpt } => ingest::run(&rcpt, config),
        Command::Hooks(cmd) => hooks::run(cmd, config),
        Command::DkimKeygen { selector, force } => {
            dkim::run_keygen(&config::dkim_dir(), &config.domain, &selector, force)
        }
        Command::Serve {
            bind,
            tls_cert,
            tls_key,
        } => serve::run(
            Some(bind.as_str()),
            tls_cert.as_deref(),
            tls_key.as_deref(),
            config,
        ),
        Command::Doctor => doctor::run(config),
        Command::Setup { .. }
        | Command::Uninstall { .. }
        | Command::Portcheck { .. }
        | Command::Mcp
        | Command::Send(_)
        | Command::Logs { .. }
        | Command::Mailboxes(_)
        | Command::Agents(_)
        | Command::Upgrade(_) => unreachable!("handled by dispatch"),
    }
}

fn build_network_ops(
    cli_override: Option<&str>,
) -> Result<setup::RealNetworkOps, Box<dyn std::error::Error>> {
    let config = config::Config::load_resolved_ignore_warnings().ok();
    let host =
        portcheck::resolve_verify_host(cli_override, config.as_ref(), setup::DEFAULT_VERIFY_HOST);
    setup::RealNetworkOps::from_verify_host(host)
}
