use crate::{config::Config, files};
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use std::path::{Path, PathBuf};

const STAGING_DIR_PREFIX: &str = "homux.staging.";

#[derive(Debug, Clone, Parser)]
pub struct Args {
    /// Selections for matchpicking (instead of hostname)
    #[arg(short = 's', long)]
    select: Vec<String>,
    /// Dry run (apply to temporary directory instead of home directory)
    #[arg(short = 'd', long)]
    dry_run: bool,
    /// Print more verbose output
    #[arg(short = 'v', long)]
    verbose: bool,
}

pub fn apply(args: &Args, config: &Config) -> Result<()> {
    let target_dir = if args.dry_run {
        config.dirs.dry_run.clone()
    } else {
        config.dirs.home.clone()
    };
    let matchpick_select = if args.select.is_empty() {
        vec![config.hostname.clone()]
    } else {
        args.select.clone()
    };

    let staging_dir = get_staging_dir()?;
    println!(
        "{:>12} at directory: {}",
        "Staging".green().bold(),
        staging_dir.display()
    );
    stage_source(config, &staging_dir, &matchpick_select, args.verbose).context("stage source")?;
    println!(
        "{:>12} to target directory: {}",
        "Applying".green().bold(),
        target_dir.display()
    );
    let apply_result = files::copy_directory_full(&staging_dir, &target_dir)
        .context("copy staging recursively to target");
    println!(
        "{:>12} up staging directory: {}",
        "Cleaning".green().bold(),
        staging_dir.display()
    );
    cleanup_staging(&staging_dir)?;
    apply_result?;
    println!("{:>12}", "Done".green().bold());
    Ok(())
}

fn get_staging_dir() -> Result<PathBuf> {
    let dir = std::env::temp_dir().join(format!("{STAGING_DIR_PREFIX}{}", std::process::id()));
    std::fs::create_dir_all(&dir).context("failed to create staging directory")?;
    Ok(dir)
}

fn cleanup_staging(staging_dir: &Path) -> Result<()> {
    std::fs::remove_dir_all(staging_dir).with_context(|| {
        format!(
            "failed to remove staging directory {}",
            staging_dir.display()
        )
    })?;
    Ok(())
}

fn stage_source(
    config: &Config,
    staging_dir: &Path,
    matchpick_select: &[String],
    verbose: bool,
) -> Result<()> {
    let source_dir_contents = files::walk_dir(&config.dirs.source)?;
    for dir_path in source_dir_contents.dirs {
        let relative_path = files::get_relative_path(&config.dirs.source, &dir_path)
            .with_context(|| format!("non-relative path: {}", dir_path.display()))?;
        let staging_path = staging_dir.join(relative_path);
        std::fs::create_dir_all(&staging_path).context("failed to create staging subdirectory")?;
    }
    let mut variables = config
        .variables
        .clone()
        .into_iter()
        .collect::<Vec<(String, String)>>();
    variables.sort_by_key(|x| std::cmp::Reverse(x.0.len()));
    for file_path in source_dir_contents.files {
        let filesize = std::fs::metadata(&file_path)
            .with_context(|| format!("failed to read file metadata: {}", file_path.display()))?
            .len();
        let relative_path = files::get_relative_path(&config.dirs.source, &file_path)
            .with_context(|| format!("non-relative path: {}", file_path.display()))?;
        if verbose {
            print!(
                "  {} {}",
                "Processing".green().dimmed(),
                relative_path.display()
            );
        }
        // Determine if file is utf8
        let bytes = std::fs::read(&file_path)
            .with_context(|| format!("failed to read file {}", file_path.display()))?;
        let text_option = String::from_utf8(bytes)
            .map_err(|utf8error| {
                if verbose {
                    print!("{}", format!(" not utf8: [{utf8error}]").yellow().bold());
                }
            })
            .ok();

        let staging_path = staging_dir.join(&relative_path);
        if let Some(text) = text_option {
            // Matchpick
            let mut text = if filesize <= config.matchpick.max_file_size {
                if text.contains(&config.matchpick.start_pattern) {
                    if verbose {
                        print!("{}", " [matchpicking]".yellow().bold());
                    }
                    matchpick::process(
                        &text,
                        matchpick_select.to_owned(),
                        &config.matchpick.start_pattern,
                        &config.matchpick.end_pattern,
                        config.matchpick.ignore_pattern.clone(),
                    )?
                } else {
                    text
                }
            } else {
                text
            };
            // Replace variables
            let mut inserted = false;
            let file_should_be_ignored = config
                .variable_ignore
                .files
                .iter()
                .any(|pattern| relative_path.to_string_lossy().contains(pattern));

            if !file_should_be_ignored {
                if let Some(line_pattern) = &config.variable_ignore.line_pattern {
                    let lines: Vec<&str> = text.lines().collect();
                    let mut processed_lines = Vec::new();

                    for line in lines {
                        if line.contains(line_pattern) {
                            processed_lines.push(line.to_string());
                        } else {
                            let mut processed_line = line.to_string();
                            for (key, value) in &variables {
                                if processed_line.contains(key) {
                                    processed_line = processed_line.replace(key, value);
                                    inserted = true;
                                }
                            }
                            processed_lines.push(processed_line);
                        }
                    }
                    text = processed_lines.join("\n");
                } else {
                    for (key, value) in &variables {
                        if text.contains(key) {
                            text = text.replace(key, value);
                            inserted = true;
                        }
                    }
                }
            }

            if verbose && inserted {
                print!("{}", " [inserted variables]".yellow().bold());
            } else if verbose && file_should_be_ignored {
                print!("{}", " [variables ignored]".yellow().bold());
            }
            std::fs::write(&staging_path, text).context("failed to write to staging dir")?;
        } else {
            std::fs::copy(&file_path, &staging_path).context("failed to copy to staging dir")?;
        }
        files::copy_file_mode(&file_path, &staging_path)?;

        if verbose {
            println!();
        }
    }
    Ok(())
}
