use crate::args::{Args, FormatArg};
use crate::config::{load_config, save_user_config};
use crate::interactive::run_interactive;
use crate::io::{print_wrote, read_stdin_with, write_output_with_tty};
use crate::parse::{
    parse_box, parse_font_dirs, parse_font_fallbacks, parse_lines, parse_timeout_ms,
};
use crate::tmux::{capture_tmux_output, tmux_title};
use clap::{CommandFactory, Parser};
use cryosnap_core::{InputSource, OutputFormat, RenderRequest};
use std::error::Error;
use std::path::{Path, PathBuf};

pub(crate) fn run() -> Result<(), Box<dyn Error>> {
    let args = Args::parse();
    run_with(
        args,
        atty::is(atty::Stream::Stdin),
        atty::is(atty::Stream::Stdout),
        None,
    )
}

pub(crate) fn run_with(
    args: Args,
    stdin_is_tty: bool,
    stdout_is_tty: bool,
    stdin_override: Option<&str>,
) -> Result<(), Box<dyn Error>> {
    let (mut config, is_default_config) = load_config(args.config.as_deref())?;
    let mut quantize_set = false;
    if let Some(background) = args.background {
        config.background = background;
    }
    if let Some(padding) = args.padding {
        config.padding = parse_box(&padding)?;
    }
    if let Some(margin) = args.margin {
        config.margin = parse_box(&margin)?;
    }
    if let Some(width) = args.width {
        config.width = width;
    }
    if let Some(height) = args.height {
        config.height = height;
    }
    if let Some(theme) = args.theme {
        config.theme = theme;
    }
    if let Some(language) = args.language {
        config.language = Some(language);
    }
    if let Some(wrap) = args.wrap {
        config.wrap = wrap;
    }
    if let Some(lines) = args.lines {
        config.lines = parse_lines(&lines)?;
    }
    if let Some(window) = args.window {
        config.window_controls = window;
    }
    if let Some(show) = args.show_line_numbers {
        config.show_line_numbers = show;
    }
    if let Some(radius) = args.border_radius {
        config.border.radius = radius;
    }
    if let Some(width) = args.border_width {
        config.border.width = width;
    }
    if let Some(color) = args.border_color {
        config.border.color = color;
    }
    if let Some(blur) = args.shadow_blur {
        config.shadow.blur = blur;
    }
    if let Some(x) = args.shadow_x {
        config.shadow.x = x;
    }
    if let Some(y) = args.shadow_y {
        config.shadow.y = y;
    }
    if let Some(family) = args.font_family {
        config.font.family = family;
    }
    if let Some(file) = args.font_file {
        config.font.file = Some(file);
    }
    if let Some(fallbacks) = args.font_fallbacks {
        config.font.fallbacks = parse_font_fallbacks(&fallbacks)?;
    }
    if let Some(dirs) = args.font_dirs {
        config.font.dirs = parse_font_dirs(&dirs)?;
    }
    if let Some(region) = args.font_cjk_region {
        config.font.cjk_region = region.into();
    }
    if let Some(auto_download) = args.font_auto_download {
        config.font.auto_download = auto_download;
    }
    if let Some(force_update) = args.font_force_update {
        config.font.force_update = force_update;
    }
    if let Some(size) = args.font_size {
        config.font.size = size;
    }
    if let Some(line_height) = args.line_height {
        config.line_height = line_height;
    }
    if let Some(scale) = args.raster_scale {
        config.raster.scale = scale;
    }
    if let Some(max_pixels) = args.raster_max_pixels {
        config.raster.max_pixels = max_pixels;
    }
    if let Some(backend) = args.raster_backend {
        config.raster.backend = backend.into();
    }
    if let Some(ligatures) = args.font_ligatures {
        config.font.ligatures = ligatures;
    }
    if let Some(mode) = args.font_system_fallback {
        config.font.system_fallback = mode.into();
    }
    if let Some(timeout) = args.execute_timeout {
        config.execute_timeout_ms = parse_timeout_ms(&timeout)?;
    }
    if let Some(optimize) = args.png_opt {
        config.png.optimize = optimize;
    }
    if let Some(level) = args.png_opt_level {
        config.png.level = level;
    }
    if let Some(strip) = args.png_strip {
        config.png.strip = strip.into();
    }
    if let Some(quantize) = args.png_quant {
        config.png.quantize = quantize;
        quantize_set = true;
    }
    if let Some(quality) = args.png_quant_quality {
        config.png.quantize_quality = quality;
    }
    if let Some(speed) = args.png_quant_speed {
        config.png.quantize_speed = speed;
    }
    if let Some(dither) = args.png_quant_dither {
        config.png.quantize_dither = dither;
    }
    if let Some(preset) = args.png_quant_preset {
        config.png.quantize_preset = Some(preset.into());
        if !quantize_set {
            config.png.quantize = true;
        }
    }
    if let Some(enabled) = args.title {
        config.title.enabled = enabled;
    }
    if let Some(text) = args.title_text {
        config.title.text = Some(text);
    }
    if let Some(style) = args.title_path_style {
        config.title.path_style = style.into();
    }
    if let Some(format) = args.title_tmux_format {
        config.title.tmux_format = format;
    }
    if let Some(align) = args.title_align {
        config.title.align = align.into();
    }
    if let Some(size) = args.title_size {
        config.title.size = size;
    }
    if let Some(color) = args.title_color {
        config.title.color = color;
    }
    if let Some(opacity) = args.title_opacity {
        config.title.opacity = opacity;
    }
    if let Some(max_width) = args.title_max_width {
        config.title.max_width = max_width;
    }
    if let Some(ellipsis) = args.title_ellipsis {
        config.title.ellipsis = ellipsis;
    }

    if args.tmux {
        if args.execute.is_some() || args.input.is_some() {
            return Err("tmux mode cannot be combined with --execute or input".into());
        }
        if args.interactive {
            return Err("tmux mode cannot be combined with --interactive".into());
        }
    }

    let mut input_arg = args.input.clone();
    let mut execute_arg = args.execute.clone();
    if args.interactive {
        if !stdin_is_tty {
            return Err("interactive mode requires a TTY".into());
        }
        run_interactive(&mut config, &mut input_arg, &mut execute_arg)?;
        if is_default_config {
            save_user_config(&config)?;
        }
    }

    let input_for_output = if args.tmux { None } else { input_arg.clone() };
    let input = if args.tmux {
        let tmux_output = capture_tmux_output(args.tmux_args.as_deref())?;
        if config.language.is_none() {
            config.language = Some("ansi".to_string());
        }
        if config.title.enabled {
            let should_fill = config
                .title
                .text
                .as_ref()
                .map(|text| text.trim().is_empty())
                .unwrap_or(true);
            if should_fill {
                if let Some(title) =
                    tmux_title(args.tmux_args.as_deref(), &config.title.tmux_format)
                {
                    config.title.text = Some(title);
                }
            }
        }
        InputSource::Text(tmux_output)
    } else if let Some(cmd) = execute_arg {
        InputSource::Command(cmd)
    } else if let Some(input) = input_arg {
        if input == "-" {
            InputSource::Text(read_stdin_with(stdin_override)?)
        } else {
            InputSource::File(PathBuf::from(input))
        }
    } else if !stdin_is_tty {
        InputSource::Text(read_stdin_with(stdin_override)?)
    } else {
        let mut cmd = Args::command();
        cmd.print_help()?;
        println!();
        return Ok(());
    };

    if let Some(output) = args.output.as_ref() {
        if let Some(expanded) = expand_output_pattern(output)? {
            if args.format.is_some() {
                return Err("output patterns cannot be combined with --format".into());
            }
            let mut outputs = Vec::with_capacity(expanded.len());
            for path in expanded {
                let format = format_from_extension(&path)
                    .ok_or_else(|| format!("unknown output format: {}", path.display()))?;
                outputs.push((path, format));
            }

            let wants_png = outputs
                .iter()
                .any(|(_, format)| matches!(format, OutputFormat::Png));
            let wants_webp = outputs
                .iter()
                .any(|(_, format)| matches!(format, OutputFormat::Webp));

            let planned = cryosnap_core::render_svg_planned(&input, &config)?;
            let svg = planned.bytes;
            let png_webp = if wants_png && wants_webp {
                Some(cryosnap_core::render_png_webp_from_svg_once(
                    &svg,
                    &config,
                    planned.needs_system_fonts,
                )?)
            } else {
                None
            };

            for (path, format) in outputs {
                let bytes = match format {
                    OutputFormat::Svg => svg.clone(),
                    OutputFormat::Png => match &png_webp {
                        Some((png, _)) => png.clone(),
                        None => cryosnap_core::render_png_from_svg(&svg, &config)?,
                    },
                    OutputFormat::Webp => match &png_webp {
                        Some((_, webp)) => webp.clone(),
                        None => cryosnap_core::render_webp_from_svg(&svg, &config)?,
                    },
                };
                std::fs::write(&path, bytes)?;
                if stdout_is_tty {
                    print_wrote(&path);
                }
            }
            return Ok(());
        }
    }

    let mut format_arg = args.format;
    let mut format = resolve_format(format_arg, args.output.as_ref());
    if stdout_is_tty && args.output.is_none() && format_arg.is_none() {
        format = OutputFormat::Png;
        format_arg = Some(FormatArg::Png);
    }
    let request = RenderRequest {
        input,
        config,
        format,
    };

    let result = cryosnap_core::render(&request)?;

    write_output_with_tty(
        result,
        args.output.as_ref(),
        input_for_output.as_deref(),
        format_arg,
        stdout_is_tty,
    )?;

    Ok(())
}

