//! `ckg query <datalog>` — run an arbitrary Cozo script.
//!
//! ## Write semantics (security note)
//!
//! **Default: read-only.** Matches the MCP `query` tool's contract —
//! `:put` / `:rm` / `:create` / `:replace` are rejected at the Cozo
//! engine layer (`ScriptMutability::Immutable`). Pass `--write` to
//! opt into mutation.
//!
//! The default flipped in v0.2 (CR-I3). Previously the CLI defaulted
//! to mutable; any user pasting prompt-injected Datalog into `ckg
//! query "$DATALOG"` was one prompt-injection away from `:rm Symbol
//! {id: ...}`. The asymmetry with MCP (which was always Immutable)
//! was a footgun. Existing automation that runs `:put` from CLI must
//! now pass `--write` (or `--unsafe`).
//!
//! ## CLI-C2: `--write` confirmation gate
//!
//! `--write` now requires explicit confirmation unless `--yes` is also
//! passed:
//! - **TTY stdin**: prints a prompt and reads a line; aborts unless the
//!   response is `y` or `yes` (case-insensitive).
//! - **Non-TTY stdin** (piped / redirected / CI): aborts immediately
//!   with a non-zero exit — pass `--yes` to bypass in automation.
//!
//! The audit `eprintln!` and `tracing::warn!` are preserved regardless
//! of how confirmation is obtained.

use std::io::{IsTerminal as _, Write as _};
use std::path::PathBuf;

use anyhow::{bail, Result};
use clap::Args as ClapArgs;

#[derive(ClapArgs, Debug)]
pub struct Args {
    /// The Cozo Datalog script to run. Multi-line OK; quote with single quotes.
    ///
    /// Large result sets are capped at 100 MB. When truncated, a sentinel line
    /// `--TRUNCATED--` is appended to stdout (after the JSON) and a warning
    /// is printed to stderr. Downstream parsers should strip or detect this
    /// sentinel; `jq` will error on it, which is intentional — it signals
    /// that the output is incomplete. Add a `:limit N` clause to your Datalog
    /// to avoid truncation.
    pub datalog: String,
    /// Allow `:put` / `:rm` / `:create` / `:replace`. Default refuses
    /// mutation at the engine layer (matches the MCP `query` contract).
    /// Aliased as `--unsafe` for legibility in scripts that explicitly
    /// intend mutation.
    #[arg(long, alias = "unsafe")]
    pub write: bool,
    /// Skip the interactive confirmation prompt when `--write` is used.
    /// Required for non-TTY contexts (CI, piped input); without it,
    /// `--write` on a non-TTY stdin aborts immediately (CLI-C2).
    #[arg(long)]
    pub yes: bool,
    /// Deprecated: read-only is now the default. Accepted as a no-op
    /// for one release so existing automation doesn't break at upgrade.
    /// L6: scheduled for removal in v0.3 (next-major); tracked in
    /// CHANGELOG under CLI-breaking changes.
    #[arg(long, hide = true)]
    pub read_only: bool,
}

pub fn run(home: Option<PathBuf>, args: Args) -> Result<()> {
    let (_base, storage) = super::repo_lookup::open_for(home, None)?;
    if args.read_only {
        // CR-IMP-4: route to stderr directly so the deprecation reaches
        // users regardless of whether `ckg-cli` initialises a tracing
        // subscriber. `tracing::warn!` would be silent under the default
        // (RUST_LOG unset) configuration that most automation runs in.
        eprintln!(
            "ckg query: warning: --read-only is now the default; the flag is a no-op and will be removed in a future release. Pass --write to opt into mutation."
        );
    }
    let rows = if args.write {
        // CLI-C2: confirmation gate — require explicit consent before
        // executing a mutable query.
        if !args.yes {
            if std::io::stdin().is_terminal() {
                // Interactive: prompt the user and read a line.
                eprint!(
                    "ckg query: WARNING: --write will mutate the graph. \
                     Type 'yes' to proceed, anything else aborts: "
                );
                // CLI-M-3: flush stderr so the prompt is visible before
                // blocking on read_line. On some terminals and in CI
                // environments stderr is line-buffered; without an explicit
                // flush the user types blind.
                std::io::stderr().flush()?;
                let mut line = String::new();
                std::io::stdin().read_line(&mut line)?;
                let answer = line.trim().to_ascii_lowercase();
                if answer != "y" && answer != "yes" {
                    bail!("ckg query: aborted by user (did not confirm --write)");
                }
            } else {
                // Non-TTY without --yes: fail closed so piped automation
                // can't silently execute destructive queries.
                bail!(
                    "ckg query: --write requires --yes when stdin is not a TTY \
                     (pass --yes to allow mutation in non-interactive mode)"
                );
            }
        }
        // CLI-C2: audit trail — log mutable queries to stderr so operators
        // can trace destructive operations in daemon/automation contexts.
        eprintln!(
            "ckg query: WARNING: executing mutable query (--write). \
             Use --read-only (default) for safe queries."
        );
        tracing::warn!(target: "audit", script = %args.datalog, "executing mutable query");
        storage.run_mutable_unchecked(&args.datalog)?
    } else {
        storage.run_immutable(&args.datalog)?
    };
    let json = rows.into_json();
    // M4: cap stdout output at 100 MB to protect terminals and
    // piped consumers from a pathological result set. Pretty-print
    // first (so the truncation boundary is always at a char boundary),
    // then truncate if needed. Warn via stderr so structured callers
    // (e.g. shell scripts) can detect the condition.
    const MAX_OUTPUT_BYTES: usize = 100 * 1024 * 1024; // 100 MB
    let rendered = serde_json::to_string_pretty(&json)?;
    if rendered.len() > MAX_OUTPUT_BYTES {
        eprintln!(
            "ckg query: WARNING: output truncated at 100 MB ({} bytes total). \
             Add a `:limit` clause to your Datalog query to avoid truncation.",
            rendered.len()
        );
        // Truncate on a UTF-8 char boundary: serde_json emits identifiers
        // and string values verbatim, so the cap can land mid-codepoint
        // (e.g. CJK paths). Walk back to the nearest boundary <= cap.
        let mut cut = MAX_OUTPUT_BYTES;
        while cut > 0 && !rendered.is_char_boundary(cut) {
            cut -= 1;
        }
        // H7: append a grep-able stdout sentinel so downstream parsers
        // (jq, shell pipelines) detect incomplete output rather than
        // silently processing a truncated JSON document.
        // `jq` will error on the `--TRUNCATED--` line — this is intentional.
        // Strip with: `ckg query ... | grep -v '^--TRUNCATED--$' | jq`.
        println!("{}", &rendered[..cut]);
        println!("--TRUNCATED-- full_bytes={}", rendered.len());
    } else {
        println!("{rendered}");
    }
    Ok(())
}
