use chrono::Utc;
use serde::Serialize;

use crate::cli::ObligationKind;
use crate::error::AppError;
use crate::ledger::{is_initialized, marker, obligations, obligations::Obligation, state_dir};
use crate::output::{self, Ctx};

#[derive(Serialize)]
struct AddResult {
    id: String,
    claim: String,
    kind: String,
    critical: bool,
    proof_cmd: String,
}

/// POSIX single-quote a string for safe embedding in a shell command.
fn sh_quote(s: &str) -> String {
    format!("'{}'", s.replace('\'', "'\\''"))
}

/// Synthesise the proof command for a literal_match obligation:
/// `grep -F -- <quoted-literal> <quoted-file>`. `-F` treats the literal as a
/// fixed string (no regex). `--` ensures literals starting with `-` are not
/// parsed as grep flags.
fn synth_literal_match(literal: &str, file: &str) -> String {
    format!("grep -F -- {} {}", sh_quote(literal), sh_quote(file))
}

/// Synthesise the proof command for a literal_regex obligation:
/// `grep -E -- <quoted-pattern> <quoted-file>`. `-E` enables POSIX
/// extended-regex (no PCRE). `--` ensures patterns starting with `-` aren't
/// parsed as grep flags.
fn synth_literal_regex(pattern: &str, file: &str) -> String {
    format!("grep -E -- {} {}", sh_quote(pattern), sh_quote(file))
}

#[allow(clippy::too_many_arguments)]
pub fn run(
    ctx: Ctx,
    claim: String,
    proof: Option<String>,
    literal: Option<String>,
    regex: Option<String>,
    file: Option<String>,
    kind: ObligationKind,
    critical: bool,
    depends_on: Vec<String>,
) -> Result<(), AppError> {
    let cwd = std::env::current_dir()?;
    if !is_initialized(&cwd) {
        return Err(AppError::NotInitialized);
    }
    let dir = state_dir(&cwd);

    let claim = claim.trim().to_string();
    if claim.is_empty() {
        return Err(AppError::InvalidInput("claim cannot be empty".into()));
    }

    // Validate dependency paths: trim whitespace, drop empties, dedupe,
    // sort, reject paths that escape the repo root via `..` or absolute
    // paths. Existence is checked at prove/gate time so the obligation can
    // be added before the file lands.
    let depends_on = obligations::normalize_depends_on(&depends_on)?;

    // clap has already enforced exactly one of {proof, literal+file, regex+file}.
    // Only kind consistency is validated here.
    let proof_cmd = match (proof, literal.as_deref(), regex.as_deref(), file.as_deref()) {
        (Some(p), None, None, None) => {
            if matches!(
                kind,
                ObligationKind::LiteralMatch | ObligationKind::LiteralRegex
            ) {
                return Err(AppError::InvalidInput(format!(
                    "--kind {kind} requires --literal/--regex and --file, not --proof"
                )));
            }
            let p = p.trim().to_string();
            if p.is_empty() {
                return Err(AppError::InvalidInput(
                    "proof command cannot be empty".into(),
                ));
            }
            p
        }
        (None, Some(lit), None, Some(f)) => {
            if !matches!(kind, ObligationKind::LiteralMatch) {
                return Err(AppError::InvalidInput(
                    "--literal and --file require --kind literal_match".into(),
                ));
            }
            if lit.is_empty() {
                return Err(AppError::InvalidInput("--literal cannot be empty".into()));
            }
            if f.is_empty() {
                return Err(AppError::InvalidInput("--file cannot be empty".into()));
            }
            synth_literal_match(lit, f)
        }
        (None, None, Some(pat), Some(f)) => {
            if !matches!(kind, ObligationKind::LiteralRegex) {
                return Err(AppError::InvalidInput(
                    "--regex and --file require --kind literal_regex".into(),
                ));
            }
            if pat.is_empty() {
                return Err(AppError::InvalidInput("--regex cannot be empty".into()));
            }
            if f.is_empty() {
                return Err(AppError::InvalidInput("--file cannot be empty".into()));
            }
            synth_literal_regex(pat, f)
        }
        _ => unreachable!(
            "clap constraints guarantee exactly one of proof, literal+file, or regex+file"
        ),
    };

    // Id allocation + append happen under an exclusive ledger lock so two
    // concurrent `add` invocations (parallel agents) can't mint the same id.
    let ob = obligations::append_with_new_id(&dir, |id| Obligation {
        id,
        claim: claim.clone(),
        kind,
        critical,
        proof_cmd: proof_cmd.clone(),
        created_at: Utc::now(),
        depends_on: depends_on.clone(),
    })?;
    let id = ob.id;

    // A new critical obligation always reopens the contract. Written
    // unconditionally (not `if !exists`) so it can't race a concurrent
    // `gate` that removes the marker between our check and its removal.
    if critical {
        marker::create(
            &dir,
            &format!("ritalin: contract open — obligation {id} added, awaiting evidence\n"),
        )?;
    }

    let result = AddResult {
        id,
        claim,
        kind: kind.to_string(),
        critical,
        proof_cmd,
    };

    output::print_success_or(ctx, &result, |r| {
        use owo_colors::OwoColorize;
        let crit = if r.critical {
            "[critical]".red().to_string()
        } else {
            "[advisory]".dimmed().to_string()
        };
        println!(
            "{} {} {} {}",
            "+".green().bold(),
            r.id.bold(),
            crit,
            r.claim
        );
        println!("  proof: {}", r.proof_cmd.dimmed());
    });

    Ok(())
}

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

    #[test]
    fn sh_quote_simple() {
        assert_eq!(sh_quote("hello"), "'hello'");
    }

    #[test]
    fn sh_quote_with_single_quote() {
        assert_eq!(sh_quote("it's"), "'it'\\''s'");
    }

    #[test]
    fn sh_quote_empty() {
        assert_eq!(sh_quote(""), "''");
    }

    #[test]
    fn synth_includes_fixed_string_flag() {
        let cmd = synth_literal_match("rgba(0,0,0,0.5)", "src/theme.css");
        assert!(cmd.starts_with("grep -F -- "));
        assert!(cmd.contains("'rgba(0,0,0,0.5)'"));
        assert!(cmd.contains("'src/theme.css'"));
    }

    #[test]
    fn synth_quotes_single_quotes_and_spaces() {
        let cmd = synth_literal_match("it's a trap", "weird path.txt");
        assert_eq!(cmd, r#"grep -F -- 'it'\''s a trap' 'weird path.txt'"#);
    }

    #[test]
    fn synth_handles_literal_starting_with_dash() {
        // The `--` guard means literals like "-webkit-" don't parse as flags.
        let cmd = synth_literal_match("-webkit-font-smoothing", "src/a.css");
        assert!(cmd.contains("-- '-webkit-font-smoothing'"));
    }
}
