use std::path::PathBuf;

use anyhow::Result;
use clap::{ArgAction, Parser, ValueEnum};

mod patcher;

const PATCH_VERSION: &str = match option_env!("PATCH_BUILD_VERSION") {
    Some(v) => v,
    None => env!("CARGO_PKG_VERSION"),
};

/// 与现有 C 版 `patch` 命令保持 CLI 参数兼容的 Rust 入口。
///
/// 当前阶段仅完成参数解析和配置装配，后续会逐步将具体补丁算法和文件处理逻辑迁移到 Rust。
#[derive(Debug, Parser)]
#[command(
    name = "patch",
    disable_help_flag = true,
    disable_help_subcommand = true,
    disable_version_flag = true,
    override_usage = "patch [OPTION]... [ORIGFILE [PATCHFILE]]",
    after_help = ""
)]
struct Cli {
    /// -b / --backup
    #[arg(short = 'b', long = "backup")]
    backup: bool,

    /// -B / --prefix
    #[arg(short = 'B', long = "prefix", value_name = "PREFIX")]
    backup_prefix: Option<String>,

    /// -c / --context
    #[arg(short = 'c', long = "context")]
    context_diff: bool,

    /// -d / --directory
    #[arg(short = 'd', long = "directory", value_name = "DIR")]
    directory: Option<PathBuf>,

    /// -D / --ifdef
    #[arg(short = 'D', long = "ifdef", value_name = "NAME")]
    ifdef_name: Option<String>,

    /// -e / --ed
    #[arg(short = 'e', long = "ed")]
    ed_diff: bool,

    /// -E / --remove-empty-files
    #[arg(short = 'E', long = "remove-empty-files")]
    remove_empty_files: bool,

    /// -f / --force
    #[arg(short = 'f', long = "force")]
    force: bool,

    /// -F / --fuzz
    #[arg(short = 'F', long = "fuzz", value_name = "LINES")]
    fuzz: Option<i32>,

    /// -g / --get
    #[arg(short = 'g', long = "get", value_name = "NUM")]
    get: Option<i32>,

    /// -i / --input
    #[arg(short = 'i', long = "input", value_name = "PATCHFILE")]
    patchfile: Option<PathBuf>,

    /// -l / --ignore-whitespace
    #[arg(short = 'l', long = "ignore-whitespace")]
    ignore_whitespace: bool,

    /// --merge（如果启用）
    #[arg(long = "merge", value_name = "STYLE", num_args(0..=1), require_equals = true, default_missing_value = "merge")]
    merge: Option<Option<MergeStyle>>,

    /// -n / --normal
    #[arg(short = 'n', long = "normal")]
    normal_diff: bool,

    /// -N / --forward
    #[arg(short = 'N', long = "forward")]
    forward: bool,

    /// -o / --output
    #[arg(short = 'o', long = "output", value_name = "FILE")]
    output: Option<PathBuf>,

    /// -p / --strip
    #[arg(short = 'p', long = "strip", value_name = "NUM")]
    strip: Option<String>,

    /// -r / --reject-file
    #[arg(short = 'r', long = "reject-file", value_name = "FILE")]
    reject_file: Option<PathBuf>,

    /// -R / --reverse
    #[arg(short = 'R', long = "reverse")]
    reverse: bool,

    /// -s / --quiet/--silent
    #[arg(short = 's', long = "quiet", alias = "silent")]
    quiet: bool,

    /// -t / --batch
    #[arg(short = 't', long = "batch")]
    batch: bool,

    /// -T / --set-time
    #[arg(short = 'T', long = "set-time")]
    set_time: bool,

    /// -u / --unified
    #[arg(short = 'u', long = "unified")]
    unified_diff: bool,

    /// -v / --version
    #[arg(short = 'v', long = "version")]
    version: bool,

    /// -V / --version-control
    #[arg(short = 'V', long = "version-control", value_name = "STYLE")]
    version_control: Option<String>,

    /// -x / --debug（仅在 DEBUGGING 时有效，这里保留参数位）
    #[arg(short = 'x', long = "debug", value_name = "NUM")]
    debug: Option<i32>,

    /// -Y / --basename-prefix
    #[arg(short = 'Y', long = "basename-prefix", value_name = "PREFIX")]
    basename_prefix: Option<String>,

