//! Hooks validation rules (CC-HK-001 to CC-HK-027)

use crate::{
    config::PerFileLintConfig,
    diagnostics::{Diagnostic, Fix},
    rules::{Validator, ValidatorMetadata},
    schemas::hooks::{Hook, HooksSchema, SettingsSchema},
};
use rust_i18n::t;
use std::path::Path;

mod helpers;
use helpers::*;

const RULE_IDS: &[&str] = &[
    "CC-HK-001",
    "CC-HK-002",
    "CC-HK-003",
    "CC-HK-004",
    "CC-HK-005",
    "CC-HK-006",
    "CC-HK-007",
    "CC-HK-008",
    "CC-HK-009",
    "CC-HK-010",
    "CC-HK-011",
    "CC-HK-012",
    "CC-HK-013",
    "CC-HK-014",
    "CC-HK-015",
    "CC-HK-016",
    "CC-HK-017",
    "CC-HK-018",
    "CC-HK-019",
    "CC-HK-020",
    "CC-HK-021",
    "CC-HK-022",
    "CC-HK-023",
    "CC-HK-024",
    "CC-HK-025",
    "CC-HK-026",
    "CC-HK-027",
];

pub struct HooksValidator;

/// Default timeout thresholds per hook type (from official Claude Code docs)
const COMMAND_HOOK_DEFAULT_TIMEOUT: u64 = 600; // 10 minutes
const PROMPT_HOOK_DEFAULT_TIMEOUT: u64 = 30; // 30 seconds

/// CC-HK-006: Missing command field
fn validate_cc_hk_006_command_field(
    command: &Option<String>,
    hook_location: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if command.is_none() {
        diagnostics.push(
            Diagnostic::error(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-006",
                t!("rules.cc_hk_006.message", location = hook_location),
            )
            .with_suggestion(t!("rules.cc_hk_006.suggestion")),
        );
    }
}

/// CC-HK-008: Script file not found
fn validate_cc_hk_008_script_exists(
    command: &str,
    project_dir: &Path,
    config: &PerFileLintConfig<'_>,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let fs = config.fs();
    for script_path in extract_script_paths(command) {
        if !has_unresolved_env_vars(&script_path) {
            let resolved = resolve_script_path(&script_path, project_dir);
            if !fs.exists(&resolved) {
                diagnostics.push(
                    Diagnostic::error(
                        path.to_path_buf(),
                        1,
                        0,
                        "CC-HK-008",
                        t!(
                            "rules.cc_hk_008.message",
                            script = script_path.as_str(),
                            resolved = resolved.display().to_string()
                        ),
                    )
                    .with_suggestion(t!("rules.cc_hk_008.suggestion")),
                );
            }
        }
    }
}

/// CC-HK-009: Dangerous command patterns
fn validate_cc_hk_009_dangerous_patterns(
    command: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if let Some((pattern, reason)) = check_dangerous_patterns(command) {
        diagnostics.push(
            Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-009",
                t!("rules.cc_hk_009.message", reason = reason),
            )
            .with_suggestion(t!("rules.cc_hk_009.suggestion", pattern = pattern)),
        );
    }
}

/// CC-HK-010: Command hook timeout policy
fn validate_cc_hk_010_command_timeout(
    timeout: &Option<u64>,
    hook_location: &str,
    version_pinned: bool,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if timeout.is_none() {
        let mut diag = Diagnostic::warning(
            path.to_path_buf(),
            1,
            0,
            "CC-HK-010",
            t!(
                "rules.cc_hk_010.command_no_timeout",
                location = hook_location
            ),
        )
        .with_suggestion(t!("rules.cc_hk_010.command_no_timeout_suggestion"));

        if !version_pinned {
            diag = diag.with_assumption(t!("rules.cc_hk_010.assumption"));
        }

        diagnostics.push(diag);
    }
    if let Some(t) = timeout {
        if *t > COMMAND_HOOK_DEFAULT_TIMEOUT {
            let mut diag = Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-010",
                t!(
                    "rules.cc_hk_010.command_exceeds",
                    location = hook_location,
                    timeout = t,
                    default = COMMAND_HOOK_DEFAULT_TIMEOUT
                ),
            )
            .with_suggestion(t!(
                "rules.cc_hk_010.command_exceeds_suggestion",
                default = COMMAND_HOOK_DEFAULT_TIMEOUT
            ));

            if !version_pinned {
                diag = diag.with_assumption(t!("rules.cc_hk_010.assumption"));
            }

            diagnostics.push(diag);
        }
    }
}

