// SPDX-License-Identifier: AGPL-3.0-only
//! Clap CLI definitions.

use cerulion_cli_engine::graph_cmd::TimeSource as EngineTimeSource;
use clap::{Parser, Subcommand, ValueEnum};

/// Clap-parseable mirror of the engine's [`EngineTimeSource`] (CER-222
/// chunk 4a). The engine crate does not depend on `clap`, so the
/// `ValueEnum` lives here and maps 1:1 to the canonical engine enum.
/// Value names are lowercased by clap's default `ValueEnum` renaming:
/// `real`, `external`, `virtual`.
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum TimeSource {
    /// Live, event-driven loop on `RealClock` (the default).
    Real,
    /// Externally-mastered time (`ExternalClock`) — inert today (CER-354/CER-424).
    External,
    /// Deterministic `VirtualClock` + 1 ms poll loop (replay / bench).
    Virtual,
}

impl From<TimeSource> for EngineTimeSource {
    fn from(ts: TimeSource) -> Self {
        match ts {
            TimeSource::Real => EngineTimeSource::Real,
            TimeSource::External => EngineTimeSource::External,
            TimeSource::Virtual => EngineTimeSource::Virtual,
        }
    }
}

#[derive(Parser)]
#[command(name = "cerulion", about = "Cerulion robotics middleware CLI", version)]
pub struct Cli {
    /// Enable verbose (debug-level) logging
    #[arg(global = true, long)]
    pub verbose: bool,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Workspace management
    Workspace {
        #[command(subcommand)]
        action: WorkspaceAction,
    },
    /// Node management
    Node {
        #[command(subcommand)]
        action: NodeAction,
    },
    /// Graph management
    Graph {
        #[command(subcommand)]
        action: GraphAction,
    },
    /// Topic introspection
    Topic {
        #[command(subcommand)]
        action: TopicAction,
    },
    /// Schema management
    Schema {
        #[command(subcommand)]
        action: SchemaAction,
    },
    /// Interactive TUI dashboard
    Tui,
    /// Inspect publish-trace bag files written by Regime A
    /// (`cerulion_core::trace::BagWriter`). Reads JSON-Lines records
    /// from a directory and prints a human-readable timeline.
    Trace {
        #[command(subcommand)]
        action: TraceAction,
    },
    /// Wipe lingering iceoryx2 SHM state (services, nodes, event files).
    ///
    /// Useful when the topic topology changes between runs — iceoryx2
    /// services persist on `/tmp/iceoryx2/` until the OS reboots, and a
    /// graph that no longer matches the cached service config can fail
    /// to publish or look like it's missing topics.
    Clean,
}

#[derive(Subcommand)]
pub enum TraceAction {
    /// Print a human-readable timeline of bag-file records in `dir`.
    ///
    /// Reads every `trace_*.jsonl` file in lexicographic order
    /// (matches BagWriter's sequential file-index naming) and prints
    /// one line per record, formatted as
    /// `<topic> seq=<N> t=<TIMESTAMP_NS> schema=<HASH>`. Use
    /// `--filter <topic>` to restrict to a single topic.
    Inspect {
        /// Directory containing `trace_*.jsonl` files.
        dir: String,
        /// Only show entries whose `topic` matches this string exactly.
        #[arg(short = 't', long)]
        filter: Option<String>,
        /// Limit the number of records printed (most recent first
        /// if `--reverse`, otherwise oldest-first).
        #[arg(short = 'n', long)]
        limit: Option<usize>,
        /// Print most-recent records first instead of oldest-first.
        #[arg(short = 'r', long)]
        reverse: bool,
    },
}

#[derive(Subcommand)]
pub enum WorkspaceAction {
    /// Create a new workspace
    Create {
        /// Workspace name
        name: String,
    },
    /// Initialize a workspace at the current directory
    Init {
        /// Location (default: current directory)
        #[arg(default_value = ".")]
        location: String,
    },
}

