use std::fmt;
use std::path::PathBuf;

use anyhow::{Context, Result};
use inquire::{MultiSelect, Select};
use terminal_size::{terminal_size, Width};

pub mod style {
    use owo_colors::OwoColorize;
    use std::fmt::Display;
    use std::io::IsTerminal;
    use std::sync::OnceLock;

    fn enabled() -> bool {
        static CACHED: OnceLock<bool> = OnceLock::new();
        *CACHED.get_or_init(|| {
            if std::env::var_os("NO_COLOR").is_some() {
                return false;
            }
            if matches!(std::env::var("FORCE_COLOR"), Ok(v) if !v.is_empty() && v != "0") {
                return true;
            }
            if matches!(std::env::var("TERM").as_deref(), Ok("dumb")) {
                return false;
            }
            std::io::stdout().is_terminal()
        })
    }

    pub fn ok_mark() -> String {
        if enabled() {
            "✔".green().to_string()
        } else {
            "✔".into()
        }
    }
    pub fn fail_mark() -> String {
        if enabled() {
            "✘".red().to_string()
        } else {
            "✘".into()
        }
    }
    pub fn warn_mark() -> String {
        if enabled() {
            "⚠".yellow().to_string()
        } else {
            "⚠".into()
        }
    }
    pub fn cancel_mark() -> String {
        if enabled() {
            "⊘".yellow().to_string()
        } else {
            "⊘".into()
        }
    }
    pub fn step_mark() -> String {
        if enabled() {
            "→".cyan().to_string()
        } else {
            "→".into()
        }
    }
    pub fn dim<T: Display>(value: T) -> String {
        if enabled() {
            value.dimmed().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn bold<T: Display>(value: T) -> String {
        if enabled() {
            value.bold().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn added<T: Display>(value: T) -> String {
        if enabled() {
            value.green().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn removed<T: Display>(value: T) -> String {
        if enabled() {
            value.red().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn cyan<T: Display>(value: T) -> String {
        if enabled() {
            value.cyan().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn yellow<T: Display>(value: T) -> String {
        if enabled() {
            value.yellow().to_string()
        } else {
            value.to_string()
        }
    }
    pub fn magenta<T: Display>(value: T) -> String {
        if enabled() {
            value.magenta().to_string()
        } else {
            value.to_string()
        }
    }
}

use crate::cli::ScopeFlags;
use crate::config::Config;
use crate::ops::{registry as ops_registry, sync, unuse, up, use_package};
use crate::project;
use crate::registry::{packages, ResolvedPackage};
use crate::state::{MachineState, ProjectIntent};

const GUTTER: &str = "  ";
const ELLIPSIS: &str = "...";

/// Per-cell styler for table data rows: `(col_index, raw_cell) -> rendered cell`.
pub type CellStyler<'a> = dyn Fn(usize, &str) -> String + 'a;

/// Interactive picker — entry point when `aimgr` runs with no subcommand.
pub fn pick() -> Result<()> {
    let ctx = PickerCtx::detect()?;
    ctx.print_header();

    let choice = Select::new("What would you like to do?", ctx.actions()).prompt()?;
    match choice {
        Action::Add => add_flow(&ctx),
        Action::Remove => remove_flow(&ctx),
        Action::Sync => sync_flow(&ctx),
        Action::Up => up::run(None),
        Action::Init => crate::ops::init::run(),
        Action::Browse => ops_registry::run(),
        Action::Quit => Ok(()),
    }
}

struct PickerCtx {
    project_root: Option<PathBuf>,
    project_packages: Vec<String>,
    global_packages: Vec<String>,
    machine_tools: usize,
    machine_packages: usize,
}

impl PickerCtx {
    fn detect() -> Result<Self> {
        let cwd = std::env::current_dir().context("getting current directory")?;
        let project_root = project::discover_root(&cwd);
        let project_packages = match &project_root {
            Some(root) => ProjectIntent::load(root)?.packages,
            None => Vec::new(),
        };
        let global_packages = Config::load()?.install.packages;
        let machine = MachineState::load()?;
        Ok(Self {
            project_root,
            project_packages,
            global_packages,
            machine_tools: machine.tools.len(),
            machine_packages: machine.packages.len(),
        })
    }

    fn print_header(&self) {
        match &self.project_root {
            Some(root) => {
                println!("aimgr — project at {}", root.display());
                println!("  project: {}", pretty_list(&self.project_packages));
            }
            None => {
                println!("aimgr — no project detected here");
            }
        }
        println!("  global:  {}", pretty_list(&self.global_packages));
        println!(
            "  machine: {} tool{}, {} package{} installed",
            self.machine_tools,
            if self.machine_tools == 1 { "" } else { "s" },
            self.machine_packages,
            if self.machine_packages == 1 { "" } else { "s" }
        );
        println!();
    }

    fn actions(&self) -> Vec<Action> {
        let mut a = vec![Action::Add, Action::Remove];
        if self.project_root.is_some() {
            a.push(Action::Sync);
        } else {
            a.push(Action::Init);
        }
        a.push(Action::Up);
        a.push(Action::Browse);
        a.push(Action::Quit);
        a
    }

    fn current_packages(&self) -> &[String] {
        if self.project_root.is_some() {
            &self.project_packages
        } else {
            &self.global_packages
        }
    }
}

#[derive(Copy, Clone)]
enum Action {
    Add,
    Remove,
    Sync,
    Up,
    Init,
    Browse,
    Quit,
}

impl fmt::Display for Action {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Action::Add => "Add a package",
            Action::Remove => "Remove a package",
            Action::Sync => "Sync this project",
            Action::Up => "Update installed tools",
            Action::Init => "Init a project here",
            Action::Browse => "Browse the registry",
            Action::Quit => "Quit",
        })
    }
}

fn add_flow(ctx: &PickerCtx) -> Result<()> {
    let current = ctx.current_packages();
    let choices: Vec<PackageChoice> = packages::list()?
        .into_iter()
        .filter(|r| !current.iter().any(|n| n == &r.package.name))
        .map(PackageChoice)
        .collect();
    if choices.is_empty() {
        println!("Everything in the registry is already in use here.");
        return Ok(());
    }
    let picked = MultiSelect::new("Pick packages to add:", choices)
        .with_vim_mode(true)
        .prompt()?;
    for PackageChoice(r) in picked {
        use_package::run(
            Some(&r.package.name),
            ScopeFlags::default(),
            None,
            crate::cli::PermissionFlags::default(),
        )?;
    }
    Ok(())
}

fn remove_flow(ctx: &PickerCtx) -> Result<()> {
    let names = ctx.current_packages().to_vec();
    if names.is_empty() {
        println!("Nothing to remove in this scope.");
        return Ok(());
    }
    let picked = MultiSelect::new("Pick packages to remove:", names).prompt()?;
    for name in picked {
        unuse::run(&name, ScopeFlags::default())?;
    }
    Ok(())
}

fn sync_flow(ctx: &PickerCtx) -> Result<()> {
    if ctx.project_root.is_none() {
        println!("`sync` runs inside a project — try `Init a project here` first.");
        return Ok(());
    }
    sync::run_verb(false, false, false)
}

struct PackageChoice(ResolvedPackage);

impl fmt::Display for PackageChoice {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let tools: Vec<&str> = self.0.package.tools.iter().map(|t| t.as_str()).collect();
        let tools = if tools.is_empty() {
            "any".into()
        } else {
            tools.join(",")
        };
        let desc = self.0.package.description.as_deref().unwrap_or("");
        write!(f, "{}  [{}]  {}", self.0.package.name, tools, desc)
    }
}

fn pretty_list(packages: &[String]) -> String {
    if packages.is_empty() {
        "(none)".into()
    } else {
        packages.join(", ")
    }
}

/// Render a whitespace-separated table (mise-style). No borders, two-space gutter.
/// When `flex` is set and output exceeds terminal width, that column is
/// truncated with `...`. An optional `styler` colors cells after width
/// computation so ANSI escapes don't throw off alignment.
pub fn print_table_styled(
    headers: &[&str],
    rows: &[Vec<String>],
    flex: Option<usize>,
    styler: Option<&CellStyler<'_>>,
) {
    let cols = headers.len();
    let mut widths: Vec<usize> = headers.iter().map(|h| h.chars().count()).collect();
    for r in rows {
        for (i, cell) in r.iter().take(cols).enumerate() {
            widths[i] = widths[i].max(cell.chars().count());
        }
    }

    if let (Some(flex_idx), Some(term)) = (flex, term_width()) {
        if flex_idx < cols {
            let gutters = GUTTER.len() * cols.saturating_sub(1);
            let fixed: usize = widths
                .iter()
                .enumerate()
                .filter(|(i, _)| *i != flex_idx)
                .map(|(_, w)| *w)
                .sum();
            let budget = term.saturating_sub(fixed + gutters);
            let min = ELLIPSIS.len();
            let flex_w = budget.max(min);
            if flex_w < widths[flex_idx] {
                widths[flex_idx] = flex_w;
            }
        }
    }

    print_row(headers.iter().copied(), &widths, flex, true, None);
    for r in rows {
        print_row(r.iter().map(|c| c.as_str()), &widths, flex, false, styler);
    }
}

fn print_row<'a, I: Iterator<Item = &'a str>>(
    cells: I,
    widths: &[usize],
    flex: Option<usize>,
    header: bool,
    styler: Option<&CellStyler<'_>>,
) {
    let cells: Vec<&str> = cells.collect();
    let mut line = String::new();
    for (i, cell) in cells.iter().enumerate() {
        if i > 0 {
            line.push_str(GUTTER);
        }
        let rendered = if flex == Some(i) {
            truncate(cell, widths[i])
        } else {
            (*cell).to_string()
        };
        let pad = widths[i].saturating_sub(rendered.chars().count());
        let styled = if header {
            style::bold(rendered)
        } else if let Some(f) = styler {
            f(i, &rendered)
        } else {
            rendered
        };
        line.push_str(&styled);
        if i + 1 < cells.len() {
            for _ in 0..pad {
                line.push(' ');
            }
        }
    }
    println!("{}", line.trim_end());
}

fn term_width() -> Option<usize> {
    if let Ok(cols) = std::env::var("COLUMNS") {
        if let Ok(n) = cols.trim().parse::<usize>() {
            if n > 0 {
                return Some(n);
            }
        }
    }
    terminal_size().map(|(Width(w), _)| w as usize)
}

fn truncate(s: &str, max: usize) -> String {
    let len = s.chars().count();
    if len <= max {
        return s.to_string();
    }
    if max <= ELLIPSIS.len() {
        return s.chars().take(max).collect();
    }
    let take = max - ELLIPSIS.len();
    let mut out: String = s.chars().take(take).collect();
    out.push_str(ELLIPSIS);
    out
}

#[cfg(test)]
mod tests {
    use super::truncate;

    #[test]
    fn truncate_shorter_than_max_is_unchanged() {
        assert_eq!(truncate("hi", 10), "hi");
    }

    #[test]
    fn truncate_adds_ellipsis() {
        assert_eq!(truncate("abcdefghij", 6), "abc...");
    }

    #[test]
    fn truncate_tight_budget_returns_prefix() {
        assert_eq!(truncate("abcdef", 2), "ab");
    }
}