    /// -z / --suffix
    #[arg(short = 'z', long = "suffix", value_name = "SUFFIX")]
    backup_suffix: Option<String>,

    /// -Z / --set-utc
    #[arg(short = 'Z', long = "set-utc")]
    set_utc: bool,

    /// --dry-run
    #[arg(long = "dry-run")]
    dry_run: bool,

    /// --verbose
    #[arg(long = "verbose")]
    verbose: bool,

    /// --binary
    #[arg(long = "binary")]
    binary: bool,

    /// --help
    #[arg(long = "help", action = ArgAction::SetTrue)]
    help_flag: bool,

    /// --backup-if-mismatch
    #[arg(long = "backup-if-mismatch")]
    backup_if_mismatch: bool,

    /// --no-backup-if-mismatch
    #[arg(long = "no-backup-if-mismatch")]
    no_backup_if_mismatch: bool,

    /// --posix
    #[arg(long = "posix")]
    posix: bool,

    /// --quoting-style
    #[arg(long = "quoting-style", value_name = "WORD")]
    quoting_style: Option<String>,

    /// --reject-format
    #[arg(long = "reject-format", value_name = "FORMAT")]
    reject_format: Option<RejectFormat>,

    /// --read-only
    #[arg(long = "read-only", value_name = "BEHAVIOR")]
    read_only: Option<ReadOnlyBehavior>,

    /// --follow-symlinks
    #[arg(long = "follow-symlinks")]
    follow_symlinks: bool,

    /// 位置参数：ORIGFILE、PATCHFILE
    #[arg(value_name = "ORIGFILE")]
    origfile: Option<PathBuf>,

    #[arg(value_name = "PATCHFILE")]
    patchfile_pos: Option<PathBuf>,
}

#[derive(Debug, Clone, ValueEnum)]
enum MergeStyle {
    #[value(name = "merge")]
    Merge,
    #[value(name = "diff3")]
    Diff3,
}

#[derive(Debug, Clone, ValueEnum)]
enum RejectFormat {
    #[value(name = "context")]
    Context,
    #[value(name = "unified")]
    Unified,
}

#[derive(Debug, Clone, ValueEnum)]
enum ReadOnlyBehavior {
    #[value(name = "ignore")]
    Ignore,
    #[value(name = "warn")]
    Warn,
    #[value(name = "fail")]
    Fail,
}

fn main() -> Result<()> {
    let cli = Cli::parse();

    if cli.help_flag {
        print_help();
        return Ok(());
    }

    if cli.version {
        println!("patch (rust) {PATCH_VERSION}");
        return Ok(());
    }

    // 将 CLI 映射为内部配置结构体。
    let config = build_config_from_cli(&cli)?;

    // 当前 Rust 版本实现 unified diff 多文件补丁的主路径：
    // patch [OPTION]... [ORIGFILE [PATCHFILE]]
    // 补丁从 stdin、-i/--input 或位置参数 PATCHFILE 读取。
    let format_hint = {
        use patcher::DiffFormatHint::*;
        let mut hint = None;
        if config.context_diff {
            hint = Some(Context);
        }
        if config.ed_diff {
            hint = Some(Ed);
        }
        if config.normal_diff {
            hint = Some(Normal);
        }
        if config.unified_diff {
            hint = Some(Unified);
        }
        hint
    };

    let patch_cfg = patcher::PatchConfig {
        input_patch: config.patchfile.clone().or_else(|| config.patchfile_pos.clone()),
        origfile: config.origfile.clone(),
        output: config.output.clone(),
        dry_run: config.dry_run,
        strip: config.strip,
        reverse: config.reverse,
        reverse_specified: config.reverse,
        fuzz: config.fuzz,
        backup: config.backup,
        backup_suffix: config.backup_suffix.clone(),
        backup_prefix: config.backup_prefix.clone(),
        basename_prefix: config.basename_prefix.clone(),
        version_control: config.version_control.as_ref().and_then(|s| {
            use patcher::BackupType;
            let low = s.to_lowercase();
            match low.as_str() {
                "simple" => Some(BackupType::Simple),
                "numbered" => Some(BackupType::Numbered),
                "existing" => Some(BackupType::Existing),
                _ => None,
            }
        }),
        backup_if_mismatch: config.backup_if_mismatch,
        no_backup_if_mismatch: config.no_backup_if_mismatch,
        merge: config.merge,
        force: config.force,
        batch: config.batch,
        noreverse: config.forward,
        reject_file: config.reject_file.clone(),
        directory: config.directory.clone(),
        patch_get: config.get,
        set_time: config.set_time,
        set_utc: config.set_utc,
        verbose: config.verbose,
        quiet: config.quiet,
        format_hint,
        read_only: match config.read_only {
            Some(ReadOnlyBehavior::Ignore) => Some(patcher::ReadOnlyMode::Ignore),
            Some(ReadOnlyBehavior::Warn) => Some(patcher::ReadOnlyMode::Warn),
            Some(ReadOnlyBehavior::Fail) => Some(patcher::ReadOnlyMode::Fail),
            None => None,
        },
        reject_format: match config.reject_format {
            Some(RejectFormat::Context) => Some(patcher::RejectFormat::Context),
            Some(RejectFormat::Unified) => Some(patcher::RejectFormat::Unified),
            None => None,
        },
        ifdef_name: config.ifdef_name.clone(),
        remove_empty_files: config.remove_empty_files,
        ignore_whitespace: config.ignore_whitespace,
        quoting_style: config.quoting_style.clone(),
        follow_symlinks: config.follow_symlinks,
        binary: config.binary,
        posix: config._posix,
    };

    let exit = patcher::apply_patches(&patch_cfg);
    // 模拟 C 版返回码：0 正常，1 有 hunk 失败，2 其他错误。
    std::process::exit(exit as i32);
}