#[derive(Subcommand)]
pub enum NodeAction {
    /// Create a new node type
    Create {
        /// Node type name
        node_type: String,
        /// Add output port: SCHEMA NAME (both required)
        #[arg(short = 'o', long = "output", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        output: Vec<String>,
        /// Add input port: SCHEMA NAME (both required). For
        /// trigger inputs use `-T` instead.
        #[arg(short = 'i', long = "input", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        input: Vec<String>,
        /// Add a TRIGGER input: SCHEMA NAME. The new input is
        /// declared as `#[input(trigger)]` and the node's trigger
        /// policy is set to `data_trigger=<NAME>`. SCHEMA and NAME
        /// are BOTH required so that a multi-`-T` user error can't
        /// be silently misread as `-T SCHEMA1=NAME(SCHEMA2)`. At
        /// most one `-T` per node; for sync over multiple inputs
        /// use `-i` for each input plus
        /// `--policy sync_window_ms=N`.
        #[arg(short = 'T', long = "trigger-input", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        trigger_input: Vec<String>,
        /// Trigger policy. SPEC is one of: `period_ms=N`,
        /// `sync_window_ms=N`, `external`,
        /// `data_trigger=NAME` (or `trigger=NAME`). N must be > 0
        /// for the time-based forms; bare `period_ms` (no `=N`)
        /// is rejected. Omitting `--policy` falls back to the
        /// defaulting rules below.
        ///
        /// Defaulting rules when `--policy` is omitted:
        /// - 0 inputs (no `-i` and no `-T`): error — a source-only
        ///   node must declare a non-data policy explicitly
        ///   (`--policy period_ms=N` or `--policy external`).
        /// - 1+ inputs (any `-i` count, no `-T`): no policy attr
        ///   is written; the runtime fires on any input arrival
        ///   and emits a warning at graph-build time so the user
        ///   notices and can pick a more specific policy if
        ///   desired. Use `--policy sync_window_ms=N`,
        ///   `--policy data_trigger=NAME`, or `-T SCHEMA NAME`
        ///   to opt into a non-default trigger.
        /// - `-T` declares an explicit trigger input regardless
        ///   of `-i` count, equivalent to
        ///   `--policy data_trigger=<NAME>`.
        ///
        /// `-T` and `--policy data_trigger=NAME` are equivalent;
        /// supply at most one. `--policy` and `-T` may be
        /// combined only when they agree on the trigger input.
        #[arg(long, value_name = "SPEC")]
        policy: Option<String>,
        /// Use raw FFI template instead of #\[cerulion_node\] macro
        #[arg(long)]
        raw_ffi: bool,
    },
    /// Delete a node type
    Delete {
        /// Node type name
        node_type: String,
    },
    /// Modify a node (add ports, change trigger policy)
    Modify {
        /// Node type name
        node_type: String,
        /// Add input port: SCHEMA NAME (both required).
        #[arg(short = 'i', long = "input", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        input: Vec<String>,
        /// Add a TRIGGER input: SCHEMA NAME. Adds the input with
        /// `#[input(trigger)]` AND sets the node's policy to
        /// `data_trigger=<NAME>`, clearing any conflicting
        /// `period_ms` / `sync_window_ms` /
        /// `external` macro args. SCHEMA and NAME are BOTH
        /// required so multi-`-T` user errors produce a 4-element
        /// vec the engine can detect, not a 2-element vec it
        /// silently misreads.
        #[arg(short = 'T', long = "trigger-input", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        trigger_input: Vec<String>,
        /// Add output port: SCHEMA NAME (both required).
        #[arg(short = 'o', long = "output", num_args = 2, value_names = ["SCHEMA", "NAME"])]
        output: Vec<String>,
        /// Set the trigger policy. SPEC is one of: `period_ms=N`,
        /// `sync_window_ms=N`, `external`,
        /// `data_trigger=NAME` (or `trigger=NAME`). N must be > 0
        /// for the time-based forms; bare `period_ms` (no `=N`)
        /// is rejected.
        ///
        /// `data_trigger=NAME` requires NAME to match an input —
        /// either an existing one or one being added in the same
        /// `node modify` call via `-i` or `-T`.
        #[arg(long, value_name = "SPEC")]
        policy: Option<String>,
    },
    /// Build a node crate
    Build {
        /// Node type name
        node_type: String,
        /// Build in release mode
        #[arg(long)]
        release: bool,
    },
    /// Stage a node into a graph
    Stage {
        /// Node type name
        node_type: String,
        /// Node instance ID
        #[arg(short = 'i', long)]
        id: Option<String>,
        /// Target graph name
        #[arg(short = 'g', long)]
        graph: Option<String>,
        /// Prefix for topic resolution
        #[arg(short = 'p', long)]
        prefix: Option<String>,
        /// Input bindings: NAME SOURCE (e.g., -I image \[camera,image\])
        #[arg(short = 'I', num_args = 2, value_names = ["NAME", "SOURCE"])]
        input_binding: Vec<String>,
    },
    /// Run a single node
    Run {
        /// Node type name
        node_type: String,
        /// Topic prefix
        #[arg(short = 'p', long)]
        prefix: Option<String>,
        /// Node instance ID
        #[arg(short = 'i', long)]
        id: Option<String>,
        /// Force the release-profile cdylib (`target/release`) when
        /// loading the node, matching `cerulion node build --release`.
        /// Without this flag the freshest built cdylib is auto-selected
        /// between `target/debug` and `target/release` (the profile you
        /// most recently built wins); the other profile stays a
        /// fallback when only one is built. Mirrors `graph run
        /// --release` (CER-193, CER-426).
        #[arg(long)]
        release: bool,
        /// Disable the CPU C-state cap (Linux PM_QoS via /dev/cpu_dma_latency).
        /// On the live (real/external) clock the cap is DERIVED from the
        /// graph's own tightest timing: a latency-sensitive graph auto-caps
        /// idle to a shallow C-state at C1, recovering MOST of the ~13µs
        /// C-state cold-wake at low power (full ~13µs only via a C0 pin,
        /// `CERULION_CPU_DMA_LOCK=1`); a latency-tolerant graph takes no lock (idles deep
        /// for power). Pass this to disable the cap entirely (laptops /
        /// power-sensitive dev). Force a hard C0 pin with
        /// `CERULION_CPU_DMA_LOCK=1`, or an explicit µs cap with
        /// `CERULION_CPU_DMA_LOCK_US=N`. Linux-only.
        #[arg(long = "no-cpu-dma-lock")]
        no_cpu_dma_lock: bool,
        /// Hard opt-out from the live-loop CPU monitor-wait park (CER-443) —
        /// env cannot re-enable it. Without this flag, on the live
        /// (real/external) clock the park AUTO-resolves when a real CPU
        /// monitor-wait primitive exists (Linux x86_64 WAITPKG / Linux aarch64
        /// WFE): it replaces the blocking WaitSet wait with a shallow per-core
        /// park (no deep cpuidle C-state), cutting the post-idle cold-wake
        /// latency at a small per-core power cost (and suppressing the global
        /// C-state cap). That auto resolution is tunable via
        /// `CERULION_MONITOR_WAIT` / `CERULION_DOORBELL` (`"1"` force-on /
        /// `"0"` disable) — but only when this flag is absent. No effect off
        /// the supported targets (no primitive → blocking WaitSet).
        #[arg(long = "no-monitor-wait")]
        no_monitor_wait: bool,
    },
    /// List all node types
    List,
    /// Show info about a node type
    Info {
        /// Node type name
        node_type: String,
    },
}

