use crate::{code_scanner, config, discovery, parser, store};
use serde_json::Value;

pub fn run() -> Result<(), String> {
    let cfg = config::Config::load()?;

    println!("  Scanning for instruction files...");
    let files = discovery::discover(&cfg)?;

    if files.is_empty() {
        println!("  No instruction files found.");
        return Ok(());
    }

    for f in &files {
        let line_count = f.content.lines().count();
        println!(
            "    \u{2713} {} ({line_count} lines, {})",
            display_path(&f.path, &cfg),
            f.source_type
        );
    }

    println!("\n  Extracting rules...");
    let db = store::Store::open(&cfg.db_path())?;

    let mut total_rules = 0;
    for file in &files {
        let triples = parser::extract_rules(&file.content, &file.source_type, file.confidence);
        let count = triples.len();
        db.upsert_file(&file.path, &file.content, &triples, &file.source_type)
            .map_err(|e| e.to_string())?;
        total_rules += count;
    }
    println!("    \u{2713} {total_rules} rules extracted");

    let classified = db.classify_all_guardrails().map_err(|e| e.to_string())?;
    println!("    \u{2713} {classified} rules classified by intent");

    // Auto-enrich if the model is already downloaded
    let model_dir = cfg.arai_base_dir.join("models").join("all-MiniLM-L6-v2");
    if model_dir.join("model.onnx").exists() {
        match crate::enrich::enrich_guardrails(&db, &cfg.arai_base_dir) {
            Ok(n) => println!("    \u{2713} {n} rules enriched by model"),
            Err(e) => eprintln!("    \u{26a0} Enrichment failed: {e}"),
        }
    }

    db.set_meta(
        "last_scan",
        &format!(
            "{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs()
        ),
    )
    .map_err(|e| e.to_string())?;

    println!("\n  Scanning source code for imports...");
    let imports = code_scanner::scan_project(&cfg.project_root);
    let import_count = imports.len();
    db.upsert_code_graph(&imports).map_err(|e| e.to_string())?;
    let tool_count = db.code_graph_tool_count().map_err(|e| e.to_string())?;
    let file_count = db.code_graph_file_count().map_err(|e| e.to_string())?;
    println!(
        "    \u{2713} {import_count} imports from {file_count} files, {tool_count} unique tools"
    );

    println!("\n  Setting up hooks...");
    inject_hooks(&cfg)?;
    println!("    \u{2713} .claude/settings.json updated");

    // Also register for native Grok TUI support (if possible).
    if let Err(e) = inject_grok_hooks(&cfg) {
        // Non-fatal for now — many users will still get value via the
        // .claude/settings.json compatibility layer that Grok loads.
        eprintln!("    \u{26a0} Grok native hook registration skipped: {e}");
    } else {
        println!("    \u{2713} .grok/hooks/arai.json updated (native Grok Build)");
        println!("      Grok Build gates project hooks behind trust: run /hooks-trust in the");
        println!("      Grok session (or launch with --trust) the first time, or Arai's hooks");
        println!("      stay inactive on the native path.");
    }

    // Track init event + flush queued telemetry
    let enrichment = if model_dir.join("model.onnx").exists() {
        "model"
    } else {
        "taxonomy"
    };
    crate::telemetry::track_init(
        &cfg.arai_base_dir,
        total_rules,
        files.len(),
        db.code_graph_tool_count().unwrap_or(0),
        enrichment,
    );
    crate::telemetry::flush(&cfg.arai_base_dir);

    println!("\n  Arai is enforcing this project's rules (Claude Code and Grok TUI).");
    Ok(())
}