#[derive(Debug)]
struct Config {
    backup: bool,
    backup_prefix: Option<String>,
    backup_suffix: Option<String>,
    basename_prefix: Option<String>,
    version_control: Option<String>,
    backup_if_mismatch: bool,
    no_backup_if_mismatch: bool,
    context_diff: bool,
    directory: Option<PathBuf>,
    ifdef_name: Option<String>,
    ed_diff: bool,
    remove_empty_files: bool,
    force: bool,
    fuzz: Option<i32>,
    get: Option<i32>,
    patchfile: Option<PathBuf>,
    ignore_whitespace: bool,
    merge: bool,
    normal_diff: bool,
    forward: bool,
    output: Option<PathBuf>,
    strip: Option<i32>,
    reject_file: Option<PathBuf>,
    reverse: bool,
    quiet: bool,
    batch: bool,
    set_time: bool,
    set_utc: bool,
    unified_diff: bool,
    verbose: bool,
    dry_run: bool,
    binary: bool,
    _posix: bool,
    quoting_style: Option<String>,
    reject_format: Option<RejectFormat>,
    read_only: Option<ReadOnlyBehavior>,
    follow_symlinks: bool,
    origfile: Option<PathBuf>,
    patchfile_pos: Option<PathBuf>,
}

fn build_config_from_cli(cli: &Cli) -> Result<Config> {
    // 这里主要做与 C 版本一致的标志组合和约束校验，
    // 暂时只做简单映射，复杂行为在后续迁移中补齐。

    let version_control = cli
        .version_control
        .clone()
        .or_else(|| std::env::var("PATCH_VERSION_CONTROL").ok())
        .or_else(|| std::env::var("VERSION_CONTROL").ok());
    // GNU：未显式指定时，POSIX 下默认 no-backup-if-mismatch，否则默认 backup-if-mismatch。
    let (backup_if_mismatch, no_backup_if_mismatch) = if cli.backup_if_mismatch || cli.no_backup_if_mismatch {
        (cli.backup_if_mismatch, cli.no_backup_if_mismatch)
    } else {
        (!cli.posix, cli.posix)
    };
    let resolved_quoting = cli
        .quoting_style
        .clone()
        .or_else(|| std::env::var("QUOTING_STYLE").ok())
        .or(Some("shell".to_string()));
    let strip = match cli.strip.as_deref() {
        Some(s) => match s.parse::<i32>() {
            Ok(v) => Some(v),
            Err(_) => {
                let prog = std::env::args().next().unwrap_or_else(|| "patch".to_string());
                eprintln!("{}: **** strip count {} is not a number", prog, s);
                std::process::exit(2);
            }
        },
        None => None,
    };

    Ok(Config {
        backup: cli.backup,
        backup_suffix: cli
            .backup_suffix
            .clone()
            .or_else(|| std::env::var("SIMPLE_BACKUP_SUFFIX").ok()),
        backup_prefix: cli.backup_prefix.clone(),
        basename_prefix: cli.basename_prefix.clone(),
        version_control,
        backup_if_mismatch,
        no_backup_if_mismatch,
        context_diff: cli.context_diff,
        directory: cli.directory.clone(),
        ifdef_name: cli.ifdef_name.clone(),
        ed_diff: cli.ed_diff,
        remove_empty_files: cli.remove_empty_files,
        force: cli.force,
        fuzz: cli.fuzz,
        get: cli.get,
        patchfile: cli.patchfile.clone(),
        ignore_whitespace: cli.ignore_whitespace,
        merge: cli.merge.is_some(),
        normal_diff: cli.normal_diff,
        forward: cli.forward,
        output: cli.output.clone(),
        strip,
        reject_file: cli.reject_file.clone(),
        reverse: cli.reverse,
        quiet: cli.quiet,
        batch: cli.batch,
        set_time: cli.set_time,
        set_utc: cli.set_utc,
        unified_diff: cli.unified_diff,
        verbose: cli.verbose,
        dry_run: cli.dry_run,
        binary: cli.binary,
        _posix: cli.posix,
        quoting_style: resolved_quoting,
        reject_format: cli.reject_format.clone(),
        read_only: cli.read_only.clone(),
        follow_symlinks: cli.follow_symlinks,
        origfile: cli.origfile.clone(),
        patchfile_pos: cli.patchfile_pos.clone(),
    })
}

