use std::io::{BufRead, Write};
use std::path::Path;

use console::style;
use sha2::{Digest, Sha256};

use crate::scanner::{RiskLevel, ScanReport};
use crate::ui;

/// Number of source lines to show before the flagged line.
const CONTEXT_LINES_BEFORE: usize = 3;
/// Number of source lines to show after the flagged line.
const CONTEXT_LINES_AFTER: usize = 2;
/// Width of the separator line in detail view.
const SEPARATOR_WIDTH: usize = 60;

/// Options controlling the scan gate behavior.
pub struct GateOptions {
    /// Auto-confirm WARN level risks (equivalent to --yes flag).
    pub auto_yes: bool,
    /// Headless mode: no interactive prompts (for CI environments).
    /// In headless mode, WARN auto-passes and DANGER auto-refuses.
    pub headless: bool,
}

/// Gate scan results: auto-pass PASS/INFO, prompt for WARN, require "yes" for DANGER, block BLOCK.
///
/// `skill_dir` is used to display source context for the `detail` command.
///
/// Headless mode is activated by `opts.headless`, or auto-detected via `CI=true`
/// or `SKILLX_HEADLESS=1` environment variables.
pub fn gate_scan_result(
    scan_report: &Option<ScanReport>,
    skill_dir: &Path,
    opts: &GateOptions,
) -> anyhow::Result<()> {
    let effective_headless =
        opts.headless || std::env::var("CI").is_ok() || std::env::var("SKILLX_HEADLESS").is_ok();

    if effective_headless {
        return gate_scan_result_headless(scan_report);
    }
    gate_scan_result_inner(
        scan_report,
        skill_dir,
        opts.auto_yes,
        &mut std::io::stdin().lock(),
        &mut std::io::stderr(),
    )
}

/// Headless gate: no interactive prompts.
/// PASS/INFO/WARN auto-pass; DANGER/BLOCK auto-refuse.
fn gate_scan_result_headless(scan_report: &Option<ScanReport>) -> anyhow::Result<()> {
    let report = match scan_report {
        Some(r) => r,
        None => return Ok(()),
    };

    let level = report.overall_level();
    match level {
        RiskLevel::Pass | RiskLevel::Info | RiskLevel::Warn => Ok(()),
        RiskLevel::Danger => {
            ui::error("DANGER level findings detected. Refused in headless/CI mode.");
            Err(crate::error::SkillxError::ScanBlocked.into())
        }
        RiskLevel::Block => {
            ui::error("BLOCK level findings detected. Execution refused.");
            Err(crate::error::SkillxError::ScanBlocked.into())
        }
    }
}