/// Remove all Arai hooks from .claude/settings.json.
pub fn deinit() -> Result<(), String> {
    let cfg = config::Config::load()?;
    let settings_path = cfg.claude_settings_path();

    if !settings_path.exists() {
        println!("  No .claude/settings.json found. Nothing to remove.");
        return Ok(());
    }

    let content = std::fs::read_to_string(&settings_path)
        .map_err(|e| format!("Could not read settings.json: {e}"))?;
    let mut settings: Value = serde_json::from_str(&content)
        .map_err(|e| format!("Could not parse settings.json: {e}"))?;

    let mut removed = 0;

    if let Some(hooks) = settings.get_mut("hooks").and_then(|v| v.as_object_mut()) {
        for (_event, entries) in hooks.iter_mut() {
            if let Some(arr) = entries.as_array_mut() {
                let before = arr.len();
                arr.retain(|entry| {
                    if let Some(hooks_arr) = entry.get("hooks").and_then(|v| v.as_array()) {
                        !hooks_arr.iter().any(|h| {
                            h.get("command")
                                .and_then(|v| v.as_str())
                                .map(|cmd| cmd.contains("arai"))
                                .unwrap_or(false)
                        })
                    } else {
                        true
                    }
                });
                removed += before - arr.len();
            }
        }
    }

    let output = serde_json::to_string_pretty(&settings)
        .map_err(|e| format!("Could not serialize settings: {e}"))?;
    std::fs::write(&settings_path, output)
        .map_err(|e| format!("Could not write settings.json: {e}"))?;

    if removed > 0 {
        println!("  Removed {removed} Arai hook(s) from .claude/settings.json");
    } else {
        println!("  No Arai hooks found in .claude/settings.json");
    }

    // Best-effort cleanup of native Grok registration
    let grok_hook_file = cfg.grok_hooks_dir().join("arai.json");
    if grok_hook_file.exists() {
        match std::fs::remove_file(&grok_hook_file) {
            Ok(_) => println!("  Removed Arai native hook file from .grok/hooks/"),
            Err(e) => eprintln!("  Warning: could not remove Grok hook file: {e}"),
        }
    }

    println!("  Arai is no longer enforcing this project's rules.");
    Ok(())
}

/// Hook events Arai registers itself into and the `matcher` pattern for
/// each.  Tool-call events (`PreToolUse`/`PostToolUse`/`UserPromptSubmit`)
/// use an empty matcher — Arai's own skip-tool list filters; Claude Code
/// shouldn't pre-filter those.  `FileChanged` uses a narrow basename
/// matcher so we don't spawn an Arai process on every unrelated file
/// edit during a build; `InstructionsLoaded` uses an empty matcher
/// because its payload already names a specific (and small) set of
/// rule-related files Claude Code loaded into context.
///
/// Path-anchored rule files (`.claude/rules/*.md`, `.cursor/rules/*.md`)
/// aren't expressible in Claude Code's literal-pipe-separated matcher
/// syntax — `InstructionsLoaded` catches their reload via the
/// in-process filter in `hooks::is_instruction_file`.
const ARAI_HOOK_REGISTRATIONS: &[(&str, &str)] = &[
    ("PreToolUse", ""),
    ("PostToolUse", ""),
    ("UserPromptSubmit", ""),
    (
        "FileChanged",
        "CLAUDE.md|.cursorrules|.windsurfrules|copilot-instructions.md",
    ),
    ("InstructionsLoaded", ""),
    // CwdChanged: monorepo navigation.  No matcher — every cd matters
    // because we may be landing in a never-scanned subpackage.
    ("CwdChanged", ""),
    // PostToolBatch: parallel-tool compliance correlation.  No matcher
    // — Arai's own skip-tool list filters per-tool inside the handler.
    ("PostToolBatch", ""),
    // PermissionDenied: classifier-disagreement audit + Warn-level
    // retry override.  Empty matcher — the handler inspects the
    // denied tool_input itself.
    ("PermissionDenied", ""),
];

fn inject_hooks(cfg: &config::Config) -> Result<(), String> {
    let settings_path = cfg.claude_settings_path();

    // Ensure .claude directory exists
    if let Some(parent) = settings_path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| format!("Could not create .claude directory: {e}"))?;
    }

    // Read existing settings or start fresh
    let mut settings: Value = if settings_path.exists() {
        let content = std::fs::read_to_string(&settings_path)
            .map_err(|e| format!("Could not read settings.json: {e}"))?;
        serde_json::from_str(&content).map_err(|e| format!("Could not parse settings.json: {e}"))?
    } else {
        serde_json::json!({})
    };

    let hooks = settings
        .as_object_mut()
        .ok_or("settings.json is not an object")?
        .entry("hooks")
        .or_insert_with(|| serde_json::json!({}));

    let hooks_obj = hooks.as_object_mut().ok_or("hooks is not an object")?;

    for (event, matcher) in ARAI_HOOK_REGISTRATIONS {
        register_arai_hook(hooks_obj, event, matcher)?;
    }

    // Write back
    let output = serde_json::to_string_pretty(&settings)
        .map_err(|e| format!("Could not serialize settings: {e}"))?;
    std::fs::write(&settings_path, output)
        .map_err(|e| format!("Could not write settings.json: {e}"))?;

    Ok(())
}

/// Hook events Grok Build actually supports (per docs.x.ai/build/features/hooks:
/// SessionStart/End, UserPromptSubmit, Pre/PostToolUse, PostToolUseFailure,
/// PermissionDenied, Stop/StopFailure, Notification, SubagentStart/Stop,
/// Pre/PostCompact).  Arai registers only the events it handles with
/// Grok-verified semantics — the Claude-only tier events (FileChanged,
/// InstructionsLoaded, CwdChanged, PostToolBatch) don't exist on Grok, and
/// Grok's PermissionDenied payload/retry contract is unverified, so neither
/// goes in the native file.
const GROK_HOOK_EVENTS: &[&str] = &["PreToolUse", "PostToolUse", "UserPromptSubmit"];