/// CC-HK-015: Model on command hook
fn validate_cc_hk_015_model_on_command(
    hook_location: &str,
    path: &Path,
    content: &str,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let mut diagnostic = Diagnostic::warning(
        path.to_path_buf(),
        1,
        0,
        "CC-HK-015",
        t!("rules.cc_hk_015.message", location = hook_location),
    )
    .with_suggestion(t!("rules.cc_hk_015.suggestion"));

    // Safe auto-fix: remove the model field line
    if let Some((start, end)) = find_unique_json_field_line_span(content, "model") {
        diagnostic = diagnostic.with_fix(Fix::delete(start, end, t!("rules.cc_hk_015.fix"), true));
    }

    diagnostics.push(diagnostic);
}

/// CC-HK-017: Prompt hook missing $ARGUMENTS
fn validate_cc_hk_017_prompt_arguments(
    prompt: &str,
    hook_location: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if !prompt.contains("$ARGUMENTS") {
        diagnostics.push(
            Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-017",
                t!("rules.cc_hk_017.message", location = hook_location),
            )
            .with_suggestion(t!("rules.cc_hk_017.suggestion")),
        );
    }
}

/// CC-HK-018: Matcher on UserPromptSubmit/Stop events (silently ignored)
fn validate_cc_hk_018_matcher_ignored(
    event: &str,
    matcher: &Option<String>,
    matcher_idx: usize,
    path: &Path,
    content: &str,
    diagnostics: &mut Vec<Diagnostic>,
) {
    let ignored_events = ["UserPromptSubmit", "Stop"];
    if ignored_events.contains(&event) && matcher.is_some() {
        let hook_location = format!("hooks.{}[{}]", event, matcher_idx);
        let mut diagnostic = Diagnostic::info(
            path.to_path_buf(),
            1,
            0,
            "CC-HK-018",
            t!(
                "rules.cc_hk_018.message",
                location = hook_location.as_str(),
                event = event
            ),
        )
        .with_suggestion(t!("rules.cc_hk_018.suggestion", event = event));

        // Safe auto-fix: remove the matcher field line
        if let Some(matcher_val) = matcher {
            if let Some((start, end)) = find_unique_matcher_line_span(content, matcher_val) {
                diagnostic = diagnostic.with_fix(Fix::delete(
                    start,
                    end,
                    t!("rules.cc_hk_018.fix", event = event),
                    true,
                ));
            }
        }

        diagnostics.push(diagnostic);
    }
}

/// CC-HK-002: Prompt hook on wrong event
fn validate_cc_hk_002_prompt_event_type(
    event: &str,
    hook_location: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if !HooksSchema::is_prompt_event(event) {
        diagnostics.push(
            Diagnostic::error(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-002",
                t!(
                    "rules.cc_hk_002.message",
                    location = hook_location,
                    event = event
                ),
            )
            .with_suggestion(t!("rules.cc_hk_002.suggestion")),
        );
    }
}

/// CC-HK-007: Missing prompt field
fn validate_cc_hk_007_prompt_field(
    prompt: &Option<String>,
    hook_location: &str,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if prompt.is_none() {
        diagnostics.push(
            Diagnostic::error(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-007",
                t!("rules.cc_hk_007.message", location = hook_location),
            )
            .with_suggestion(t!("rules.cc_hk_007.suggestion")),
        );
    }
}

