//! `cellctl` — kubectl-style CLI for CellOS.
//!
//! Doctrine alignment (CHATROOM Session 16):
//!   * **Thin client.** Every subcommand corresponds to exactly one HTTP call
//!     against `cellos-server`. No client-side state, no caches, no projections.
//!   * **Events are the source of truth.** `cellctl logs` and `cellctl events`
//!     surface the CloudEvent stream verbatim; the state machine lives in the
//!     server-side projector.
//!   * **Exit codes are a contract.** Routed through pattern-11
//!     `axiom_exit::Exit`: 0=success, 2=usage, 3=preflight/validation,
//!     64=API/transport failure (tool-specific). Errors go to stderr;
//!     machine-readable output goes to stdout.
//!
//! See `crates/cellos-ctl/src/exit.rs` for the exit-code definitions.
//!
//! ## Public entry
//!
//! Most consumers run the `cellctl` binary directly. The `cellos` meta-crate
//! at `crates/cellos-meta/` re-exports this crate's [`run`] as one of its
//! three installable binaries so `cargo install cellos` ships cellctl,
//! cellos-server, and cellos-supervisor in one go.

pub mod client;
pub mod cmd;
pub mod config;
pub mod exit;
pub mod model;
pub mod output;

use std::path::PathBuf;

use clap::{Parser, Subcommand};

use crate::client::CellosClient;
use crate::exit::{CtlError, CtlResult};
use crate::output::OutputFormat;

/// kubectl-style CLI for CellOS.
#[derive(Parser, Debug)]
#[command(
    name = "cellctl",
    version,
    about = "kubectl-style CLI for CellOS",
    long_about = None,
)]
struct Cli {
    /// Override the server URL (otherwise read from config or $CELLCTL_SERVER).
    #[arg(long, global = true, env = "CELLCTL_SERVER")]
    server: Option<String>,

    /// Override the bearer token (otherwise read from config or $CELLCTL_TOKEN).
    #[arg(long, global = true, env = "CELLCTL_TOKEN", hide_env_values = true)]
    token: Option<String>,

    #[command(subcommand)]
    cmd: Cmd,
}

#[derive(Subcommand, Debug)]
enum Cmd {
    /// Submit a formation spec to the server (POST /v1/formations).
    Apply {
        /// Path to a formation YAML file.
        #[arg(short = 'f', long = "file")]
        file: PathBuf,
    },
    /// List resources.
    Get {
        #[command(subcommand)]
        what: GetWhat,
    },
    /// Show full state + recent events for a single resource.
    Describe {
        #[command(subcommand)]
        what: DescribeWhat,
    },
    /// Delete a resource.
    Delete {
        #[command(subcommand)]
        what: DeleteWhat,
    },
    /// Stream CloudEvents for a single cell.
    Logs {
        /// Cell name or id.
        cell: String,
        /// Keep the connection open and stream new events.
        #[arg(long, short = 'f')]
        follow: bool,
        /// Show only the last N events.
        #[arg(long)]
        tail: Option<usize>,
    },
    /// Stream global / formation-scoped CloudEvents.
    Events {
        /// Filter to a single formation.
        #[arg(long)]
        formation: Option<String>,
        /// Keep the connection open (uses WebSocket /ws/events).
        #[arg(long, short = 'f')]
        follow: bool,
        /// One-shot only: return events with `seq > since`. Pair with
        /// the `cursor` from a previous `cellctl events` response to
        /// page through history without duplicates.
        #[arg(long)]
        since: Option<u64>,
        /// One-shot only: cap the response page (default 100, server
        /// clamps at 1000). Ignored when `--follow` is set.
        #[arg(long)]
        limit: Option<usize>,
    },
    /// Poll a formation until it reaches a terminal state.
    Rollout {
        #[command(subcommand)]
        what: RolloutWhat,
    },
    /// Show what would change between local YAML and the server-side formation.
    Diff {
        /// Path to a formation YAML file.
        #[arg(short = 'f', long = "file")]
        file: PathBuf,
    },
    /// Read/write cellctl config (~/.cellctl/config).
    Config {
        #[command(subcommand)]
        what: ConfigWhat,
    },
    /// Print the cellctl client + server version.
    Version,
    /// Cross-domain audit bundle ops over a local spool (S19/S20, ADR-0029).
    ///
    /// These are LOCAL file operations — no server client. The export/import/
    /// verify logic lives in `cellos-sink-spool::bundle`; this is the operator
    /// surface.
    Audit {
        #[command(subcommand)]
        what: AuditWhat,
    },
    /// Accreditation evidence: lint the 800-53 catalog or emit OSCAL artifacts.
    ///
    /// LOCAL operations over the in-repo control-mapping catalog (logic lives in
    /// `cellos-evidence`). Emitted artifacts are CANDIDATE evidence, not a
    /// self-attestation of control satisfaction.
    Evidence {
        #[command(subcommand)]
        what: EvidenceWhat,
    },
    /// Spin up a localhost browser proxy for the cellctl web view (ADR-0017).
    Webui {
        /// Launch the system browser at the URL after binding.
        #[arg(long)]
        open: bool,
        /// Bind mode: `auto` (default; loopback in this MVP),
        /// `loopback` (force 127.0.0.1), or `unix` (planned).
        #[arg(long, value_enum, default_value = "auto")]
        bind: cmd::webui::BindMode,
    },
}

