//! Logic for the `timebomb defuse` subcommand.
//!
//! Walks through each detonated fuse interactively, prompting the user to
//! extend the date, delete the line, or skip it. After processing all
//! fuses it prints a summary.

use crate::annotation::Fuse;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::remove::remove_line;
use crate::scanner::scan;
use crate::snooze::snooze_line;
use chrono::NaiveDate;
use colored::Colorize;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/// The action chosen by the user for a single detonated fuse.
enum FixAction {
    /// Replace the expiry date with this new date.
    Extend(NaiveDate),
    /// Remove the fuse line entirely.
    Delete,
    /// Leave this fuse unchanged.
    Skip,
}

/// A resolved decision pairing an action with the fuse it targets.
struct Decision {
    action: FixAction,
    /// Absolute path to the file containing the fuse.
    abs_path: PathBuf,
    /// 1-based line number of the fuse.
    line: usize,
}

/// Summary counts returned by `run_fix`.
pub struct FixSummary {
    pub extended: usize,
    pub deleted: usize,
    pub skipped: usize,
}

// ---------------------------------------------------------------------------
// Public entry point
// ---------------------------------------------------------------------------

/// Core logic for `timebomb defuse`.
///
/// Pass 1 — interactive: scan for detonated fuses and prompt the user for
/// each one.
///
/// Pass 2 — apply: group decisions by file, sort line numbers descending, and
/// apply edits bottom-up so earlier line numbers are not shifted by removals.
///
/// Always returns `Ok(FixSummary)`; the caller exits 0.
pub fn run_fix(scan_path: &Path, cfg: &Config, today: NaiveDate) -> Result<FixSummary> {
    // Pass 1: collect detonated fuses ----------------------------------------
    let result = scan(scan_path, cfg, today)?;
    let detonated: Vec<&Fuse> = result.detonated();

    if detonated.is_empty() {
        println!("No detonated fuses found.");
        return Ok(FixSummary {
            extended: 0,
            deleted: 0,
            skipped: 0,
        });
    }

    println!(
        "{} detonated fuse(s) to review:\n",
        detonated.len().to_string().red().bold()
    );

    // Pass 1: prompt the user for each fuse ---------------------------------
    let mut decisions: Vec<Decision> = Vec::new();

    for ann in &detonated {
        let abs_path = scan_path.join(&ann.file);

        println!(
            "{} {}:{}",
            "[DETONATED]".red().bold(),
            ann.file.display(),
            ann.line
        );
        println!(
            "  {} [{}]: {}",
            ann.tag.yellow(),
            ann.date.format("%Y-%m-%d"),
            ann.message
        );

        let action = prompt_action(today)?;

        decisions.push(Decision {
            action,
            abs_path,
            line: ann.line,
        });

        println!();
    }

    // Pass 2: apply decisions grouped by file, bottom-up --------------------
    // Group decisions by absolute file path.
    let mut by_file: HashMap<PathBuf, Vec<&Decision>> = HashMap::new();
    for d in &decisions {
        by_file.entry(d.abs_path.clone()).or_default().push(d);
    }

    let mut summary = FixSummary {
        extended: 0,
        deleted: 0,
        skipped: 0,
    };

    for (file_path, mut file_decisions) in by_file {
        // Sort by line number descending so edits don't shift subsequent lines.
        file_decisions.sort_unstable_by_key(|d| Reverse(d.line));

        for d in file_decisions {
            match &d.action {
                FixAction::Skip => {
                    summary.skipped += 1;
                }
                FixAction::Delete => {
                    remove_line(&file_path, d.line)?;
                    summary.deleted += 1;
                }
                FixAction::Extend(new_date) => {
                    apply_extend(&file_path, d.line, *new_date)?;
                    summary.extended += 1;
                }
            }
        }
    }

    Ok(summary)
}

// ---------------------------------------------------------------------------
// Interactive prompt helpers
// ---------------------------------------------------------------------------