/// CC-HK-010: Prompt hook timeout policy
fn validate_cc_hk_010_prompt_timeout(
    timeout: &Option<u64>,
    hook_location: &str,
    version_pinned: bool,
    path: &Path,
    diagnostics: &mut Vec<Diagnostic>,
) {
    if timeout.is_none() {
        let mut diag = Diagnostic::warning(
            path.to_path_buf(),
            1,
            0,
            "CC-HK-010",
            t!(
                "rules.cc_hk_010.prompt_no_timeout",
                location = hook_location
            ),
        )
        .with_suggestion(t!("rules.cc_hk_010.prompt_no_timeout_suggestion"));

        if !version_pinned {
            diag = diag.with_assumption(t!("rules.cc_hk_010.assumption"));
        }

        diagnostics.push(diag);
    }
    if let Some(t) = timeout {
        if *t > PROMPT_HOOK_DEFAULT_TIMEOUT {
            let mut diag = Diagnostic::warning(
                path.to_path_buf(),
                1,
                0,
                "CC-HK-010",
                t!(
                    "rules.cc_hk_010.prompt_exceeds",
                    location = hook_location,
                    timeout = t,
                    default = PROMPT_HOOK_DEFAULT_TIMEOUT
                ),
            )
            .with_suggestion(t!(
                "rules.cc_hk_010.prompt_exceeds_suggestion",
                default = PROMPT_HOOK_DEFAULT_TIMEOUT
            ));

            if !version_pinned {
                diag = diag.with_assumption(t!("rules.cc_hk_010.assumption"));
            }

            diagnostics.push(diag);
        }
    }
}

#[cfg(test)]
#[allow(dead_code)]
impl HooksValidator {
    fn check_dangerous_patterns(&self, command: &str) -> Option<(&'static str, &'static str)> {
        check_dangerous_patterns(command)
    }

    fn extract_script_paths(&self, command: &str) -> Vec<String> {
        extract_script_paths(command)
    }

    fn resolve_script_path(&self, script_path: &str, project_dir: &Path) -> std::path::PathBuf {
        resolve_script_path(script_path, project_dir)
    }

    fn has_unresolved_env_vars(&self, path: &str) -> bool {
        has_unresolved_env_vars(path)
    }
}

impl Validator for HooksValidator {
    fn metadata(&self) -> ValidatorMetadata {
        ValidatorMetadata {
            name: self.name(),
            rule_ids: RULE_IDS,
        }
    }

