use crate::{
    invocation::{SkillContext, SkillInput},
    outcome::SkillOutcome,
    skill::{Skill, SkillId},
};
use forge::{
    budget::estimator::TokenEstimator,
    signal::capture::RawSignal,
    substrate::{local::LocalSubstrate, Substrate, SubstrateCommand},
    tracker::store::ExecutionStore,
};
use serde_json::json;

pub struct ScoutSkill;

impl Skill for ScoutSkill {
    fn id(&self) -> SkillId {
        SkillId("scout")
    }
    fn display_name(&self) -> &str {
        "Scout"
    }

    fn input_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "command":      { "type": "string" },
                "working_dir":  { "type": "string" },
                "timeout_secs": { "type": "integer", "default": 30 },
                "dry_run":      { "type": "boolean", "default": false }
            },
            "required": ["command"]
        })
    }

    fn output_schema(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "stdout":         { "type": "string" },
                "stderr":         { "type": "string" },
                "exit_code":      { "type": "integer" },
                "tokens_before":  { "type": "integer" },
                "tokens_after":   { "type": "integer" },
                "savings_pct":    { "type": "number" }
            }
        })
    }

    fn execute(&self, input: SkillInput, ctx: &SkillContext) -> anyhow::Result<SkillOutcome> {
        let cmd_str = input.get_str("command").unwrap_or("").to_string();
        let dry_run = input.get_bool("dry_run").unwrap_or(false);
        let timeout = input.get_u64("timeout_secs").unwrap_or(30);
        let cwd = input
            .get_str("working_dir")
            .unwrap_or(&ctx.working_dir)
            .to_string();

        if dry_run {
            return Ok(SkillOutcome::success(
                "scout",
                json!({
                    "stdout": "[dry-run]", "stderr": "", "exit_code": 0,
                    "tokens_before": 0, "tokens_after": 0, "savings_pct": 0.0
                }),
            ));
        }

        // Commands with shell operators need sh -c
        let (program, args) = if has_shell_ops(&cmd_str) {
            ("sh".to_string(), vec!["-c".to_string(), cmd_str.clone()])
        } else {
            let words = shell_split(&cmd_str);
            if words.is_empty() {
                anyhow::bail!("empty command");
            }
            (words[0].clone(), words[1..].to_vec())
        };

        let sub_cmd = SubstrateCommand::new(&program)
            .args(args.clone())
            .cwd(cwd)
            .timeout(timeout);

        match LocalSubstrate.execute(sub_cmd) {
            Ok(result) => {
                let duration_ms = result.duration_ms;
                let signal = RawSignal::from_result(&program, args.clone(), result);
                let raw = signal.combined_output();
                let tokens_before = TokenEstimator::count(&raw);

                let mesh = weave::FilterMesh::default_mesh();
                let lens_ctx =
                    weave::lenses::LensContext::new(4000).with_exit_code(signal.exit_code);
                let lens_out = mesh.apply(&program, &args, &raw, &lens_ctx);
                let tokens_after = lens_out.tokens_after;
                let savings_pct = TokenEstimator::savings_pct(tokens_before, tokens_after);

                // Secret redaction — runs after compression, before content reaches the agent.
                // Skip if BCTX_NO_REDACT=1 (intentional secret sharing) or recipe opts out.
                let content = if std::env::var("BCTX_NO_REDACT").as_deref() == Ok("1")
                    || !mesh.scan_secrets_enabled(&program, &args)
                {
                    lens_out.content.clone()
                } else {
                    weave::SecretScanner::new().scan(&lens_out.content)
                };

                // Record in execution tracker for Chronicler / Conductor
                let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
                let db_path = std::path::PathBuf::from(&home)
                    .join(".bctx")
                    .join("executions.db");
                if let Ok(store) = ExecutionStore::open(&db_path) {
                    let display_cmd = if has_shell_ops(&cmd_str) {
                        &cmd_str
                    } else {
                        &program
                    };
                    let rec = ExecutionStore::new_record(
                        display_cmd,
                        &if has_shell_ops(&cmd_str) {
                            args.clone()
                        } else {
                            args[..].to_vec()
                        },
                        tokens_before,
                        tokens_after,
                        signal.exit_code,
                        duration_ms,
                    );
                    let _ = store.record(&rec);
                }

                Ok(SkillOutcome::success(
                    "scout",
                    json!({
                        "stdout":        content,
                        "stderr":        signal.stderr,
                        "exit_code":     signal.exit_code,
                        "tokens_before": tokens_before,
                        "tokens_after":  tokens_after,
                        "savings_pct":   savings_pct
                    }),
                )
                .with_tokens(tokens_after))
            }
            Err(e) => Ok(SkillOutcome::success(
                "scout",
                json!({
                    "stdout": "", "stderr": e.to_string(), "exit_code": -1,
                    "tokens_before": 0, "tokens_after": 0, "savings_pct": 0.0
                }),
            )),
        }
    }
}

fn has_shell_ops(s: &str) -> bool {
    s.contains('|')
        || s.contains('>')
        || s.contains('<')
        || s.contains("&&")
        || s.contains("||")
        || s.contains(';')
}

/// Minimal shell-word split: respects single/double quotes, no escape sequences.
fn shell_split(s: &str) -> Vec<String> {
    let mut words = Vec::new();
    let mut current = String::new();
    let mut in_single = false;
    let mut in_double = false;

    for ch in s.chars() {
        match ch {
            '\'' if !in_double => in_single = !in_single,
            '"' if !in_single => in_double = !in_double,
            ' ' | '\t' if !in_single && !in_double => {
                if !current.is_empty() {
                    words.push(std::mem::take(&mut current));
                }
            }
            _ => current.push(ch),
        }
    }
    if !current.is_empty() {
        words.push(current);
    }
    words
}

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

    #[test]
    fn splits_simple() {
        assert_eq!(
            shell_split("git log --oneline -20"),
            ["git", "log", "--oneline", "-20"]
        );
    }

    #[test]
    fn respects_quotes() {
        assert_eq!(
            shell_split(r#"echo "hello world""#),
            ["echo", "hello world"]
        );
    }

    #[test]
    fn single_word() {
        assert_eq!(shell_split("ls"), ["ls"]);
    }
}