#[derive(Subcommand, Debug)]
enum GetWhat {
    /// List formations.
    Formations {
        #[arg(long, short = 'o', default_value = "table")]
        output: String,
    },
    /// List cells (optionally filtered to a single formation).
    Cells {
        #[arg(long)]
        formation: Option<String>,
        #[arg(long, short = 'o', default_value = "table")]
        output: String,
    },
}

#[derive(Subcommand, Debug)]
enum DescribeWhat {
    /// Describe a formation by name or id.
    Formation {
        /// Formation name or id to describe.
        name: String,
    },
    /// Describe a cell by name or id.
    Cell {
        /// Cell name or id to describe.
        name: String,
    },
}

#[derive(Subcommand, Debug)]
enum DeleteWhat {
    /// Delete a formation (also tears down its cells server-side).
    Formation {
        /// Formation name or id to delete.
        name: String,
        /// Skip interactive confirmation.
        #[arg(long, short = 'y')]
        yes: bool,
    },
}

#[derive(Subcommand, Debug)]
enum RolloutWhat {
    /// Poll a formation until it reaches COMPLETED or FAILED.
    Status {
        /// Formation name or id to poll.
        name: String,
        /// Give up after N seconds (default: no timeout).
        #[arg(long)]
        timeout: Option<u64>,
    },
}

#[derive(Subcommand, Debug)]
enum AuditWhat {
    /// Export a contiguous chain range `[from, to]` as a bundle file.
    ExportBundle {
        /// Spool directory holding the audit chain.
        #[arg(long)]
        dir: PathBuf,
        /// Operator-declared chain id (routing label) recorded in the manifest.
        #[arg(long)]
        chain_id: String,
        /// First row seq to include.
        #[arg(long)]
        from: u64,
        /// Last row seq to include (defaults to the chain head).
        #[arg(long)]
        to: Option<u64>,
        /// Output bundle file to write.
        #[arg(long)]
        out: PathBuf,
    },
    /// Import a bundle file into the local spool (fail-closed).
    ImportBundle {
        /// Spool directory to import into.
        #[arg(long)]
        dir: PathBuf,
        /// Bundle file to read.
        #[arg(long = "in")]
        input: PathBuf,
    },
    /// Verify the on-disk chain and report status (exits 1 on a break).
    VerifyChain {
        /// Spool directory holding the audit chain.
        #[arg(long)]
        dir: PathBuf,
    },
}

#[derive(Subcommand, Debug)]
enum EvidenceWhat {
    /// Lint the 800-53 control-mapping catalog (schema + on-disk seam/test refs).
    Lint,
    /// Emit the OSCAL component-definition + SSP-fragment to a directory.
    Emit {
        /// Output directory for the OSCAL artifacts.
        #[arg(long)]
        out: PathBuf,
    },
}

#[derive(Subcommand, Debug)]
enum ConfigWhat {
    /// Set the server URL persistently.
    SetServer {
        /// Server base URL (e.g. http://127.0.0.1:8080).
        url: String,
    },
    /// Set the bearer token persistently.
    SetToken {
        /// Bearer token to send as `Authorization: Bearer <TOKEN>`.
        token: String,
    },
    /// Print the resolved config.
    Show,
}

