use anyhow::{Context, Result};
use corcept_contract::validate_value;
use corcept_doctrine::{default_documents, validate as validate_doctrine};
use corcept_guards::{
    evaluate_pre_tool, evaluate_stop, extract_command, extract_path, StopVerdict,
};
use corcept_ledger::{ensure_ledger, read_events, verify_hash_chain_readonly};
use corcept_memory::ensure_dirs as ensure_memory_dirs;
use corcept_sink::{build_ledger_event, SinkDispatcher, SinkRecord};
use corcept_types::{
    dir_permissions_secure, operator_data_dir, project_ledger_dir, transition_for, AuthorityLevel,
    CorceptConfig, HookEnvelope, HookOutput, LedgerEventKind,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitOptions {
    pub path: PathBuf,
    pub dry_run: bool,
    pub force: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitReport {
    pub path: PathBuf,
    pub dry_run: bool,
    pub created: Vec<PathBuf>,
    pub modified: Vec<PathBuf>,
    pub skipped: Vec<PathBuf>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckResult {
    pub name: String,
    pub status: String,
    pub detail: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DoctorOptions {
    #[serde(default)]
    pub validate_perms: bool,
    #[serde(default)]
    pub strict: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DoctorReport {
    pub status: String,
    pub checks: Vec<CheckResult>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
    pub status: String,
    pub event_count: usize,
    pub hash_chain_valid: bool,
    pub last_event: Option<String>,
    pub warnings: Vec<String>,
}

pub fn init_project(options: InitOptions) -> Result<InitReport> {
    let root = options.path;
    let mut report = InitReport {
        path: root.clone(),
        dry_run: options.dry_run,
        created: vec![],
        modified: vec![],
        skipped: vec![],
        warnings: vec![],
    };

    plan_dir(&root.join(".claude"), &mut report, options.dry_run)?;
    plan_dir(
        &root.join(".corcept").join("doctrine"),
        &mut report,
        options.dry_run,
    )?;
    plan_dir(
        &root.join(".corcept").join("memory").join("accepted"),
        &mut report,
        options.dry_run,
    )?;
    plan_dir(
        &root.join(".corcept").join("memory").join("candidates"),
        &mut report,
        options.dry_run,
    )?;
    plan_dir(
        &root.join(".corcept").join("memory").join("rejected"),
        &mut report,
        options.dry_run,
    )?;
    plan_dir(
        &root.join(".corcept").join("ledger"),
        &mut report,
        options.dry_run,
    )?;
    plan_dir(
        &root.join(".corcept").join("reports"),
        &mut report,
        options.dry_run,
    )?;

    write_file(
        &root.join(".corcept").join("config.yaml"),
        &serde_yaml::to_string(&CorceptConfig::default())?,
        options.force,
        options.dry_run,
        &mut report,
    )?;
    write_file(
        &root.join(".claude").join("CLAUDE.md"),
        render_claude_md(),
        options.force,
        options.dry_run,
        &mut report,
    )?;
    write_file(
        &root.join(".claude").join("settings.json"),
        &render_project_settings()?,
        options.force,
        options.dry_run,
        &mut report,
    )?;

    for (name, content) in default_documents() {
        write_file(
            &root.join(".corcept").join("doctrine").join(name),
            content,
            options.force,
            options.dry_run,
            &mut report,
        )?;
    }

    write_file(
        &root
            .join(".corcept")
            .join("memory")
            .join("accepted")
            .join("README.md"),
        "# Accepted Memory\n\nApproved project memory lives here.\n",
        options.force,
        options.dry_run,
        &mut report,
    )?;
    write_file(
        &root
            .join(".corcept")
            .join("memory")
            .join("candidates")
            .join("README.md"),
        "# Candidate Memory\n\nEvidence-backed proposed memories live here until promoted.\n",
        options.force,
        options.dry_run,
        &mut report,
    )?;
    write_file(
        &root
            .join(".corcept")
            .join("memory")
            .join("rejected")
            .join("README.md"),
        "# Rejected Memory\n\nRejected or superseded memory candidates live here.\n",
        options.force,
        options.dry_run,
        &mut report,
    )?;

    if !options.dry_run {
        ensure_ledger(&root)?;
        ensure_memory_dirs(&root)?;
    } else {
        report
            .created
            .push(root.join(".corcept").join("ledger").join("events.jsonl"));
    }

    Ok(report)
}

fn plan_dir(path: &Path, report: &mut InitReport, dry_run: bool) -> Result<()> {
    if path.exists() {
        return Ok(());
    }
    report.created.push(path.to_path_buf());
    if !dry_run {
        fs::create_dir_all(path)
            .with_context(|| format!("creating directory {}", path.display()))?;
    }
    Ok(())
}

fn write_file(
    path: &Path,
    content: &str,
    force: bool,
    dry_run: bool,
    report: &mut InitReport,
) -> Result<()> {
    if path.exists() && !force {
        report.skipped.push(path.to_path_buf());
        return Ok(());
    }
    if path.exists() {
        report.modified.push(path.to_path_buf());
    } else {
        report.created.push(path.to_path_buf());
    }
    if !dry_run {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(path, content).with_context(|| format!("writing {}", path.display()))?;
    }
    Ok(())
}

pub fn load_config(root: impl AsRef<Path>) -> Result<CorceptConfig> {
    let path = root.as_ref().join(".corcept").join("config.yaml");
    if !path.exists() {
        return Ok(CorceptConfig::default());
    }
    let raw =
        fs::read_to_string(&path).with_context(|| format!("reading config {}", path.display()))?;
    Ok(serde_yaml::from_str(&raw)?)
}

pub fn doctor(path: impl AsRef<Path>) -> Result<DoctorReport> {
    doctor_with_options(path, DoctorOptions::default())
}

pub fn doctor_with_options(path: impl AsRef<Path>, options: DoctorOptions) -> Result<DoctorReport> {
    let root = path.as_ref();
    let mut checks = Vec::new();
    push_check(
        &mut checks,
        "config",
        root.join(".corcept/config.yaml").exists(),
        "Project config exists",
    );
    push_check(
        &mut checks,
        "claude_md",
        root.join(".claude/CLAUDE.md").exists(),
        "Claude project instructions exist",
    );
    push_check(
        &mut checks,
        "ledger",
        root.join(".corcept/ledger/events.jsonl").exists(),
        "Ledger exists",
    );
    push_check(
        &mut checks,
        "memory",
        root.join(".corcept/memory").exists(),
        "Memory directory exists",
    );

    let doctrine_warnings = validate_doctrine(root)
        .unwrap_or_else(|err| vec![format!("Doctrine validation failed: {err}")]);
    push_check(
        &mut checks,
        "doctrine",
        doctrine_warnings.is_empty(),
        if doctrine_warnings.is_empty() {
            "Doctrine validates"
        } else {
            "Doctrine warnings present"
        },
    );

    let hash_valid = verify_hash_chain_readonly(root).unwrap_or(false);
    push_check(
        &mut checks,
        "ledger_hash_chain",
        hash_valid,
        "Ledger hash chain verifies",
    );

    if options.strict {
        let schema_ok = validate_ledger_schema(root);
        push_check(
            &mut checks,
            "ledger_schema",
            schema_ok,
            "Ledger events validate against corcept.ledger_event.v1 schema",
        );
    }

    if options.validate_perms {
        let ledger_dir = project_ledger_dir(root);
        let ledger_perms = dir_permissions_secure(&ledger_dir);
        push_check(
            &mut checks,
            "ledger_dir_perms",
            ledger_perms,
            "Project ledger directory is owner-only (0700) or absent",
        );
        if let Some(op_data) = operator_data_dir() {
            if op_data.exists() {
                let op_perms = dir_permissions_secure(&op_data);
                push_check(
                    &mut checks,
                    "operator_data_dir_perms",
                    op_perms,
                    "Operator data directory is owner-only (0700)",
                );
            }
        }
    }

    let all_pass = checks.iter().all(|check| check.status == "pass");
    let status = if all_pass {
        "pass"
    } else if options.strict {
        "fail"
    } else {
        "warn"
    }
    .to_string();
    Ok(DoctorReport { status, checks })
}

fn validate_ledger_schema(root: &Path) -> bool {
    let Ok(events) = read_events(root) else {
        return false;
    };
    for event in events {
        let Ok(value) = serde_json::to_value(&event) else {
            return false;
        };
        if validate_value("corcept-ledger-event-v1.schema.json", &value).is_err() {
            return false;
        }
    }
    true
}

fn push_check(checks: &mut Vec<CheckResult>, name: &str, pass: bool, detail: &str) {
    checks.push(CheckResult {
        name: name.to_string(),
        status: if pass { "pass" } else { "warn" }.to_string(),
        detail: detail.to_string(),
    });
}

pub fn audit(path: impl AsRef<Path>) -> Result<AuditReport> {
    let events = read_events(&path).unwrap_or_default();
    let hash_chain_valid = verify_hash_chain_readonly(&path).unwrap_or(false);
    let mut warnings = Vec::new();
    if !hash_chain_valid {
        warnings.push("Ledger hash chain is invalid or ledger is missing.".to_string());
    }
    let last_event = events.last().map(|event| event.event_type.clone());
    Ok(AuditReport {
        status: if warnings.is_empty() { "pass" } else { "warn" }.to_string(),
        event_count: events.len(),
        hash_chain_valid,
        last_event,
        warnings,
    })
}

pub fn handle_hook(raw_json: &str, command: &str) -> Result<HookOutput> {
    let input: HookEnvelope = serde_json::from_str(raw_json).context("parsing hook input JSON")?;
    let cwd = input.cwd.clone().unwrap_or(std::env::current_dir()?);
    let config = load_config(&cwd).unwrap_or_default();

    match command {
        "session-start" => {
            append_hook_event(
                &cwd,
                &input,
                "session-start",
                LedgerEventKind::SessionStarted,
                AuthorityLevel::L0Observe,
                None,
                Some("allow"),
                Some("Session started"),
            )?;
            Ok(HookOutput::context("SessionStart", "CORCEPT active: doctrine, memory, guard, and audit policy loaded. Use CORCEPT skills for structured workflows."))
        }
        "user-prompt-submit" => {
            let prompt = input.prompt.clone().unwrap_or_default();
            append_hook_event(
                &cwd,
                &input,
                "user-prompt-submit",
                LedgerEventKind::PromptSubmitted,
                AuthorityLevel::L0Observe,
                None,
                Some("allow"),
                Some("Prompt received"),
            )?;
            let context = classify_prompt_context(&prompt);
            Ok(HookOutput::context("UserPromptSubmit", context))
        }
        "pretool-guard" => {
            let verdict = evaluate_pre_tool(&input, &config);
            let target = extract_path(input.tool_input.as_ref())
                .or_else(|| extract_command(input.tool_input.as_ref()));
            append_hook_event(
                &cwd,
                &input,
                "pretool-guard",
                LedgerEventKind::ToolRequested,
                verdict.authority_level,
                target,
                Some(&verdict.decision.to_string()),
                Some(&verdict.reason),
            )?;
            Ok(verdict.to_hook_output())
        }
        "posttool-audit" => {
            let event_kind = classify_posttool_event(&input);
            let decision = classify_posttool_decision(&input);
            let target = extract_path(input.tool_input.as_ref())
                .or_else(|| extract_command(input.tool_input.as_ref()));
            append_hook_event(
                &cwd,
                &input,
                "posttool-audit",
                event_kind,
                AuthorityLevel::L3ExecuteLocal,
                target,
                Some(&decision),
                Some("PostToolUse audited"),
            )?;
            Ok(HookOutput::context(
                "PostToolUse",
                "CORCEPT audited the completed tool call.",
            ))
        }
        "stop-check" => match evaluate_stop(&cwd, input.stop_hook_active.unwrap_or(false)) {
            StopVerdict::Allow(reason) => {
                append_hook_event(
                    &cwd,
                    &input,
                    "stop-check",
                    LedgerEventKind::StopAllowed,
                    AuthorityLevel::L0Observe,
                    None,
                    Some("allow"),
                    Some(&reason),
                )?;
                Ok(HookOutput::default())
            }
            StopVerdict::Block(reason) => {
                append_hook_event(
                    &cwd,
                    &input,
                    "stop-check",
                    LedgerEventKind::StopBlocked,
                    AuthorityLevel::L0Observe,
                    None,
                    Some("block"),
                    Some(&reason),
                )?;
                Ok(HookOutput::block(reason))
            }
        },
        other => Ok(HookOutput::block(format!(
            "Unknown CORCEPT hook command: {other}"
        ))),
    }
}

#[allow(clippy::too_many_arguments)]
fn append_hook_event(
    root: &Path,
    input: &HookEnvelope,
    command: &str,
    kind: LedgerEventKind,
    authority_level: AuthorityLevel,
    target: Option<String>,
    decision: Option<&str>,
    reason: Option<&str>,
) -> Result<()> {
    let transition = transition_for(command, kind, decision);
    let mut metadata = BTreeMap::new();
    metadata.insert(
        "transition_id".to_string(),
        serde_json::Value::String(transition.id().to_string()),
    );
    if let Some(tool_input) = &input.tool_input {
        metadata.insert("tool_input".to_string(), sanitize_value(tool_input));
    }
    let event = build_ledger_event(
        input.session_id.clone(),
        input
            .agent_type
            .clone()
            .unwrap_or_else(|| "corcept-runtime".to_string()),
        kind,
        authority_level,
        input.tool_name.clone(),
        target,
        decision.map(ToOwned::to_owned),
        reason.map(ToOwned::to_owned),
        metadata,
    );
    let correlation = input
        .session_id
        .clone()
        .unwrap_or_else(|| "unknown".to_string());
    let outcome = decision.unwrap_or("recorded");
    let record = SinkRecord::new(correlation, kind, outcome);
    let dispatcher = SinkDispatcher::hook_default(root);
    dispatcher.emit_all(&record, Some(&event))?;
    Ok(())
}

fn sanitize_value(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let sanitized = map
                .iter()
                .map(|(key, value)| {
                    let lower = key.to_ascii_lowercase();
                    if lower.contains("token")
                        || lower.contains("secret")
                        || lower.contains("password")
                        || lower.contains("key")
                    {
                        (key.clone(), Value::String("[REDACTED]".to_string()))
                    } else {
                        (key.clone(), sanitize_value(value))
                    }
                })
                .collect();
            Value::Object(sanitized)
        }
        Value::Array(values) => Value::Array(values.iter().map(sanitize_value).collect()),
        other => other.clone(),
    }
}

fn classify_prompt_context(prompt: &str) -> String {
    let lower = prompt.to_ascii_lowercase();
    if [
        "deploy",
        "prod",
        "production",
        "secret",
        "token",
        "auth",
        "billing",
        "migration",
    ]
    .iter()
    .any(|needle| lower.contains(needle))
    {
        "CORCEPT: This prompt appears security- or side-effect-sensitive. Apply doctrine, require concrete evidence, and treat production/external actions as L4.".to_string()
    } else {
        "CORCEPT: Use bounded diffs, explicit assumptions, and evidence-backed completion."
            .to_string()
    }
}

fn classify_posttool_event(input: &HookEnvelope) -> LedgerEventKind {
    match input.tool_name.as_deref().unwrap_or_default() {
        "Edit" | "Write" | "MultiEdit" | "NotebookEdit" => LedgerEventKind::FileModified,
        "Bash" => {
            let command = extract_command(input.tool_input.as_ref()).unwrap_or_default();
            if is_test_command(&command) {
                LedgerEventKind::TestRun
            } else {
                LedgerEventKind::CommandExecuted
            }
        }
        _ => LedgerEventKind::ToolCompleted,
    }
}

fn is_test_command(command: &str) -> bool {
    let normalized = command
        .split_whitespace()
        .collect::<Vec<_>>()
        .join(" ")
        .to_ascii_lowercase();
    let test_prefixes = [
        "cargo test",
        "cargo nextest",
        "npm test",
        "npm run test",
        "pnpm test",
        "pnpm run test",
        "yarn test",
        "bun test",
        "pytest",
        "python -m pytest",
        "python3 -m pytest",
        "go test",
        "mvn test",
        "gradle test",
        "./gradlew test",
    ];
    test_prefixes
        .iter()
        .any(|prefix| normalized == *prefix || normalized.starts_with(&format!("{prefix} ")))
}

fn classify_posttool_decision(input: &HookEnvelope) -> String {
    let exit_code = input
        .tool_response
        .as_ref()
        .and_then(|value| value.get("exit_code"))
        .and_then(|value| value.as_i64());
    match exit_code {
        Some(0) => "pass".to_string(),
        Some(_) => "fail".to_string(),
        None => "recorded".to_string(),
    }
}

fn render_project_settings() -> Result<String> {
    let value = json!({
        "hooks": {
            "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "corcept hook session-start" }] }],
            "UserPromptSubmit": [{ "matcher": "", "hooks": [{ "type": "command", "command": "corcept hook user-prompt-submit" }] }],
            "PreToolUse": [{ "matcher": "Bash|Read|Grep|Glob|Edit|Write|MultiEdit|NotebookEdit|WebFetch|WebSearch", "hooks": [{ "type": "command", "command": "corcept hook pretool-guard" }] }],
            "PostToolUse": [{ "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit", "hooks": [{ "type": "command", "command": "corcept hook posttool-audit" }] }],
            "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "corcept hook stop-check" }] }]
        }
    });
    Ok(serde_json::to_string_pretty(&value)?)
}

fn render_claude_md() -> &'static str {
    r#"# CORCEPT Project Instructions

You are operating inside an CORCEPT-governed project.

## Authority

Follow this precedence:

1. Direct user instruction for the current task.
2. Active CORCEPT doctrine.
3. Accepted CORCEPT memory.
4. This file.
5. Skill or agent-local instructions.

Do not promote memory or doctrine without explicit approval.

## Operating rules

- State assumptions before acting when scope is unclear.
- Prefer bounded diffs.
- Do not edit files outside approved task scope.
- Do not claim tests passed unless you ran them or the user provided evidence.
- Treat secrets as unreadable; identify their presence only.
- Use CORCEPT skills for structured workflows.

## Required evidence

For completed coding work, report files changed, tests run, test result, known untested risks, and unresolved issues.
"#
}

#[cfg(test)]
mod tests {
    use super::*;
    use corcept_types::LedgerEventKind;
    use serde_json::{json, Value};
    use std::fs;
    use std::path::PathBuf;

    fn repo_root() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("../..")
            .canonicalize()
            .unwrap()
    }

    fn load_fixture(name: &str, cwd: &Path) -> String {
        let raw = fs::read_to_string(repo_root().join("tests/fixtures/hooks").join(name))
            .unwrap_or_else(|_| panic!("fixture {name}"));
        let mut value: Value = serde_json::from_str(&raw).expect("fixture json");
        value["cwd"] = Value::String(cwd.to_string_lossy().into_owned());
        serde_json::to_string(&value).expect("fixture json string")
    }

    fn init_temp_project() -> tempfile::TempDir {
        let dir = tempfile::tempdir().unwrap();
        init_project(InitOptions {
            path: dir.path().to_path_buf(),
            dry_run: false,
            force: false,
        })
        .unwrap();
        dir
    }

    #[test]
    fn dry_run_does_not_write() {
        let dir = tempfile::tempdir().unwrap();
        let target = dir.path().join("project");
        let report = init_project(InitOptions {
            path: target.clone(),
            dry_run: true,
            force: false,
        })
        .unwrap();
        assert!(report.created.iter().any(|p| p.ends_with("config.yaml")));
        assert!(!target.exists());
    }

    #[test]
    fn hook_denies_dangerous_command() {
        let dir = tempfile::tempdir().unwrap();
        init_project(InitOptions {
            path: dir.path().to_path_buf(),
            dry_run: false,
            force: false,
        })
        .unwrap();
        let input = json!({
            "session_id":"s",
            "transcript_path":"/tmp/t.jsonl",
            "cwd": dir.path(),
            "hook_event_name":"PreToolUse",
            "tool_name":"Bash",
            "tool_input":{"command":"rm -rf /"},
            "tool_use_id":"t"
        });
        let out = handle_hook(&input.to_string(), "pretool-guard").unwrap();
        let json = serde_json::to_string(&out).unwrap();
        assert!(json.contains("deny"));
    }

    #[test]
    fn hook_fixture_pretool_denies_rm_rf() {
        run_hook_fixture("pretool-bash-rm-rf.json", "pretool-guard", |out| {
            assert!(out.contains("deny"));
        });
    }

    #[test]
    fn hook_fixture_pretool_denies_env_read() {
        run_hook_fixture("pretool-read-env.json", "pretool-guard", |out| {
            assert!(out.contains("deny"));
        });
    }

    #[test]
    fn hook_fixture_pretool_asks_npm_install() {
        run_hook_fixture("pretool-bash-npm-install.json", "pretool-guard", |out| {
            assert!(out.contains("ask"));
        });
    }

    #[test]
    fn hook_fixture_pretool_allows_safe_echo() {
        run_hook_fixture("pretool-bash-safe.json", "pretool-guard", |out| {
            assert!(!out.contains("deny") && !out.contains("ask"));
        });
    }

    #[test]
    fn hook_fixture_posttool_records_event() {
        run_hook_fixture("posttool-npm-test.json", "posttool-audit", |_| {});
    }

    #[test]
    fn hook_fixture_session_start_writes_versioned_event() {
        let dir = init_temp_project();
        let raw = load_fixture("session-start.json", dir.path());
        handle_hook(&raw, "session-start").unwrap();
        let events = read_events(dir.path()).unwrap();
        assert!(events.iter().any(|e| {
            LedgerEventKind::SessionStarted.matches_str(&e.event_type)
                && e.metadata.get("transition_id").and_then(|v| v.as_str())
                    == Some("T010_session_start")
        }));
    }

    #[test]
    fn hook_fixture_stop_check_allows_clean_project() {
        run_hook_fixture("stop-check.json", "stop-check", |out| {
            assert!(!out.contains("block"));
        });
    }

    fn run_hook_fixture(name: &str, command: &str, assert_out: impl FnOnce(&str)) {
        let dir = init_temp_project();
        let raw = load_fixture(name, dir.path());
        let out = handle_hook(&raw, command).unwrap();
        let json = serde_json::to_string(&out).unwrap();
        assert_out(&json);
        assert!(!read_events(dir.path()).unwrap().is_empty());
    }
}
