//! nash: the observation/enforcement gate (docs/NASH.md §4.2).
//!
//! A process-global, verdict-shaped hook at the shell's command choke point.
//! The default implementation allows everything and observes nothing, so an
//! unconfigured shell behaves exactly like upstream brush. nash installs a
//! recording gate at startup (`nash-observe`); a future enforcement mode may
//! return `Deny` (and, later, hold pending a host policy round-trip) without
//! any further changes to this crate.

use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::OnceLock;

/// Details about a simple command about to be executed (builtin, function, or
/// external), captured after expansion.
#[derive(Clone, Debug)]
pub struct ExecStart {
    /// Full argv, argv[0] first.
    pub argv: Vec<String>,
    /// The shell's working directory at execution time.
    pub cwd: PathBuf,
    /// nash: the redirections applied to this command (docs/NASH.md §5.2). The
    /// observer reads back the affected byte range after the command completes.
    pub redirects: Vec<RedirectRecord>,
    /// nash: the pipeline stage this command occupies, when it runs as part of
    /// `a | b | c` (docs/NASH.md §5.1). `None` for a standalone command.
    pub pipeline: Option<PipelineSlot>,
}

/// Where a command sits in the pipeline it belongs to (docs/NASH.md §5.1).
///
/// Shares its `id` with the [`PipeEvent`]s of the same pipeline, which is what
/// lets an observer name the commands either side of a link (`curl … | sh`)
/// instead of reporting bare stage indices.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PipelineSlot {
    /// Correlates this stage with the pipeline's links; see [`PipeEvent::pipeline_id`].
    pub id: u64,
    /// Zero-based position of this stage within the pipeline.
    pub index: usize,
    /// Total number of stages in the pipeline.
    pub len: usize,
}

/// How a redirection's data is captured after the command runs (docs/NASH.md §5.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RedirectReadback {
    /// A truncating write (`>`, `>|`, `&>`): read the head of the final file.
    Truncate,
    /// An append (`>>`): read the bytes added after `size_before`.
    Append,
    /// An input (`<`): read the head of the source file.
    Input,
    /// Inline data known at setup (heredoc / here-string): no file read-back.
    Inline,
}

/// One redirection observed on a command. For regular files nash hands the child
/// the *real* fd (semantics untouched) and reads back a bounded range afterward;
/// for heredoc/here-string the body is captured inline at setup.
#[derive(Clone, Debug)]
pub struct RedirectRecord {
    /// The operator as written (`>`, `>>`, `<`, `2>`, `&>`, `<<`, `<<<`).
    pub op: String,
    /// The affected file descriptor number.
    pub fd: u32,
    /// The regular-file target, if any (`None` for heredoc/here-string).
    pub path: Option<PathBuf>,
    /// How to read the data back.
    pub readback: RedirectReadback,
    /// The file's size at setup time (for append ranges); 0 otherwise.
    pub size_before: u64,
    /// Inline body for heredoc/here-string (already known at setup).
    pub inline: Option<String>,
}

/// Details about a completed simple command.
#[derive(Clone, Debug)]
pub struct ExecEnd {
    /// Exit code (128+signal for signal deaths).
    pub exit_code: i32,
}

/// Data observed flowing across one pipe link `a | b` (docs/NASH.md §5.3).
/// Reported once the link drains (producer closed or consumer went away).
#[derive(Clone, Debug)]
pub struct PipeEvent {
    /// Correlates the links of one pipeline invocation.
    pub pipeline_id: u64,
    /// Zero-based index of the producing stage.
    pub from_index: usize,
    /// Zero-based index of the consuming stage.
    pub to_index: usize,
    /// The producing stage as written, rendered from its AST at spawn time.
    ///
    /// Carried on the link itself so an observer never has to *infer* which command fed a pipe
    /// from the exec events around it: a stage that is a compound command (`{ a; b; } | c`) emits
    /// one exec per command inside it, and none of them says which owned the pipe. Rendering the
    /// stage answers it outright — and does so at spawn, before the producer has even exited.
    ///
    /// It is also the *pre-expansion* text, so `curl -H "Bearer $TOKEN" | sh` names the stage with
    /// `$TOKEN` rather than the expanded secret an argv-derived name would carry.
    pub from_text: String,
    /// The consuming stage as written. See [`PipeEvent::from_text`].
    pub to_text: String,
    /// Total bytes that crossed the link.
    pub total_bytes: u64,
    /// A bounded prefix of the bytes (observer applies its own caps/redaction).
    pub captured: Vec<u8>,
    /// Whether `total_bytes` exceeded `captured`.
    pub truncated: bool,
}

/// The gate's decision for a command about to execute.
pub enum Verdict {
    /// Run the command; the token is echoed back via `on_exit`.
    Allow(u64),
    /// Refuse to run the command; the shell reports `message` on stderr and
    /// the command evaluates to `status`.
    Deny {
        /// Exit status the denied command evaluates to.
        status: u8,
        /// Message reported to stderr.
        message: String,
    },
}

/// Process-global hook invoked around every simple command.
pub trait Gate: Send + Sync {
    /// Called before a simple command runs. Must not block in observe-only
    /// implementations.
    fn on_exec(&self, ev: ExecStart) -> Verdict;
    /// Called when a simple command completes, with the token issued by
    /// `on_exec`.
    fn on_exit(&self, token: u64, ev: ExecEnd);
    /// nash: called with the (trimmed) output of a command substitution
    /// `$(…)` / backticks (docs/NASH.md §5.3). Default no-op.
    fn on_cmdsub(&self, _output: &str) {}
    /// nash: called once a pipe link `a | b` drains (docs/NASH.md §5.3).
    /// Invoked from the tee copier thread. Default no-op.
    fn on_pipe(&self, _ev: PipeEvent) {}
}

struct AllowAll;

impl Gate for AllowAll {
    fn on_exec(&self, _ev: ExecStart) -> Verdict {
        Verdict::Allow(0)
    }
    fn on_exit(&self, _token: u64, _ev: ExecEnd) {}
}

static GATE: OnceLock<Box<dyn Gate>> = OnceLock::new();
static ALLOW_ALL: AllowAll = AllowAll;

/// Installs the process-global gate. May be called at most once, before any
/// shell runs; later calls are ignored.
pub fn set_gate(gate: Box<dyn Gate>) {
    let _ = GATE.set(gate);
}

/// Returns the installed gate, or the allow-all default.
pub(crate) fn gate() -> &'static dyn Gate {
    GATE.get().map_or(&ALLOW_ALL as &dyn Gate, AsRef::as_ref)
}

/// Whether a real (non-default) gate is installed. Pipe teeing — which adds a
/// thread + extra pipe per link — is only done when this is true (docs/NASH.md §5.3).
pub(crate) fn is_active() -> bool {
    GATE.get().is_some()
}

static PIPELINE_COUNTER: AtomicU64 = AtomicU64::new(1);

/// A fresh id correlating the links of one pipeline invocation.
pub(crate) fn next_pipeline_id() -> u64 {
    PIPELINE_COUNTER.fetch_add(1, Ordering::Relaxed)
}