/// Run the `cellctl` CLI. Returns when the command completes or exits the
/// process on error via [`CtlError::exit`]. This is the entry point both
/// the standalone `cellctl` binary and the `cellos` meta-crate's `cellctl`
/// shim call into.
pub fn run() {
    // Tracing is opt-in via $RUST_LOG so noisy debug output never goes to stderr
    // by default — that would muddy the doctrine error contract.
    //
    // HIGH-B5: when tracing IS enabled, the redacted filter on the fmt
    // layer suppresses reqwest/hyper TRACE events that would otherwise dump
    // bearer tokens (cellctl makes authenticated reqwest calls to
    // cellos-server's API; `RUST_LOG=reqwest=trace` is exactly the failure
    // mode this fixes).
    if std::env::var_os("RUST_LOG").is_some() {
        use tracing_subscriber::layer::SubscriberExt;
        use tracing_subscriber::util::SubscriberInitExt;
        use tracing_subscriber::Layer;

        let fmt_layer = tracing_subscriber::fmt::layer()
            .with_writer(std::io::stderr)
            .with_filter(cellos_core::observability::redacted_filter());

        let _ = tracing_subscriber::registry()
            .with(tracing_subscriber::EnvFilter::from_default_env())
            .with(fmt_layer)
            .try_init();
    }

    let cli = Cli::parse();

    // Build a single-threaded current-thread runtime; cellctl is I/O bound and
    // doesn't benefit from a multi-thread scheduler.
    let rt = match tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
    {
        Ok(rt) => rt,
        Err(e) => CtlError::usage(format!("init tokio runtime: {e}")).exit(),
    };

    // Both arms route the process exit byte through the pattern-11
    // `axiom_exit::Exit` FSM (success -> `Exit::Ok`; failure -> the variant
    // mapped by `CtlError::exit`).
    match rt.block_on(dispatch(cli)) {
        Ok(()) => std::process::exit(axiom_exit::Exit::Ok.code().into()),
        Err(e) => e.exit(),
    }
}

async fn dispatch(cli: Cli) -> CtlResult<()> {
    // Effective config = on-disk config overridden by CLI flags / env vars.
    let mut cfg = config::load().unwrap_or_default();
    if let Some(s) = cli.server {
        cfg.server_url = Some(s);
    }
    if let Some(t) = cli.token {
        cfg.api_token = Some(t);
    }

    // Config + Version + Webui don't go through the normal CellosClient
    // dispatch — Webui takes the raw config to drive its reverse proxy.
    match cli.cmd {
        Cmd::Config { what } => return run_config(what),
        // Audit is a pure-local file op (like Config/Webui): no server client.
        Cmd::Audit { what } => return run_audit(what),
        Cmd::Evidence { what } => return run_evidence(what),
        Cmd::Version => {
            let client = CellosClient::new(&cfg)?;
            return cmd::version::run(&client).await;
        }
        Cmd::Webui { open, bind } => {
            return cmd::webui::run(&cfg, open, bind).await;
        }
        _ => {}
    }

    let client = CellosClient::new(&cfg)?;

    match cli.cmd {
        Cmd::Apply { file } => cmd::apply::run(&client, &file).await,
        Cmd::Get { what } => match what {
            GetWhat::Formations { output } => {
                let fmt: OutputFormat = output.parse()?;
                cmd::get::formations(&client, fmt).await
            }
            GetWhat::Cells { formation, output } => {
                let fmt: OutputFormat = output.parse()?;
                cmd::get::cells(&client, formation.as_deref(), fmt).await
            }
        },
        Cmd::Describe { what } => match what {
            DescribeWhat::Formation { name } => cmd::describe::formation(&client, &name).await,
            DescribeWhat::Cell { name } => cmd::describe::cell(&client, &name).await,
        },
        Cmd::Delete { what } => match what {
            DeleteWhat::Formation { name, yes } => {
                cmd::delete::formation(&client, &name, yes).await
            }
        },
        Cmd::Logs { cell, follow, tail } => cmd::logs::run(&client, &cell, follow, tail).await,
        Cmd::Events {
            formation,
            follow,
            since,
            limit,
        } => cmd::events::run(&client, formation.as_deref(), follow, since, limit).await,
        Cmd::Rollout { what } => match what {
            RolloutWhat::Status { name, timeout } => {
                cmd::rollout::status(&client, &name, timeout).await
            }
        },
        Cmd::Diff { file } => cmd::diff::run(&client, &file).await,
        Cmd::Config { .. }
        | Cmd::Audit { .. }
        | Cmd::Evidence { .. }
        | Cmd::Version
        | Cmd::Webui { .. } => {
            unreachable!("handled above")
        }
    }
}

fn run_audit(what: AuditWhat) -> CtlResult<()> {
    match what {
        AuditWhat::ExportBundle {
            dir,
            chain_id,
            from,
            to,
            out,
        } => cmd::audit::export_bundle(&dir, &chain_id, from, to, &out),
        AuditWhat::ImportBundle { dir, input } => cmd::audit::import_bundle(&dir, &input),
        AuditWhat::VerifyChain { dir } => cmd::audit::verify_chain(&dir),
    }
}

fn run_evidence(what: EvidenceWhat) -> CtlResult<()> {
    match what {
        EvidenceWhat::Lint => cmd::evidence::lint(),
        EvidenceWhat::Emit { out } => cmd::evidence::emit(&out),
    }
}

fn run_config(what: ConfigWhat) -> CtlResult<()> {
    match what {
        ConfigWhat::SetServer { url } => cmd::config_cmd::set_server(&url),
        ConfigWhat::SetToken { token } => cmd::config_cmd::set_token(&token),
        ConfigWhat::Show => cmd::config_cmd::show(),
    }
}