#[derive(Subcommand)]
pub enum GraphAction {
    /// Create a new graph
    Create {
        /// Graph name
        name: String,
        /// Namespace prefix
        #[arg(short = 'n', long)]
        prefix: Option<String>,
    },
    /// Run a graph
    Run {
        /// Graph name
        name: String,
        /// Clock that drives the run (CER-222):
        ///   `real`     — live event-driven loop on RealClock (default; wakes
        ///                within microseconds of a publish on the iceoryx2 WaitSet),
        ///   `virtual`  — deterministic VirtualClock + 1 ms poll loop (replay,
        ///                deterministic runs, benchmarking),
        ///   `external` — ExternalClock for an external time master. INERT today
        ///                (CER-354/CER-424 not wired): the clock starts at 0 and
        ///                will not advance, so time-based triggers never fire.
        #[arg(long = "time-source", value_enum, default_value_t = TimeSource::Real)]
        time_source: TimeSource,
        /// Skip workspace validation before running
        #[arg(long)]
        no_validate: bool,
        /// Force release-profile cdylibs (`target/release`) when
        /// loading node libraries, matching `cerulion node build
        /// --release`. Without this flag the freshest built cdylib is
        /// auto-selected between `target/debug` and `target/release`
        /// (the profile you most recently built wins); the other
        /// profile stays a fallback when only one is built. The pre-run
        /// validation uses the same mode (CER-193, CER-426).
        #[arg(long)]
        release: bool,
        /// Disable the CPU C-state cap (Linux PM_QoS via /dev/cpu_dma_latency).
        /// On the live (real/external) clock the cap is DERIVED from the
        /// graph's own tightest timing: a latency-sensitive graph auto-caps
        /// idle to a shallow C-state at C1, recovering MOST of the ~13µs
        /// C-state cold-wake at low power (full ~13µs only via a C0 pin,
        /// `CERULION_CPU_DMA_LOCK=1`); a latency-tolerant graph takes no lock. No effect
        /// under `--time-source virtual` (the poll loop never idles; auto
        /// takes no lock there). Force a hard C0 pin with
        /// `CERULION_CPU_DMA_LOCK=1`, or an explicit µs cap with
        /// `CERULION_CPU_DMA_LOCK_US=N`. Linux-only.
        #[arg(long = "no-cpu-dma-lock")]
        no_cpu_dma_lock: bool,
        /// Hard opt-out from the live-loop CPU monitor-wait park (CER-443) —
        /// env cannot re-enable it. Without this flag, on the live
        /// (real/external) clock the park AUTO-resolves when a real CPU
        /// monitor-wait primitive exists (Linux x86_64 WAITPKG / Linux aarch64
        /// WFE): it replaces the blocking WaitSet wait with a shallow per-core
        /// park (no deep cpuidle C-state), cutting the post-idle cold-wake
        /// latency at a small per-core power cost (and suppressing the global
        /// C-state cap). That auto resolution is tunable via
        /// `CERULION_MONITOR_WAIT` / `CERULION_DOORBELL` (`"1"` force-on /
        /// `"0"` disable) — but only when this flag is absent. No effect under
        /// `--time-source virtual` (the poll loop never parks).
        #[arg(long = "no-monitor-wait")]
        no_monitor_wait: bool,
    },
    /// Validate a graph against the workspace
    Validate {
        /// Graph name
        name: String,
        /// Force release-profile cdylibs (`target/release`) when
        /// resolving node libraries, matching `cerulion node build
        /// --release`. Without this flag the freshest built cdylib is
        /// auto-selected between `target/debug` and `target/release`
        /// (the profile you most recently built wins); the other
        /// profile stays a fallback when only one is built. The report
        /// shows which profile was found (CER-193, CER-426).
        #[arg(long)]
        release: bool,
    },
    /// List all graphs
    List,
}