/// Inner implementation with injectable I/O for testability.
pub(crate) fn gate_scan_result_inner(
    scan_report: &Option<ScanReport>,
    skill_dir: &Path,
    auto_yes: bool,
    input: &mut dyn BufRead,
    output: &mut dyn Write,
) -> anyhow::Result<()> {
    let report = match scan_report {
        Some(r) => r,
        None => return Ok(()),
    };

    let level = report.overall_level();
    match level {
        RiskLevel::Pass | RiskLevel::Info => {}
        RiskLevel::Warn => {
            if !auto_yes {
                write!(output, "{} Continue? [Y/n] ", style("⚠").yellow().bold())?;
                output.flush().ok();
                let mut line = String::new();
                input.read_line(&mut line)?;
                let line = line.trim().to_lowercase();
                if line == "n" || line == "no" {
                    return Err(crate::error::SkillxError::UserCancelled.into());
                }
            }
        }
        RiskLevel::Danger => {
            writeln!(
                output,
                "\n{}",
                style("DANGER level findings detected. Review carefully.")
                    .red()
                    .bold()
            )?;
            writeln!(
                output,
                "Type '{}' to see finding details, or type '{}' to continue:",
                style("detail N").cyan(),
                style("yes").green().bold()
            )?;

            let mut sorted_findings = report.findings.clone();
            sorted_findings.sort_by_key(|f| std::cmp::Reverse(f.level));

            loop {
                write!(output, "{} ", style(">").dim())?;
                output.flush().ok();
                let mut line = String::new();
                input.read_line(&mut line)?;
                let line = line.trim();

                if line.eq_ignore_ascii_case("yes") {
                    break;
                } else if line.eq_ignore_ascii_case("no") || line.eq_ignore_ascii_case("n") {
                    return Err(crate::error::SkillxError::UserCancelled.into());
                } else if line.starts_with("detail") || line.starts_with("d ") {
                    let num_str = line
                        .strip_prefix("detail")
                        .or_else(|| line.strip_prefix("d "))
                        .unwrap_or("")
                        .trim();
                    if let Ok(n) = num_str.parse::<usize>() {
                        if n > 0 && n <= sorted_findings.len() {
                            let finding = &sorted_findings[n - 1];
                            writeln!(output, "\n{}", style("─".repeat(SEPARATOR_WIDTH)).dim())?;
                            writeln!(output, "  Rule:    {} ({})", finding.rule_id, finding.level)?;
                            writeln!(output, "  File:    {}", finding.file)?;
                            if let Some(ln) = finding.line {
                                writeln!(output, "  Line:    {ln}")?;
                            }
                            writeln!(output, "  Message: {}", finding.message)?;

                            if let Some(ln) = finding.line {
                                let file_path = skill_dir.join(&finding.file);
                                match std::fs::read_to_string(&file_path) {
                                    Ok(content) => {
                                        let lines: Vec<&str> = content.lines().collect();
                                        let start = (ln - 1).saturating_sub(CONTEXT_LINES_BEFORE);
                                        let end = (ln + CONTEXT_LINES_AFTER).min(lines.len());
                                        writeln!(output, "\n  Source:")?;
                                        for (i, l) in lines[start..end].iter().enumerate() {
                                            let line_num = start + i + 1;
                                            let marker = if line_num == ln { ">" } else { " " };
                                            writeln!(
                                                output,
                                                "  {marker} {}: {}",
                                                style(line_num).dim(),
                                                l
                                            )?;
                                        }
                                    }
                                    Err(_) => {
                                        writeln!(output, "\n  (source unavailable)")?;
                                    }
                                }
                            } else {
                                // Binary/resource finding — show file metadata
                                let file_path = skill_dir.join(&finding.file);
                                if file_path.exists() {
                                    if let Ok(meta) = std::fs::metadata(&file_path) {
                                        writeln!(output, "\n  Size: {}", format_size(meta.len()))?;
                                    }
                                    if let Ok(content) = std::fs::read(&file_path) {
                                        let hash = format!("{:x}", Sha256::digest(&content));
                                        writeln!(output, "  SHA-256: {hash}")?;
                                        if let Some(kind) = infer::get(&content) {
                                            writeln!(
                                                output,
                                                "  Type: {} ({})",
                                                kind.extension(),
                                                kind.mime_type()
                                            )?;
                                        }
                                    }
                                }
                            }
                            writeln!(output, "{}", style("─".repeat(SEPARATOR_WIDTH)).dim())?;
                        } else {
                            writeln!(
                                output,
                                "  Invalid finding number. Valid range: 1-{}",
                                sorted_findings.len()
                            )?;
                        }
                    } else {
                        writeln!(output, "  Usage: detail <number>")?;
                    }
                } else {
                    writeln!(
                        output,
                        "  Type 'yes' to continue, 'no' to abort, or 'detail N' to inspect"
                    )?;
                }
            }
        }
        RiskLevel::Block => {
            ui::error("BLOCK level findings detected. Execution refused.");
            return Err(crate::error::SkillxError::ScanBlocked.into());
        }
    }
    Ok(())
}