/// Prompt the user for an action on a single detonated fuse.
///
/// Loops until a valid response is received. Returns `FixAction`.
fn prompt_action(today: NaiveDate) -> Result<FixAction> {
    loop {
        print!("  Action [e=extend / d=delete / s=skip / ?=help]: ");
        io::stdout().flush().map_err(|e| Error::Io {
            source: e,
            path: None,
        })?;

        let stdin = io::stdin();
        let mut buf = String::new();
        stdin.lock().read_line(&mut buf).map_err(|e| Error::Io {
            source: e,
            path: None,
        })?;

        match buf.trim() {
            "e" | "E" => {
                let new_date = prompt_date(today)?;
                return Ok(FixAction::Extend(new_date));
            }
            "d" | "D" => return Ok(FixAction::Delete),
            "s" | "S" => return Ok(FixAction::Skip),
            "?" => {
                println!("  e  — extend: enter a new expiry date (must be after today)");
                println!("  d  — delete: remove the fuse line from the file");
                println!("  s  — skip:   leave the fuse unchanged and continue");
            }
            "" => {
                // EOF or empty line — treat as skip to avoid an infinite loop in
                // non-interactive environments (e.g. pipes or test harnesses).
                return Ok(FixAction::Skip);
            }
            other => {
                println!("  Unknown option '{}'. Enter e, d, s, or ?.", other);
            }
        }
    }
}

/// Prompt the user to enter a new expiry date. Validates that it is strictly
/// after `today`. Loops until a valid date is entered.
fn prompt_date(today: NaiveDate) -> Result<NaiveDate> {
    loop {
        print!("  New expiry date (YYYY-MM-DD): ");
        io::stdout().flush().map_err(|e| Error::Io {
            source: e,
            path: None,
        })?;

        let stdin = io::stdin();
        let mut buf = String::new();
        stdin.lock().read_line(&mut buf).map_err(|e| Error::Io {
            source: e,
            path: None,
        })?;

        let trimmed = buf.trim();

        match NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
            Ok(date) if date > today => return Ok(date),
            Ok(_) => {
                println!(
                    "  Date must be after today ({}). Try again.",
                    today.format("%Y-%m-%d")
                );
            }
            Err(_) => {
                println!("  '{}' is not a valid date. Expected YYYY-MM-DD.", trimmed);
            }
        }
    }
}

// ---------------------------------------------------------------------------
// File mutation helpers
// ---------------------------------------------------------------------------