fn print_help() {
    // 复用原 C 版 help 文本，确保兼容。
    // 文本来源：src/patch.c 中 option_help 数组。
    println!("Usage: patch [OPTION]... [ORIGFILE [PATCHFILE]]\n");
    let option_help = [
        "Input options:",
        "",
        "  -p NUM  --strip=NUM  Strip NUM leading components from file names.",
        "  -F LINES  --fuzz LINES  Set the fuzz factor to LINES for inexact matching.",
        "  -l  --ignore-whitespace  Ignore white space changes between patch and input.",
        "",
        "  -c  --context  Interpret the patch as a context difference.",
        "  -e  --ed  Interpret the patch as an ed script.",
        "  -n  --normal  Interpret the patch as a normal difference.",
        "  -u  --unified  Interpret the patch as a unified difference.",
        "",
        "  -N  --forward  Ignore patches that appear to be reversed or already applied.",
        "  -R  --reverse  Assume patches were created with old and new files swapped.",
        "",
        "  -i PATCHFILE  --input=PATCHFILE  Read patch from PATCHFILE instead of stdin.",
        "",
        "Output options:",
        "",
        "  -o FILE  --output=FILE  Output patched files to FILE.",
        "  -r FILE  --reject-file=FILE  Output rejects to FILE.",
        "",
        "  -D NAME  --ifdef=NAME  Make merged if-then-else output using NAME.",
        "  --merge  Merge using conflict markers instead of creating reject files.",
        "  -E  --remove-empty-files  Remove output files that are empty after patching.",
        "",
        "  -Z  --set-utc  Set times of patched files, assuming diff uses UTC (GMT).",
        "  -T  --set-time  Likewise, assuming local time.",
        "",
        "  --quoting-style=WORD   output file names using quoting style WORD.",
        "    Valid WORDs are: literal, shell, shell-always, c, escape.",
        "    Default is taken from QUOTING_STYLE env variable, or 'shell' if unset.",
        "",
        "Backup and version control options:",
        "",
        "  -b  --backup  Back up the original contents of each file.",
        "  --backup-if-mismatch  Back up if the patch does not match exactly.",
        "  --no-backup-if-mismatch  Back up mismatches only if otherwise requested.",
        "",
        "  -V STYLE  --version-control=STYLE  Use STYLE version control.",
        "\tSTYLE is either 'simple', 'numbered', or 'existing'.",
        "  -B PREFIX  --prefix=PREFIX  Prepend PREFIX to backup file names.",
        "  -Y PREFIX  --basename-prefix=PREFIX  Prepend PREFIX to backup file basenames.",
        "  -z SUFFIX  --suffix=SUFFIX  Append SUFFIX to backup file names.",
        "",
        "  -g NUM  --get=NUM  Get files from RCS etc. if positive; ask if negative.",
        "",
        "Miscellaneous options:",
        "",
        "  -t  --batch  Ask no questions; skip bad-Prereq patches; assume reversed.",
        "  -f  --force  Like -t, but ignore bad-Prereq patches, and assume unreversed.",
        "  -s  --quiet  --silent  Work silently unless an error occurs.",
        "  --verbose  Output extra information about the work being done.",
        "  --dry-run  Do not actually change any files; just print what would happen.",
        "  --posix  Conform to the POSIX standard.",
        "",
        "  -d DIR  --directory=DIR  Change the working directory to DIR first.",
        "  --reject-format=FORMAT  Create 'context' or 'unified' rejects.",
        "  --binary  Read and write data in binary mode.",
        "  --read-only=BEHAVIOR  How to handle read-only input files: 'ignore' that they",
        "                        are read-only, 'warn' (default), or 'fail'.",
        "",
        "  -v  --version  Output version info.",
        "  --help  Output this help.",
        "",
    ];

    for line in option_help {
        println!("{line}");
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn clear_envs() {
        for key in [
            "PATCH_VERSION_CONTROL",
            "VERSION_CONTROL",
            "QUOTING_STYLE",
            "SIMPLE_BACKUP_SUFFIX",
        ] {
            std::env::remove_var(key);
        }
    }

    #[test]
    fn build_config_uses_gnu_defaults_when_not_posix() {
        let _guard = env_lock().lock().expect("env lock poisoned");
        clear_envs();

        let cli = Cli::parse_from(["patch"]);
        let cfg = build_config_from_cli(&cli).expect("build config should succeed");

        assert!(cfg.backup_if_mismatch);
        assert!(!cfg.no_backup_if_mismatch);
        assert_eq!(cfg.quoting_style.as_deref(), Some("shell"));
        assert_eq!(cfg.strip, None);
    }

    #[test]
    fn build_config_posix_defaults_disable_backup_if_mismatch() {
        let _guard = env_lock().lock().expect("env lock poisoned");
        clear_envs();

        let cli = Cli::parse_from(["patch", "--posix"]);
        let cfg = build_config_from_cli(&cli).expect("build config should succeed");

        assert!(!cfg.backup_if_mismatch);
        assert!(cfg.no_backup_if_mismatch);
    }

    #[test]
    fn build_config_reads_env_fallbacks() {
        let _guard = env_lock().lock().expect("env lock poisoned");
        clear_envs();
        std::env::set_var("VERSION_CONTROL", "existing");
        std::env::set_var("QUOTING_STYLE", "c");
        std::env::set_var("SIMPLE_BACKUP_SUFFIX", ".bak");

        let cli = Cli::parse_from(["patch"]);
        let cfg = build_config_from_cli(&cli).expect("build config should succeed");

        assert_eq!(cfg.version_control.as_deref(), Some("existing"));
        assert_eq!(cfg.quoting_style.as_deref(), Some("c"));
        assert_eq!(cfg.backup_suffix.as_deref(), Some(".bak"));

        clear_envs();
    }

    #[test]
    fn cli_values_override_env() {
        let _guard = env_lock().lock().expect("env lock poisoned");
        clear_envs();
        std::env::set_var("PATCH_VERSION_CONTROL", "simple");
        std::env::set_var("QUOTING_STYLE", "escape");

        let cli = Cli::parse_from([
            "patch",
            "--version-control",
            "numbered",
            "--quoting-style",
            "literal",
            "-p",
            "3",
        ]);
        let cfg = build_config_from_cli(&cli).expect("build config should succeed");

        assert_eq!(cfg.version_control.as_deref(), Some("numbered"));
        assert_eq!(cfg.quoting_style.as_deref(), Some("literal"));
        assert_eq!(cfg.strip, Some(3));

        clear_envs();
    }
}

#[cfg(test)]
mod help_tests {
    use super::*;

    #[test]
    fn print_help_executes_full_text() {
        print_help();
    }
}