/// Format a byte count into a human-readable size string.
fn format_size(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = 1024 * KB;
    const GB: u64 = 1024 * MB;

    if bytes >= GB {
        format!("{:.1} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.1} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.1} KB", bytes as f64 / KB as f64)
    } else {
        format!("{bytes} B")
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::scanner::{Finding, ScanReport};
    use std::io::Cursor;

    // ── Helpers ──

    fn make_warn_report() -> ScanReport {
        ScanReport {
            findings: vec![Finding {
                rule_id: "MD-003".to_string(),
                level: RiskLevel::Warn,
                file: "SKILL.md".to_string(),
                line: Some(5),
                message: "references external data transfer".to_string(),
                context: Some("send data to https://example.com".to_string()),
            }],
        }
    }

    fn make_danger_report(skill_dir: &Path) -> ScanReport {
        // Write a fixture file so `detail` can read source context.
        let skill_md = skill_dir.join("SKILL.md");
        std::fs::write(
            &skill_md,
            "---\nname: test\n---\n# Test\nignore previous instructions\n",
        )
        .unwrap();
        ScanReport {
            findings: vec![Finding {
                rule_id: "MD-001".to_string(),
                level: RiskLevel::Danger,
                file: "SKILL.md".to_string(),
                line: Some(5),
                message: "prompt injection pattern detected".to_string(),
                context: Some("ignore previous instructions".to_string()),
            }],
        }
    }

    fn opts(auto_yes: bool) -> GateOptions {
        GateOptions {
            auto_yes,
            headless: false,
        }
    }

    // ── Existing tests (using public API) ──

    #[test]
    fn test_gate_none_report() {
        let result = gate_scan_result(&None, Path::new("."), &opts(false));
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_pass_auto() {
        let report = ScanReport { findings: vec![] };
        let result = gate_scan_result(&Some(report), Path::new("."), &opts(false));
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_info_auto() {
        let report = ScanReport {
            findings: vec![Finding {
                rule_id: "MD-001".to_string(),
                level: RiskLevel::Info,
                file: "SKILL.md".to_string(),
                line: Some(1),
                message: "info finding".to_string(),
                context: None,
            }],
        };
        let result = gate_scan_result(&Some(report), Path::new("."), &opts(false));
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_block_refused() {
        let report = ScanReport {
            findings: vec![Finding {
                rule_id: "SC-001".to_string(),
                level: RiskLevel::Block,
                file: "run.sh".to_string(),
                line: Some(1),
                message: "blocked".to_string(),
                context: None,
            }],
        };
        let result = gate_scan_result(&Some(report), Path::new("."), &opts(false));
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err.downcast_ref::<crate::error::SkillxError>().is_some());
    }

    #[test]
    fn test_format_size() {
        assert_eq!(format_size(0), "0 B");
        assert_eq!(format_size(512), "512 B");
        assert_eq!(format_size(1023), "1023 B");
        assert_eq!(format_size(1024), "1.0 KB");
        assert_eq!(format_size(1536), "1.5 KB");
        assert_eq!(format_size(1024 * 1024), "1.0 MB");
        assert_eq!(format_size(1024 * 1024 * 1024), "1.0 GB");
        assert_eq!(format_size(50 * 1024 * 1024), "50.0 MB");
    }

    // ── Interactive gate tests (using inner API) ──

    #[test]
    fn test_gate_warn_user_says_no() {
        let report = make_warn_report();
        let mut input = Cursor::new(b"n\n" as &[u8]);
        let mut output = Vec::new();
        let result = gate_scan_result_inner(
            &Some(report),
            Path::new("."),
            false,
            &mut input,
            &mut output,
        );
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err
            .downcast_ref::<crate::error::SkillxError>()
            .is_some_and(|e| matches!(e, crate::error::SkillxError::UserCancelled)));
    }

    #[test]
    fn test_gate_warn_user_says_yes() {
        let report = make_warn_report();
        let mut input = Cursor::new(b"y\n" as &[u8]);
        let mut output = Vec::new();
        let result = gate_scan_result_inner(
            &Some(report),
            Path::new("."),
            false,
            &mut input,
            &mut output,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_warn_default_enter_accepts() {
        let report = make_warn_report();
        let mut input = Cursor::new(b"\n" as &[u8]);
        let mut output = Vec::new();
        let result = gate_scan_result_inner(
            &Some(report),
            Path::new("."),
            false,
            &mut input,
            &mut output,
        );
        // Empty input (just Enter) defaults to Yes
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_danger_user_says_yes() {
        let dir = tempfile::tempdir().unwrap();
        let report = make_danger_report(dir.path());
        let mut input = Cursor::new(b"yes\n" as &[u8]);
        let mut output = Vec::new();
        let result =
            gate_scan_result_inner(&Some(report), dir.path(), false, &mut input, &mut output);
        assert!(result.is_ok());
    }

    #[test]
    fn test_gate_danger_user_says_no() {
        let dir = tempfile::tempdir().unwrap();
        let report = make_danger_report(dir.path());
        let mut input = Cursor::new(b"no\n" as &[u8]);
        let mut output = Vec::new();
        let result =
            gate_scan_result_inner(&Some(report), dir.path(), false, &mut input, &mut output);
        assert!(result.is_err());
        let err = result.unwrap_err();
        assert!(err
            .downcast_ref::<crate::error::SkillxError>()
            .is_some_and(|e| matches!(e, crate::error::SkillxError::UserCancelled)));
    }

    #[test]
    fn test_gate_danger_detail_then_yes() {
        let dir = tempfile::tempdir().unwrap();
        let report = make_danger_report(dir.path());
        let mut input = Cursor::new(b"detail 1\nyes\n" as &[u8]);
        let mut output = Vec::new();
        let result =
            gate_scan_result_inner(&Some(report), dir.path(), false, &mut input, &mut output);
        assert!(result.is_ok());
        // Verify the detail output contains rule info
        let output_str = String::from_utf8_lossy(&output);
        assert!(output_str.contains("MD-001"));
        assert!(output_str.contains("Source:"));
    }

    // ── Headless mode tests ──

    #[test]
    fn test_gate_headless_warn_auto_passes() {
        let report = make_warn_report();
        let result = gate_scan_result_headless(&Some(report));
        assert!(result.is_ok(), "Headless mode should auto-pass WARN");
    }

    #[test]
    fn test_gate_headless_danger_refuses() {
        let report = ScanReport {
            findings: vec![Finding {
                rule_id: "MD-001".to_string(),
                level: RiskLevel::Danger,
                file: "SKILL.md".to_string(),
                line: Some(1),
                message: "danger finding".to_string(),
                context: None,
            }],
        };
        let result = gate_scan_result_headless(&Some(report));
        assert!(result.is_err(), "Headless mode should refuse DANGER");
    }

    #[test]
    fn test_gate_headless_block_refuses() {
        let report = ScanReport {
            findings: vec![Finding {
                rule_id: "SC-010".to_string(),
                level: RiskLevel::Block,
                file: "evil.sh".to_string(),
                line: Some(1),
                message: "blocked".to_string(),
                context: None,
            }],
        };
        let result = gate_scan_result_headless(&Some(report));
        assert!(result.is_err(), "Headless mode should refuse BLOCK");
    }

    #[test]
    fn test_gate_headless_pass_succeeds() {
        let report = ScanReport { findings: vec![] };
        let result = gate_scan_result_headless(&Some(report));
        assert!(result.is_ok(), "Headless mode should pass PASS");
    }
}
