use std::io::{self, IsTerminal, Write};

use dialoguer::{Input, Select};
use eyre::{Result, eyre};
use name_forge::{DictionarySource, GeneratorConfig};

use crate::CliArgs;

const DEFAULT_MIN_LEN: usize = 3;
const DEFAULT_MAX_LEN: usize = 5;
const DEFAULT_RESULTS_LIMIT: usize = 10;

/// Builds a GeneratorConfig by merging CLI flags with interactive prompts.
pub fn build_config(args: CliArgs) -> Result<GeneratorConfig> {
    let interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal();

    let min_len = match args.min_len {
        Some(value) => value,
        None => prompt_usize("Minimum length", DEFAULT_MIN_LEN, interactive, None)?,
    };

    let max_len = match args.max_len {
        Some(value) => value,
        None => {
            let default = DEFAULT_MAX_LEN.max(min_len);
            prompt_usize("Maximum length", default, interactive, Some(min_len))?
        }
    };

    let results_limit = match args.limit {
        Some(value) => Some(value),
        None if args.no_limit => None,
        None => prompt_results_limit(interactive)?,
    };

    let parallel = args.parallel;
    let alphabetical = args.alphabetical;

    let dictionary = select_dictionary(&args)?;

    let mut config = GeneratorConfig::new(min_len, max_len);
    config.results_limit = results_limit;
    config.parallel = parallel;
    config.dictionary = dictionary;
    config.shuffle = !alphabetical;

    Ok(config)
}

fn select_dictionary(args: &CliArgs) -> Result<DictionarySource> {
    if let Some(path) = &args.dictionary {
        return Ok(DictionarySource::CustomPath(path.clone()));
    }

    if args.names_only {
        return Ok(DictionarySource::Names);
    }

    if args.words_only {
        return Ok(DictionarySource::Words);
    }

    Ok(DictionarySource::Combined)
}

fn prompt_usize(
    prompt: &str,
    default: usize,
    interactive: bool,
    min_value: Option<usize>,
) -> Result<usize> {
    if interactive {
        match select_numeric(prompt, default, min_value) {
            Ok(value) => return Ok(value),
            Err(err) if !interactive_error_is_fatal(&err) => {}
            Err(err) => return Err(err.into()),
        };
    }

    prompt_usize_stdio(prompt, default)
}

fn prompt_usize_stdio(prompt: &str, default: usize) -> Result<usize> {
    loop {
        let raw = prompt_line(prompt, Some(&default.to_string()))?;
        if raw.is_empty() {
            return Ok(default);
        }

        match raw.parse::<usize>() {
            Ok(value) => return Ok(value),
            Err(_) => eprintln!("Please enter a positive integer."),
        }
    }
}

fn prompt_line(prompt: &str, default: Option<&str>) -> Result<String> {
    if io::stdin().is_terminal() {
        print_prompt(prompt, default)?;
    }

    let mut buffer = String::new();
    let read = io::stdin().read_line(&mut buffer)?;
    if read == 0 {
        return Err(eyre!("no input received"));
    }

    Ok(buffer.trim().to_string())
}

fn print_prompt(prompt: &str, default: Option<&str>) -> Result<()> {
    if let Some(default) = default {
        print!("{prompt} [{default}]: ");
    } else {
        print!("{prompt}: ");
    }
    io::stdout().flush()?;
    Ok(())
}

fn interactive_error_is_fatal(err: &dialoguer::Error) -> bool {
    matches!(
        err,
        dialoguer::Error::IO(io_err)
            if matches!(io_err.kind(), io::ErrorKind::Interrupted | io::ErrorKind::UnexpectedEof)
    )
}

fn prompt_results_limit(interactive: bool) -> Result<Option<usize>> {
    if interactive {
        match select_limit() {
            Ok(choice) => return Ok(choice),
            Err(err) if !interactive_error_is_fatal(&err) => {}
            Err(err) => return Err(err.into()),
        };
    }

    Ok(Some(DEFAULT_RESULTS_LIMIT))
}

fn select_limit() -> std::result::Result<Option<usize>, dialoguer::Error> {
    let options = vec![
        LimitChoice::Default(DEFAULT_RESULTS_LIMIT),
        LimitChoice::Preset(20),
        LimitChoice::Preset(50),
        LimitChoice::Preset(100),
        LimitChoice::Unlimited,
        LimitChoice::Custom,
    ];

    let selection = Select::new()
        .with_prompt("Maximum available results")
        .items(&options)
        .default(0)
        .interact_opt()?;

    match selection.and_then(|idx| options.get(idx).copied()) {
        Some(LimitChoice::Default(limit)) | Some(LimitChoice::Preset(limit)) => Ok(Some(limit)),
        Some(LimitChoice::Unlimited) => Ok(None),
        Some(LimitChoice::Custom) => {
            let value = Input::<usize>::new().with_prompt("Custom limit").interact_text()?;
            Ok(Some(value))
        }
        None => Err(dialoguer::Error::IO(std::io::Error::from(std::io::ErrorKind::Interrupted))),
    }
}

#[derive(Debug, Clone, Copy)]
enum LimitChoice {
    Default(usize),
    Preset(usize),
    Unlimited,
    Custom,
}

impl std::fmt::Display for LimitChoice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Default(limit) => write!(f, "Default ({limit})"),
            Self::Preset(limit) => write!(f, "{limit}"),
            Self::Unlimited => write!(f, "No limit"),
            Self::Custom => write!(f, "Custom…"),
        }
    }
}

fn select_numeric(
    prompt: &str,
    default: usize,
    min_value: Option<usize>,
) -> std::result::Result<usize, dialoguer::Error> {
    let min = min_value.unwrap_or(1);
    let mut presets = vec![
        default,
        min,
        min + 1,
        min + 2,
        default + 1,
        default + 2,
        default + 3,
        default + 5,
        default + 10,
    ];
    presets.retain(|value| *value >= min);
    presets.sort_unstable();
    presets.dedup();
    presets.retain(|value| *value != default);

    let mut options: Vec<NumericChoice> = presets.into_iter().map(NumericChoice::Preset).collect();
    options.insert(0, NumericChoice::Preset(default));
    options.push(NumericChoice::Custom);

    let selection = Select::new().with_prompt(prompt).items(&options).default(0).interact_opt()?;

    match selection.and_then(|idx| options.get(idx).copied()) {
        Some(NumericChoice::Preset(value)) => Ok(value),
        Some(NumericChoice::Custom) => loop {
            let value = Input::<usize>::new()
                .with_prompt(format!("{prompt} (min {min})"))
                .with_initial_text(default.to_string())
                .interact_text()?;
            if value < min {
                eprintln!("{prompt} must be at least {min}");
                continue;
            }
            return Ok(value);
        },
        None => Err(dialoguer::Error::IO(std::io::Error::from(std::io::ErrorKind::Interrupted))),
    }
}

#[derive(Debug, Clone, Copy)]
enum NumericChoice {
    Preset(usize),
    Custom,
}

impl std::fmt::Display for NumericChoice {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Preset(value) => write!(f, "{value}"),
            Self::Custom => write!(f, "Custom…"),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn selects_dictionary_from_flags() {
        let args = CliArgs {
            min_len: Some(1),
            max_len: Some(1),
            limit: Some(1),
            parallel: true,
            alphabetical: false,
            dictionary: Some(PathBuf::from("custom.txt")),
            names_only: false,
            words_only: false,
            verbose: false,
            no_limit: false,
        };

        let config = build_config(args).unwrap();
        assert!(matches!(config.dictionary, DictionarySource::CustomPath(_)));
    }
}