    /// Main validation entry point for hooks configuration.
    ///
    /// ## Validation Phases
    ///
    /// 1. **Category check** - Early return if hooks category disabled
    /// 2. **JSON parsing** - Parse raw JSON, report CC-HK-012 on failure
    /// 3. **Pre-parse validation** - Raw JSON checks (CC-HK-005, CC-HK-011, CC-HK-013..014, CC-HK-016, CC-HK-020..024)
    /// 4. **Typed parsing** - Parse into SettingsSchema
    /// 5. **Event iteration** - Validate each event and hook (CC-HK-015, CC-HK-017, CC-HK-018)
    fn validate_per_file(
        &self,
        path: &Path,
        content: &str,
        config: &PerFileLintConfig<'_>,
    ) -> Vec<Diagnostic> {
        let mut diagnostics = Vec::new();

        if !config.rules().hooks {
            return diagnostics;
        }

        let raw_value: serde_json::Value = match serde_json::from_str(content) {
            Ok(v) => v,
            Err(e) => {
                if config.is_rule_enabled("CC-HK-012") {
                    diagnostics.push(
                        Diagnostic::error(
                            path.to_path_buf(),
                            1,
                            0,
                            "CC-HK-012",
                            t!("rules.cc_hk_012.message", error = e.to_string()),
                        )
                        .with_suggestion(t!("rules.cc_hk_012.suggestion")),
                    );
                }
                return diagnostics;
            }
        };

        // Raw JSON validation: CC-HK-005, 011, 013, 014, 016, 020-025.
        // Single consolidated traversal (2 passes max instead of 11).
        if !validate_all_raw_hooks(&raw_value, path, content, config, &mut diagnostics) {
            return diagnostics;
        }

        let settings: SettingsSchema = match serde_json::from_value(raw_value) {
            Ok(s) => s,
            Err(e) => {
                if config.is_rule_enabled("CC-HK-012") {
                    diagnostics.push(
                        Diagnostic::error(
                            path.to_path_buf(),
                            1,
                            0,
                            "CC-HK-012",
                            t!("rules.cc_hk_012.message", error = e.to_string()),
                        )
                        .with_suggestion(t!("rules.cc_hk_012.suggestion")),
                    );
                }
                return diagnostics;
            }
        };

        let project_dir = path
            .parent()
            .and_then(|p| {
                if p.ends_with(".claude") {
                    p.parent()
                } else {
                    Some(p)
                }
            })
            .unwrap_or_else(|| Path::new("."));

        for (event, matchers) in &settings.hooks {
            // --- Event-level validation ---
            // CC-HK-001: Invalid event name
            if config.is_rule_enabled("CC-HK-001") {
                if !validate_cc_hk_001_event_name(event, path, content, &mut diagnostics) {
                    continue;
                }
            } else if !HooksSchema::VALID_EVENTS.contains(&event.as_str()) {
                continue; // Skip invalid events even if rule disabled
            }

            // CC-HK-019: Deprecated event name
            if config.is_rule_enabled("CC-HK-019") {
                validate_cc_hk_019_deprecated_event(event, path, content, &mut diagnostics);
            }

            for (matcher_idx, matcher) in matchers.iter().enumerate() {
                // --- Matcher-level validation ---
                // CC-HK-003: Matcher hint for tool events
                if config.is_rule_enabled("CC-HK-003") {
                    validate_cc_hk_003_matcher_hint(
                        event,
                        &matcher.matcher,
                        matcher_idx,
                        path,
                        &mut diagnostics,
                    );
                }

                // CC-HK-004: Matcher on non-tool event
                if config.is_rule_enabled("CC-HK-004") {
                    validate_cc_hk_004_matcher_forbidden(
                        event,
                        &matcher.matcher,
                        matcher_idx,
                        path,
                        content,
                        &mut diagnostics,
                    );
                }

                // CC-HK-018: Matcher on UserPromptSubmit/Stop
                if config.is_rule_enabled("CC-HK-018") {
                    validate_cc_hk_018_matcher_ignored(
                        event,
                        &matcher.matcher,
                        matcher_idx,
                        path,
                        content,
                        &mut diagnostics,
                    );
                }

                // --- Hook-level validation ---
                for (hook_idx, hook) in matcher.hooks.iter().enumerate() {
                    let hook_location = format!(
                        "hooks.{}{}.hooks[{}]",
                        event,
                        matcher
                            .matcher
                            .as_ref()
                            .map(|m| format!("[matcher={}]", m))
                            .unwrap_or_else(|| format!("[{}]", matcher_idx)),
                        hook_idx
                    );

                    match hook {
                        Hook::Command {
                            command,
                            timeout,
                            model,
                            ..
                        } => {
                            // CC-HK-010: Command timeout policy
                            if config.is_rule_enabled("CC-HK-010") {
                                validate_cc_hk_010_command_timeout(
                                    timeout,
                                    &hook_location,
                                    config.is_claude_code_version_pinned(),
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-006: Missing command field
                            if config.is_rule_enabled("CC-HK-006") {
                                validate_cc_hk_006_command_field(
                                    command,
                                    &hook_location,
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-015: Model on command hook
                            if config.is_rule_enabled("CC-HK-015") && model.is_some() {
                                validate_cc_hk_015_model_on_command(
                                    &hook_location,
                                    path,
                                    content,
                                    &mut diagnostics,
                                );
                            }

                            if let Some(cmd) = command {
                                // CC-HK-008: Script file not found
                                if config.is_rule_enabled("CC-HK-008") {
                                    validate_cc_hk_008_script_exists(
                                        cmd,
                                        project_dir,
                                        config,
                                        path,
                                        &mut diagnostics,
                                    );
                                }

                                // CC-HK-009: Dangerous command patterns
                                if config.is_rule_enabled("CC-HK-009") {
                                    validate_cc_hk_009_dangerous_patterns(
                                        cmd,
                                        path,
                                        &mut diagnostics,
                                    );
                                }
                            }
                        }
                        Hook::Prompt {
                            prompt, timeout, ..
                        } => {
                            // CC-HK-010: Prompt timeout policy
                            if config.is_rule_enabled("CC-HK-010") {
                                validate_cc_hk_010_prompt_timeout(
                                    timeout,
                                    &hook_location,
                                    config.is_claude_code_version_pinned(),
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-002: Prompt on wrong event
                            if config.is_rule_enabled("CC-HK-002") {
                                validate_cc_hk_002_prompt_event_type(
                                    event,
                                    &hook_location,
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-007: Missing prompt field
                            if config.is_rule_enabled("CC-HK-007") {
                                validate_cc_hk_007_prompt_field(
                                    prompt,
                                    &hook_location,
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-017: Prompt hook missing $ARGUMENTS
                            if config.is_rule_enabled("CC-HK-017") {
                                if let Some(p) = prompt {
                                    validate_cc_hk_017_prompt_arguments(
                                        p,
                                        &hook_location,
                                        path,
                                        &mut diagnostics,
                                    );
                                }
                            }
                        }
                        Hook::Agent {
                            prompt, timeout, ..
                        } => {
                            // CC-HK-010: Agent timeout policy (same as prompt)
                            if config.is_rule_enabled("CC-HK-010") {
                                validate_cc_hk_010_prompt_timeout(
                                    timeout,
                                    &hook_location,
                                    config.is_claude_code_version_pinned(),
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-002: Agent hook on wrong event (same restriction as prompt)
                            if config.is_rule_enabled("CC-HK-002") {
                                validate_cc_hk_002_prompt_event_type(
                                    event,
                                    &hook_location,
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-007: Missing prompt field
                            if config.is_rule_enabled("CC-HK-007") {
                                validate_cc_hk_007_prompt_field(
                                    prompt,
                                    &hook_location,
                                    path,
                                    &mut diagnostics,
                                );
                            }

                            // CC-HK-017: Agent hook missing $ARGUMENTS
                            if config.is_rule_enabled("CC-HK-017") {
                                if let Some(p) = prompt {
                                    validate_cc_hk_017_prompt_arguments(
                                        p,
                                        &hook_location,
                                        path,
                                        &mut diagnostics,
                                    );
                                }
                            }
                        }
                        Hook::Http { .. } => {
                            // HTTP hooks are validated at the raw JSON level:
                            // CC-HK-005 type check, CC-HK-011 timeout, CC-HK-016 type validation,
                            // CC-HK-020 url validation, CC-HK-024 headers env vars.
                            // No additional typed validation needed here.
                        }
                        Hook::McpTool { .. } => {
                            // MCP tool hooks (added Claude Code v2.1.118) are
                            // validated at the raw JSON level via CC-HK-016
                            // (string allow-list). Schema details for required
                            // fields are not yet documented at
                            // code.claude.com/docs/en/hooks (as of 2026-04-23);
                            // re-tighten when upstream docs land.
                        }
                    }
                }
            }
        }

        diagnostics
    }
}

#[cfg(test)]
mod tests;