fn resolve_format(arg: Option<FormatArg>, output: Option<&PathBuf>) -> OutputFormat {
    if let Some(arg) = arg {
        return match arg {
            FormatArg::Svg => OutputFormat::Svg,
            FormatArg::Png => OutputFormat::Png,
            FormatArg::Webp => OutputFormat::Webp,
        };
    }
    if let Some(path) = output {
        if let Some(format) = format_from_extension(path) {
            return format;
        }
    }
    OutputFormat::Svg
}

fn format_from_extension(path: &Path) -> Option<OutputFormat> {
    let ext = path.extension().and_then(|v| v.to_str())?;
    match ext.to_ascii_lowercase().as_str() {
        "png" => Some(OutputFormat::Png),
        "svg" => Some(OutputFormat::Svg),
        "webp" => Some(OutputFormat::Webp),
        _ => None,
    }
}

fn expand_output_pattern(output: &Path) -> Result<Option<Vec<PathBuf>>, Box<dyn Error>> {
    let output = output.to_string_lossy();
    let open = output.find('{');
    let close = output.find('}');

    match (open, close) {
        (None, None) => Ok(None),
        (Some(_), None) | (None, Some(_)) => Err("invalid output pattern".into()),
        (Some(start), Some(end)) => {
            if end <= start {
                return Err("invalid output pattern".into());
            }
            if output[start + 1..].contains('{') || output[end + 1..].contains('}') {
                return Err("invalid output pattern".into());
            }
            let inner = &output[start + 1..end];
            let parts: Vec<&str> = inner
                .split(',')
                .map(|s| s.trim())
                .filter(|s| !s.is_empty())
                .collect();
            if parts.is_empty() {
                return Err("invalid output pattern".into());
            }
            let prefix = &output[..start];
            let suffix = &output[end + 1..];
            let mut outputs = Vec::new();
            for part in parts {
                outputs.push(PathBuf::from(format!("{prefix}{part}{suffix}")));
            }
            Ok(Some(outputs))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::args::{
        FontCjkRegionArg, FontSystemFallbackArg, PngQuantPresetArg, PngStripArg, RasterBackendArg,
        TitleAlignArg, TitlePathStyleArg,
    };
    use crate::test_utils::{cwd_lock, env_lock};
    use tempfile::tempdir;

    #[test]
    fn resolve_format_from_output() {
        let out = resolve_format(None, Some(&PathBuf::from("out.png")));
        assert!(matches!(out, OutputFormat::Png));
        let out = resolve_format(None, Some(&PathBuf::from("out.svg")));
        assert!(matches!(out, OutputFormat::Svg));
        let out = resolve_format(None, Some(&PathBuf::from("out.webp")));
        assert!(matches!(out, OutputFormat::Webp));
    }

    #[test]
    fn resolve_format_from_arg() {
        let out = resolve_format(Some(FormatArg::Png), None);
        assert!(matches!(out, OutputFormat::Png));
    }

    #[test]
    fn format_from_extension_unknown() {
        assert!(format_from_extension(Path::new("out.txt")).is_none());
    }

    #[test]
    fn expand_output_pattern_basic() {
        let outputs = expand_output_pattern(&PathBuf::from("out.{svg,png,webp}"))
            .expect("expand")
            .expect("outputs");
        let names: Vec<String> = outputs
            .iter()
            .map(|p| p.to_string_lossy().to_string())
            .collect();
        assert_eq!(names, vec!["out.svg", "out.png", "out.webp"]);
    }

    #[test]
    fn expand_output_pattern_invalid() {
        let err = expand_output_pattern(&PathBuf::from("out.{svg,png")).err();
        assert!(err.is_some());
    }

    #[test]
    fn expand_output_pattern_invalid_variants() {
        assert!(expand_output_pattern(Path::new("out.}{")).is_err());
        assert!(expand_output_pattern(Path::new("out.{svg,{png}}")).is_err());
        assert!(expand_output_pattern(Path::new("out.{}")).is_err());
    }

    #[test]
    fn run_with_rejects_tmux_execute_combo() {
        let mut args = Args::parse_from(["cryosnap"]);
        args.tmux = true;
        args.execute = Some("echo hi".to_string());
        let err = run_with(args, true, false, None).unwrap_err();
        assert!(err.to_string().contains("tmux mode"));
    }

    #[test]
    fn run_with_interactive_requires_tty() {
        let mut args = Args::parse_from(["cryosnap"]);
        args.interactive = true;
        let err = run_with(args, false, false, None).unwrap_err();
        assert!(err.to_string().contains("interactive mode requires a TTY"));
    }

    #[test]
    fn run_with_output_pattern_conflicts_with_format() {
        let dir = tempdir().expect("temp dir");
        let mut args = Args::parse_from(["cryosnap"]);
        args.input = Some("-".to_string());
        args.output = Some(dir.path().join("out.{svg,png}"));
        args.format = Some(FormatArg::Svg);
        let err = run_with(args, false, false, Some("hello")).unwrap_err();
        assert!(err
            .to_string()
            .contains("output patterns cannot be combined"));
    }

    #[test]
    fn run_with_reads_stdin_when_piped() {
        let dir = tempdir().expect("temp dir");
        let out_path = dir.path().join("out.svg");
        let mut args = Args::parse_from(["cryosnap"]);
        args.output = Some(out_path.clone());
        args.png_quant = Some(true);
        let result = run_with(args, false, false, Some("hello"));
        assert!(result.is_ok());
        let content = std::fs::read_to_string(out_path).expect("read svg");
        assert!(content.contains("<svg"));
    }

    #[test]
    fn run_with_applies_many_overrides() {
        let dir = tempdir().expect("temp dir");
        let out_path = dir.path().join("out.svg");
        let mut args = Args::parse_from(["cryosnap"]);
        args.input = Some("-".to_string());
        args.output = Some(out_path.clone());
        args.background = Some("#101010".to_string());
        args.padding = Some("1,2,3,4".to_string());
        args.margin = Some("5,6".to_string());
        args.width = Some(800.0);
        args.height = Some(600.0);
        args.theme = Some("charm".to_string());
        args.language = Some("rust".to_string());
        args.wrap = Some(80);
        args.lines = Some("1,2".to_string());
        args.window = Some(true);
        args.show_line_numbers = Some(true);
        args.border_radius = Some(4.0);
        args.border_width = Some(1.0);
        args.border_color = Some("#333333".to_string());
        args.shadow_blur = Some(6.0);
        args.shadow_x = Some(1.0);
        args.shadow_y = Some(2.0);
        args.font_family = Some("monospace".to_string());
        args.font_size = Some(12.0);
        args.line_height = Some(1.4);
        args.raster_scale = Some(2.0);
        args.raster_max_pixels = Some(1_000_000);
        args.raster_backend = Some(RasterBackendArg::Resvg);
        args.font_ligatures = Some(false);
        args.execute_timeout = Some("500ms".to_string());
        args.png_opt = Some(false);
        args.png_opt_level = Some(3);
        args.png_strip = Some(PngStripArg::All);
        args.png_quant_quality = Some(80);
        args.png_quant_speed = Some(5);
        args.png_quant_dither = Some(0.7);
        args.png_quant_preset = Some(PngQuantPresetArg::Fast);
        args.title = Some(true);
        args.title_text = Some("Title".to_string());
        args.title_path_style = Some(TitlePathStyleArg::Basename);
        args.title_tmux_format = Some("format".to_string());
        args.title_align = Some(TitleAlignArg::Right);
        args.title_size = Some(10.0);
        args.title_color = Some("#ffffff".to_string());
        args.title_opacity = Some(0.7);
        args.title_max_width = Some(30);
        args.title_ellipsis = Some("..".to_string());

        let result = run_with(args, false, false, Some("hello"));
        assert!(result.is_ok());
        assert!(out_path.exists());
    }

    fn asset_path(name: &str) -> PathBuf {
        let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
        manifest.join("..").join("..").join("assets").join(name)
    }

    #[test]
    fn run_with_font_overrides_and_file_input() {
        let _lock = env_lock().lock().expect("lock");
        let dir = tempdir().expect("temp dir");
        let input_path = dir.path().join("input.txt");
        let out_path = dir.path().join("out.svg");
        let fonts_dir = dir.path().join("fonts");
        std::fs::create_dir_all(&fonts_dir).expect("fonts dir");
        std::fs::write(&input_path, "hello").expect("write");
        let font_file = asset_path("JetBrainsMono-Regular.ttf");

        let mut args = Args::parse_from(["cryosnap"]);
        args.input = Some(input_path.to_string_lossy().to_string());
        args.output = Some(out_path.clone());
        args.font_file = Some(font_file.to_string_lossy().to_string());
        args.font_fallbacks = Some("Noto Sans CJK SC".to_string());
        args.font_dirs = Some(fonts_dir.to_string_lossy().to_string());
        args.font_cjk_region = Some(FontCjkRegionArg::Jp);
        args.font_auto_download = Some(false);
        args.font_force_update = Some(true);
        args.font_system_fallback = Some(FontSystemFallbackArg::Never);

        let result = run_with(args, true, false, None);
        assert!(result.is_ok());
        assert!(out_path.exists());
    }

    #[test]
    fn run_with_output_pattern_writes_multiple() {
        let dir = tempdir().expect("temp dir");
        let mut args = Args::parse_from(["cryosnap"]);
        args.input = Some("-".to_string());
        args.output = Some(dir.path().join("out.{svg,png}"));
        let result = run_with(args, false, false, Some("hello"));
        assert!(result.is_ok());
        assert!(dir.path().join("out.svg").exists());
        assert!(dir.path().join("out.png").exists());
    }

    #[test]
    fn run_with_output_pattern_writes_webp_and_prints() {
        let dir = tempdir().expect("temp dir");
        let mut args = Args::parse_from(["cryosnap"]);
        args.input = Some("-".to_string());
        args.output = Some(dir.path().join("out.{svg,webp}"));
        let result = run_with(args, false, true, Some("hello"));
        assert!(result.is_ok());
        assert!(dir.path().join("out.svg").exists());
        assert!(dir.path().join("out.webp").exists());
    }

    #[test]
    fn run_with_help_when_no_input_and_tty() {
        let args = Args::parse_from(["cryosnap"]);
        let result = run_with(args, true, false, None);
        assert!(result.is_ok());
    }

    #[test]
    fn run_with_stdout_tty_default_png() {
        let _lock = cwd_lock().lock().expect("lock");
        let dir = tempdir().expect("temp dir");
        let cwd = std::env::current_dir().expect("cwd");
        std::env::set_current_dir(dir.path()).expect("chdir");

        let args = Args::parse_from(["cryosnap"]);
        let result = run_with(args, false, true, Some("hello"));
        assert!(result.is_ok());
        assert!(dir.path().join("cryosnap.png").exists());

        std::env::set_current_dir(cwd).expect("restore");
    }

    #[cfg(unix)]
    #[test]
    fn run_with_execute_command_output() {
        let _lock = env_lock().lock().expect("lock");
        let dir = tempdir().expect("temp dir");
        let out_path = dir.path().join("out.svg");
        let mut args = Args::parse_from(["cryosnap"]);
        args.execute = Some("printf 'hello'".to_string());
        args.output = Some(out_path.clone());
        let result = run_with(args, true, false, None);
        assert!(result.is_ok());
        let content = std::fs::read_to_string(out_path).expect("read");
        assert!(content.contains("<svg"));
    }

    #[cfg(unix)]
    #[test]
    fn run_with_tmux_capture_uses_fake_tmux() {
        use std::os::unix::fs::PermissionsExt;

        let _lock = env_lock().lock().expect("lock");
        let bin_dir = tempdir().expect("temp dir");
        let tmux_path = bin_dir.path().join("tmux");
        let script = r#"#!/bin/sh
if [ "$1" = "capture-pane" ]; then
  echo "line1"
  exit 0
fi
if [ "$1" = "display-message" ]; then
  echo "tmux title"
  exit 0
fi
echo "unexpected" 1>&2
exit 1
"#;
        std::fs::write(&tmux_path, script).expect("write");
        let mut perms = std::fs::metadata(&tmux_path).expect("meta").permissions();
        perms.set_mode(0o755);
        std::fs::set_permissions(&tmux_path, perms).expect("chmod");

        let prev_path = std::env::var("PATH").ok();
        let new_path = format!(
            "{}:{}",
            bin_dir.path().display(),
            prev_path.clone().unwrap_or_default()
        );
        std::env::set_var("PATH", new_path);

        let dir = tempdir().expect("temp dir");
        let out_path = dir.path().join("out.svg");
        let mut args = Args::parse_from(["cryosnap"]);
        args.tmux = true;
        args.tmux_args = Some("-t %3".to_string());
        args.output = Some(out_path.clone());
        args.title = Some(true);
        args.title_tmux_format = Some("#{pane_title}".to_string());

        let result = run_with(args, true, false, None);
        assert!(result.is_ok());
        assert!(out_path.exists());

        if let Some(path) = prev_path {
            std::env::set_var("PATH", path);
        } else {
            std::env::remove_var("PATH");
        }
    }
}
