use crate::env;
use crate::hook_env;
use itertools::Itertools;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr;

mod bash;
mod elvish;
mod fish;
mod nushell;
mod pwsh;
mod xonsh;
mod zsh;

#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
pub enum ShellType {
    Bash,
    Elvish,
    Fish,
    Nu,
    Xonsh,
    Zsh,
    Pwsh,
}

impl ShellType {
    pub fn as_shell(&self) -> Box<dyn Shell> {
        match self {
            Self::Bash => Box::<bash::Bash>::default(),
            Self::Elvish => Box::<elvish::Elvish>::default(),
            Self::Fish => Box::<fish::Fish>::default(),
            Self::Nu => Box::<nushell::Nushell>::default(),
            Self::Xonsh => Box::<xonsh::Xonsh>::default(),
            Self::Zsh => Box::<zsh::Zsh>::default(),
            Self::Pwsh => Box::<pwsh::Pwsh>::default(),
        }
    }
}

impl Display for ShellType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Bash => write!(f, "bash"),
            Self::Elvish => write!(f, "elvish"),
            Self::Fish => write!(f, "fish"),
            Self::Nu => write!(f, "nu"),
            Self::Xonsh => write!(f, "xonsh"),
            Self::Zsh => write!(f, "zsh"),
            Self::Pwsh => write!(f, "pwsh"),
        }
    }
}

impl FromStr for ShellType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.to_lowercase();
        let s = s.rsplit_once('/').map(|(_, s)| s).unwrap_or(&s);
        match s {
            "bash" | "sh" => Ok(Self::Bash),
            "elvish" => Ok(Self::Elvish),
            "fish" => Ok(Self::Fish),
            "nu" => Ok(Self::Nu),
            "xonsh" => Ok(Self::Xonsh),
            "zsh" => Ok(Self::Zsh),
            "pwsh" => Ok(Self::Pwsh),
            _ => Err(format!("unsupported shell type: {s}")),
        }
    }
}

pub trait Shell: Display {
    fn activate(&self, opts: ActivateOptions) -> String;
    fn deactivate(&self) -> String;
    fn set_env(&self, k: &str, v: &str) -> String;
    fn prepend_env(&self, k: &str, v: &str) -> String;
    /// Prepend env, moving existing entries to the front if already present.
    /// Default falls back to prepend_env. Fish overrides with --move flag.
    fn move_prepend_env(&self, k: &str, v: &str) -> String {
        self.prepend_env(k, v)
    }
    /// Whether this shell natively deduplicates/reorders PATH entries.
    /// When true, activate_shims skips the is_dir_in_path guard and uses
    /// MovePrependEnv to ensure correct ordering on re-source.
    fn supports_move_path(&self) -> bool {
        false
    }
    fn unset_env(&self, k: &str) -> String;

    /// Set a shell alias. Returns empty string if not supported by this shell.
    fn set_alias(&self, name: &str, cmd: &str) -> String {
        // Default implementation returns empty string (unsupported)
        let _ = (name, cmd);
        String::new()
    }

    /// Unset a shell alias. Returns empty string if not supported by this shell.
    fn unset_alias(&self, name: &str) -> String {
        // Default implementation returns empty string (unsupported)
        let _ = name;
        String::new()
    }

    fn format_activate_prelude(&self, prelude: &[ActivatePrelude]) -> String {
        prelude
            .iter()
            .map(|p| match p {
                ActivatePrelude::SetEnv(k, v) => self.set_env(k, v),
                ActivatePrelude::PrependEnv(k, v) => self.prepend_env(k, v),
                ActivatePrelude::MovePrependEnv(k, v) => self.move_prepend_env(k, v),
            })
            .join("")
    }
}

#[allow(clippy::enum_variant_names)]
pub enum ActivatePrelude {
    SetEnv(String, String),
    PrependEnv(String, String),
    /// Like PrependEnv but moves existing entries to the front (for fish --move).
    /// Used only by activate_shims to reorder paths on re-source.
    MovePrependEnv(String, String),
}

pub struct ActivateOptions {
    pub exe: PathBuf,
    pub flags: String,
    pub no_hook_env: bool,
    pub prelude: Vec<ActivatePrelude>,
}

pub fn build_deactivation_script(shell: &dyn Shell) -> String {
    if !env::is_activated() {
        return String::new();
    }

    let mut out = hook_env::clear_old_env(shell);
    out.push_str(&hook_env::clear_aliases(shell));
    out.push_str(&shell.deactivate());
    out
}

pub fn get_shell(shell: Option<ShellType>) -> Option<Box<dyn Shell>> {
    shell.or(*env::MISE_SHELL).map(|st| st.as_shell())
}