/// Replace the date bracket on the given line in `file_path` with `new_date`.
///
/// Reads the entire file, replaces the target line, and writes it back.
fn apply_extend(file_path: &Path, line_number: usize, new_date: NaiveDate) -> Result<()> {
    let content = std::fs::read_to_string(file_path).map_err(|e| Error::Io {
        source: e,
        path: Some(file_path.to_path_buf()),
    })?;

    let lines: Vec<&str> = content.lines().collect();

    if line_number < 1 || line_number > lines.len() {
        return Err(Error::InvalidArgument(format!(
            "line {} is out of range for '{}' ({} lines)",
            line_number,
            file_path.display(),
            lines.len(),
        )));
    }

    let original = lines[line_number - 1];

    let new_line = snooze_line(original, new_date).ok_or_else(|| {
        Error::InvalidArgument(format!(
            "no timebomb date bracket found on line {} of '{}'",
            line_number,
            file_path.display(),
        ))
    })?;

    let mut new_content = String::with_capacity(content.len() + new_line.len());
    for (i, line) in lines.iter().enumerate() {
        if i == line_number - 1 {
            new_content.push_str(&new_line);
        } else {
            new_content.push_str(line);
        }
        new_content.push('\n');
    }
    // Preserve the original file's trailing newline behaviour
    if !content.ends_with('\n') {
        new_content.pop();
    }

    // Write atomically: temp file in the same directory, then rename so a
    // mid-write crash never leaves a partially-written source file.
    let tmp_path = file_path.with_extension(format!("tmp.{}", std::process::id()));
    std::fs::write(&tmp_path, new_content).map_err(|e| Error::Io {
        source: e,
        path: Some(tmp_path.clone()),
    })?;
    std::fs::rename(&tmp_path, file_path).map_err(|e| Error::Io {
        source: e,
        path: Some(file_path.to_path_buf()),
    })?;

    Ok(())
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::NaiveDate;
    use std::fs;
    use tempfile::tempdir;

    fn date(s: &str) -> NaiveDate {
        NaiveDate::parse_from_str(s, "%Y-%m-%d").unwrap()
    }

    fn today() -> NaiveDate {
        // Fixed date — well after fixture expired dates (2018–2021), so all
        // fixture fuses are treated as detonated without depending on the wall clock.
        date("2026-03-22")
    }

    // ── apply_extend ─────────────────────────────────────────────────────────

    #[test]
    fn test_fix_extend_replaces_date() {
        let dir = tempdir().unwrap();
        let file = dir.path().join("test.rs");
        fs::write(&file, "// TODO[2020-01-01]: expired annotation\n").unwrap();

        apply_extend(&file, 1, date("2027-06-01")).unwrap();

        let content = fs::read_to_string(&file).unwrap();
        assert!(content.contains("2027-06-01"), "new date should appear");
        assert!(!content.contains("2020-01-01"), "old date should be gone");
    }

    // ── remove_line (the Delete path in run_fix) ──────────────────────────

    #[test]
    fn test_fix_delete_removes_line() {
        let dir = tempdir().unwrap();
        let file = dir.path().join("test.rs");
        fs::write(
            &file,
            "fn alpha() {}\n// TODO[2020-01-01]: expired\nfn beta() {}\n",
        )
        .unwrap();

        remove_line(&file, 2).unwrap();

        let content = fs::read_to_string(&file).unwrap();
        let lines: Vec<&str> = content.lines().collect();
        assert_eq!(lines.len(), 2);
        assert_eq!(lines[0], "fn alpha() {}");
        assert_eq!(lines[1], "fn beta() {}");
    }

    // ── bottom-up ordering prevents line-shift corruption ─────────────────

    #[test]
    fn test_fix_multi_file_bottom_up_order() {
        let dir = tempdir().unwrap();
        let file = dir.path().join("multi.rs");
        // Lines 1, 3, 5 are context; lines 2 and 4 are expired annotations.
        fs::write(
            &file,
            "fn a() {}\n\
             // TODO[2020-01-01]: first expired\n\
             fn b() {}\n\
             // TODO[2019-06-01]: second expired\n\
             fn c() {}\n",
        )
        .unwrap();

        // Simulate what run_fix does: delete line 4 first (descending), then line 2.
        remove_line(&file, 4).unwrap();
        remove_line(&file, 2).unwrap();

        let content = fs::read_to_string(&file).unwrap();
        let lines: Vec<&str> = content.lines().collect();
        assert_eq!(lines.len(), 3);
        assert_eq!(lines[0], "fn a() {}");
        assert_eq!(lines[1], "fn b() {}");
        assert_eq!(lines[2], "fn c() {}");
        assert!(!content.contains("first expired"));
        assert!(!content.contains("second expired"));
    }

    // ── date validation: new date must be after today ─────────────────────

    #[test]
    fn test_fix_extend_date_before_today_rejected() {
        // apply_extend itself does not validate the date — that is done by
        // prompt_date. Verify that a date <= today is correctly detected as
        // invalid at the prompt level by calling the validation logic directly.
        let past = date("2020-01-01");
        let t = today();
        // past <= today must be rejected
        assert!(
            past <= t,
            "sanity: 2020-01-01 should be before or equal to today"
        );

        // A future date must pass
        let future = date("2028-01-01");
        assert!(future > t, "sanity: 2028-01-01 should be after today");
    }

    // ── run_fix with no expired annotations ──────────────────────────────

    #[test]
    fn test_run_fix_no_expired_returns_all_zeros() {
        let dir = tempdir().unwrap();
        let file = dir.path().join("ok.rs");
        fs::write(&file, "// TODO[2099-01-01]: far future\n").unwrap();

        let cfg = crate::config::Config::default();
        let summary = run_fix(dir.path(), &cfg, today()).unwrap();

        assert_eq!(summary.extended, 0);
        assert_eq!(summary.deleted, 0);
        assert_eq!(summary.skipped, 0);
    }
}