/// Register Arai hooks into the Grok Build native location (.grok/hooks/arai.json).
/// This is the preferred path for users who primarily use Grok Build
/// (formerly Grok TUI / supergrok).
fn inject_grok_hooks(cfg: &config::Config) -> Result<(), String> {
    let hooks_dir = cfg.grok_hooks_dir();

    // Ensure the directory exists
    std::fs::create_dir_all(&hooks_dir)
        .map_err(|e| format!("Could not create .grok/hooks directory: {e}"))?;

    let hook_file = hooks_dir.join("arai.json");

    // Build the same hook registrations we use for Claude.
    let mut grok_hooks: Value = serde_json::json!({ "hooks": {} });
    let hooks_obj = grok_hooks
        .get_mut("hooks")
        .and_then(|v| v.as_object_mut())
        .ok_or("Could not create hooks object")?;

    for (event, matcher) in ARAI_HOOK_REGISTRATIONS {
        // Only write events Grok Build supports — unsupported registrations
        // would sit dead in the file and read as misconfiguration in the
        // /hooks extensions modal.
        if !GROK_HOOK_EVENTS.contains(event) {
            continue;
        }
        let event_arr = hooks_obj
            .entry(event.to_string())
            .or_insert_with(|| serde_json::json!([]))
            .as_array_mut()
            .ok_or(format!("{event} is not an array"))?;

        // Only add if not already present (idempotent)
        let arai_exists = event_arr.iter().any(|entry| {
            if let Some(hooks_arr) = entry.get("hooks").and_then(|v| v.as_array()) {
                hooks_arr.iter().any(|h| {
                    h.get("command")
                        .and_then(|v| v.as_str())
                        .map(|cmd| cmd.contains("arai"))
                        .unwrap_or(false)
                })
            } else {
                false
            }
        });

        if !arai_exists {
            event_arr.push(serde_json::json!({
                "matcher": matcher,
                "hooks": [
                    {
                        "type": "command",
                        "command": "arai guardrails --match-stdin",
                        "timeout": 3
                    }
                ]
            }));
        }
    }

    let output = serde_json::to_string_pretty(&grok_hooks)
        .map_err(|e| format!("Could not serialize Grok hook file: {e}"))?;
    std::fs::write(&hook_file, output)
        .map_err(|e| format!("Could not write .grok/hooks/arai.json: {e}"))?;

    Ok(())
}

/// Idempotently register Arai's hook command under one event in the
/// settings.json `hooks` map.  No-op if any existing entry under that
/// event already points at an `arai`-containing command (handles the
/// re-run case where `arai init` is invoked twice).  An event-specific
/// matcher pre-filters which payloads reach Arai (see
/// `ARAI_HOOK_REGISTRATIONS`).
fn register_arai_hook(
    hooks_obj: &mut serde_json::Map<String, Value>,
    event: &str,
    matcher: &str,
) -> Result<(), String> {
    let event_arr = hooks_obj
        .entry(event.to_string())
        .or_insert_with(|| serde_json::json!([]))
        .as_array_mut()
        .ok_or(format!("{event} is not an array"))?;

    let arai_exists = event_arr.iter().any(|entry| {
        if let Some(hooks_arr) = entry.get("hooks").and_then(|v| v.as_array()) {
            hooks_arr.iter().any(|h| {
                h.get("command")
                    .and_then(|v| v.as_str())
                    .map(|cmd| cmd.contains("arai"))
                    .unwrap_or(false)
            })
        } else {
            false
        }
    });

    if !arai_exists {
        let arai_hook = serde_json::json!({
            "matcher": matcher,
            "hooks": [
                {
                    "type": "command",
                    "command": "arai guardrails --match-stdin",
                    "timeout": 3
                }
            ]
        });
        event_arr.push(arai_hook);
    }
    Ok(())
}

/// Display a path relative to project root when possible.
fn display_path(path: &str, cfg: &config::Config) -> String {
    let root = cfg.project_root.to_string_lossy();
    if path.starts_with(root.as_ref()) {
        path[root.len()..].trim_start_matches('/').to_string()
    } else if path.starts_with(cfg.home_dir.to_string_lossy().as_ref()) {
        format!("~{}", &path[cfg.home_dir.to_string_lossy().len()..])
    } else {
        path.to_string()
    }
}