#[derive(Subcommand)]
pub enum TopicAction {
    /// List active topics
    List,
    /// Show info about a topic
    Info {
        /// Topic name
        topic: String,
    },
    /// Echo messages from a topic
    Echo {
        /// Topic name
        topic: String,
    },
    /// Measure publish rate
    Hz {
        /// Topic name
        topic: String,
    },
}

#[derive(Subcommand)]
pub enum SchemaAction {
    /// Create a new schema
    Create {
        /// Schema name
        name: String,
    },
    /// Delete a schema
    Delete {
        /// Schema name
        name: String,
    },
    /// Show info about a schema
    Info {
        /// Schema name
        name: String,
    },
}

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

    /// `graph run <name>` (no flag) defaults to `TimeSource::Real` — the
    /// live-by-default behavior (CER-222 chunk 4a; CER-398 chunk 25b flip).
    #[test]
    fn graph_run_defaults_to_real() {
        let cli = Cli::try_parse_from(["cerulion", "graph", "run", "g"])
            .expect("bare `graph run g` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run { time_source, .. },
            } => assert_eq!(time_source, TimeSource::Real, "default must be Real"),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// `graph run <name> --time-source virtual` parses to `TimeSource::Virtual`.
    #[test]
    fn graph_run_time_source_virtual() {
        let cli =
            Cli::try_parse_from(["cerulion", "graph", "run", "g", "--time-source", "virtual"])
                .expect("`graph run g --time-source virtual` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run { time_source, .. },
            } => assert_eq!(time_source, TimeSource::Virtual),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// `graph run <name> --time-source external` parses to `TimeSource::External`.
    #[test]
    fn graph_run_time_source_external() {
        let cli =
            Cli::try_parse_from(["cerulion", "graph", "run", "g", "--time-source", "external"])
                .expect("`graph run g --time-source external` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run { time_source, .. },
            } => assert_eq!(time_source, TimeSource::External),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// The removed `--sim-clock` flag is now an unknown arg and must fail to
    /// parse (CER-222 chunk 4a hard-removed it, no alias).
    #[test]
    fn graph_run_rejects_removed_sim_clock_flag() {
        assert!(
            Cli::try_parse_from(["cerulion", "graph", "run", "g", "--sim-clock"]).is_err(),
            "--sim-clock was removed and must be rejected"
        );
    }

    /// CER-440: `graph run g --no-cpu-dma-lock` parses with the flag set.
    #[test]
    fn graph_run_no_cpu_dma_lock_flag_sets_true() {
        let cli = Cli::try_parse_from(["cerulion", "graph", "run", "g", "--no-cpu-dma-lock"])
            .expect("`graph run g --no-cpu-dma-lock` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run {
                    no_cpu_dma_lock, ..
                },
            } => assert!(no_cpu_dma_lock, "flag must set no_cpu_dma_lock = true"),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-440: bare `graph run g` leaves `no_cpu_dma_lock` false (lock
    /// default-on for the live path).
    #[test]
    fn graph_run_no_cpu_dma_lock_defaults_false() {
        let cli = Cli::try_parse_from(["cerulion", "graph", "run", "g"])
            .expect("bare `graph run g` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run {
                    no_cpu_dma_lock, ..
                },
            } => assert!(!no_cpu_dma_lock, "default must be no_cpu_dma_lock = false"),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-440: `--no-cpu-dma-lock` coexists with `--time-source virtual`
    /// (the flag is independent of the clock selection).
    #[test]
    fn graph_run_no_cpu_dma_lock_coexists_with_virtual() {
        let cli = Cli::try_parse_from([
            "cerulion",
            "graph",
            "run",
            "g",
            "--no-cpu-dma-lock",
            "--time-source",
            "virtual",
        ])
        .expect("`graph run g --no-cpu-dma-lock --time-source virtual` must parse");
        match cli.command {
            Commands::Graph {
                action:
                    GraphAction::Run {
                        no_cpu_dma_lock,
                        time_source,
                        ..
                    },
            } => {
                assert!(no_cpu_dma_lock);
                assert_eq!(time_source, TimeSource::Virtual);
            }
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-440: `--no-cpu-dma-lock` coexists with `--time-source external`
    /// — the opt-out is honored even on the live `External` clock (whose
    /// default would otherwise acquire the lock).
    #[test]
    fn graph_run_no_cpu_dma_lock_coexists_with_external() {
        let cli = Cli::try_parse_from([
            "cerulion",
            "graph",
            "run",
            "g",
            "--no-cpu-dma-lock",
            "--time-source",
            "external",
        ])
        .expect("`graph run g --no-cpu-dma-lock --time-source external` must parse");
        match cli.command {
            Commands::Graph {
                action:
                    GraphAction::Run {
                        no_cpu_dma_lock,
                        time_source,
                        ..
                    },
            } => {
                assert!(no_cpu_dma_lock);
                assert_eq!(time_source, TimeSource::External);
            }
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-443: `graph run g --no-monitor-wait` parses with the flag set.
    #[test]
    fn graph_run_no_monitor_wait_flag_sets_true() {
        let cli = Cli::try_parse_from(["cerulion", "graph", "run", "g", "--no-monitor-wait"])
            .expect("`graph run g --no-monitor-wait` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run {
                    no_monitor_wait, ..
                },
            } => assert!(no_monitor_wait, "flag must set no_monitor_wait = true"),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-443: bare `graph run g` leaves `no_monitor_wait` false (park
    /// auto-on for the live path where the primitive exists).
    #[test]
    fn graph_run_no_monitor_wait_defaults_false() {
        let cli = Cli::try_parse_from(["cerulion", "graph", "run", "g"])
            .expect("bare `graph run g` must parse");
        match cli.command {
            Commands::Graph {
                action: GraphAction::Run {
                    no_monitor_wait, ..
                },
            } => assert!(!no_monitor_wait, "default must be no_monitor_wait = false"),
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-443: `--no-monitor-wait` coexists with `--time-source virtual` (the
    /// flag is independent of the clock selection; the park is live-only so it
    /// is a no-op under virtual, but the flag must still parse).
    #[test]
    fn graph_run_no_monitor_wait_coexists_with_virtual() {
        let cli = Cli::try_parse_from([
            "cerulion",
            "graph",
            "run",
            "g",
            "--no-monitor-wait",
            "--time-source",
            "virtual",
        ])
        .expect("`graph run g --no-monitor-wait --time-source virtual` must parse");
        match cli.command {
            Commands::Graph {
                action:
                    GraphAction::Run {
                        no_monitor_wait,
                        time_source,
                        ..
                    },
            } => {
                assert!(no_monitor_wait);
                assert_eq!(time_source, TimeSource::Virtual);
            }
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-443: `--no-monitor-wait` coexists with `--time-source external` —
    /// the opt-out is honored on the live `External` clock (whose default would
    /// otherwise arm the park where the primitive exists).
    #[test]
    fn graph_run_no_monitor_wait_coexists_with_external() {
        let cli = Cli::try_parse_from([
            "cerulion",
            "graph",
            "run",
            "g",
            "--no-monitor-wait",
            "--time-source",
            "external",
        ])
        .expect("`graph run g --no-monitor-wait --time-source external` must parse");
        match cli.command {
            Commands::Graph {
                action:
                    GraphAction::Run {
                        no_monitor_wait,
                        time_source,
                        ..
                    },
            } => {
                assert!(no_monitor_wait);
                assert_eq!(time_source, TimeSource::External);
            }
            _ => panic!("expected Graph::Run"),
        }
    }

    /// CER-443: `--no-monitor-wait` and `--no-cpu-dma-lock` coexist (independent
    /// opt-outs; both set true together).
    #[test]
    fn graph_run_no_monitor_wait_and_no_cpu_dma_lock_coexist() {
        let cli = Cli::try_parse_from([
            "cerulion",
            "graph",
            "run",
            "g",
            "--no-monitor-wait",
            "--no-cpu-dma-lock",
        ])
        .expect("both opt-out flags must parse together");
        match cli.command {
            Commands::Graph {
                action:
                    GraphAction::Run {
                        no_monitor_wait,
                        no_cpu_dma_lock,
                        ..
                    },
            } => {
                assert!(no_monitor_wait);
                assert!(no_cpu_dma_lock);
            }
            _ => panic!("expected Graph::Run"),
        }
    }
}
