use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{bail, Context, Result};
use inquire::{Confirm, MultiSelect, Select};

use crate::cli::{PermissionFile, PermissionFlags, ScopeFlags};
use crate::config::Config;
use crate::project;
use crate::registry::{packages, tools, ResolvedPackage, Scope};
use crate::state::{MachineState, ProjectIntent, ProjectState};
use crate::ui::style;

use super::install::launch_editor;
use super::permissions::{self as perms, EffectiveScope};
use super::{exec, sync, yes};

pub fn run(
    name: Option<&str>,
    flags: ScopeFlags,
    method: Option<String>,
    permissions: PermissionFlags,
) -> Result<()> {
    match (name, flags.all) {
        (Some(n), _) => run_named(n, flags, method, &permissions),
        (None, true) => install_all(flags, &permissions),
        (None, false) => pick_flow(flags, &permissions),
    }
}

/// `use --all`: install every package declared in `cfg.install.packages`. Inside
/// a managed project, ask whether to also wire each package into the project
/// (add to intent + reconcile).
fn install_all(flags: ScopeFlags, permissions: &PermissionFlags) -> Result<()> {
    if flags.global {
        bail!("`--all` and `-g` cannot be combined");
    }
    let declared = Config::load()?.install.packages;
    if declared.is_empty() {
        println!(
            "no packages declared in {}",
            crate::config::config_file()?.display()
        );
        return Ok(());
    }

    let cwd = std::env::current_dir().context("getting current directory")?;
    let project_root = project::discover_root(&cwd);
    let wire_into_project = match &project_root {
        Some(_) if yes::enabled() => false,
        Some(_) => matches!(
            Select::new(
                "Managed folder detected. Scope for install?",
                vec!["global only", "also wire into this project"],
            )
            .prompt()?,
            "also wire into this project"
        ),
        None => false,
    };

    let project_flags = ScopeFlags::default();
    let global_flags = ScopeFlags {
        global: true,
        ..ScopeFlags::default()
    };

    for name in &declared {
        if wire_into_project {
            run_named(name, project_flags.clone(), None, permissions)?;
        } else {
            run_named(name, global_flags.clone(), None, permissions)?;
        }
    }
    Ok(())
}

fn run_named(
    name: &str,
    flags: ScopeFlags,
    method: Option<String>,
    permissions: &PermissionFlags,
) -> Result<()> {
    if tools::resolve(name)?.is_some() {
        bail!("`{name}` is a tool, not a package — try `aimgr install {name}`");
    }

    let resolved = match packages::resolve(name)? {
        Some(r) => r,
        None => return scaffold_custom_package(name),
    };
    let package = resolved.package;

    let cwd = std::env::current_dir().context("getting current directory")?;
    let target = resolve_target(&cwd, flags.global, flags.all)?;

    let mut machine = MachineState::load()?;
    let mut cfg = Config::load()?;
    if package.scope != Scope::Project {
        let already_present = machine.packages.contains_key(&package.name)
            || exec::probe_present(&package) == Some(true);
        let force = if already_present && !yes::enabled() {
            Confirm::new(&format!("`{name}` is already installed. Reinstall?"))
                .with_default(false)
                .prompt()?
        } else {
            false
        };
        exec::ensure_package_installed(&package, &mut machine, &mut cfg, method.as_deref(), force)?;
    }

    match target {
        Target::Project(root) => {
            let added = add_to_project_intent(&root, name)?;
            let in_global = cfg.install.packages.iter().any(|p| p == name);
            if package.scope != Scope::Global {
                cfg.save()?;
                machine.save()?;
                sync::reconcile_project(&root, false)?;
            } else {
                machine.register_project(&root);
                cfg.save()?;
                machine.save()?;
            }
            if added {
                println!("{} {name} added to {}", style::ok_mark(), root.display());
            } else {
                println!(
                    "{} {name} already in {} (refreshed)",
                    style::ok_mark(),
                    root.display()
                );
            }
            if in_global {
                println!("  {}", style::dim("(also present in global config)"));
            }
            apply_package_permissions(&package, permissions, Some(&root))?;
        }
        Target::Global => {
            let added = cfg.add_package(name);
            cfg.save()?;
            machine.save()?;
            if added {
                println!("{} {name} added to global config", style::ok_mark());
            } else {
                println!("{} {name} already in global config", style::ok_mark());
            }
            apply_package_permissions(&package, permissions, None)?;
        }
        Target::All => {
            let added_global = cfg.add_package(name);
            cfg.save()?;
            let projects = machine.projects.clone();
            if package.scope == Scope::Global {
                machine.save()?;
            } else {
                machine.save()?;
                for root in &projects {
                    add_to_project_intent(root, name)?;
                    sync::reconcile_project(root, false)?;
                }
            }
            if added_global {
                println!(
                    "{} {name} added to global config and {} project(s)",
                    style::ok_mark(),
                    projects.len()
                );
            } else {
                println!(
                    "{} {name} refreshed in global config and {} project(s)",
                    style::ok_mark(),
                    projects.len()
                );
            }
            // For --all, default to global scope unless a flag overrides; we
            // don't iterate every project for permissions (that's an explicit
            // per-project opt-in via `aimgr use <name>` in each).
            apply_package_permissions(&package, permissions, None)?;
        }
    }
    Ok(())
}

fn scaffold_custom_package(name: &str) -> Result<()> {
    let path = packages::custom::path_for(name)?;
    if path.exists() {
        bail!("custom package already exists at {}", path.display());
    }
    if !yes::enabled() {
        let ok = Confirm::new(&format!(
            "`{name}` is not in the registry. Scaffold a custom package at {}?",
            path.display()
        ))
        .with_default(true)
        .prompt()?;
        if !ok {
            return Ok(());
        }
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
    }
    let tmpl = format!(
        r#"name        = "{name}"
description = ""
tools       = ["claude"]
scope       = "both"

# verify = "command -v {name}"

[[install]]
id      = "manual"
label   = "fill in"
command = "echo TODO: replace with real install command"
# interactive = true   # set if the installer needs a TTY (wizard, prompts)

# [[prereq]]
# name    = "some-tool"
# check   = "command -v some-tool"
# [prereq.install]
# apt    = "some-tool"
# pacman = "some-tool"

[[instructions]]
content = """
Teach the model how to use `{name}` here.
"""
"#
    );
    fs::write(&path, tmpl).with_context(|| format!("writing {}", path.display()))?;
    println!(
        "wrote {} — fill in then re-run `aimgr use {name}`",
        path.display()
    );
    launch_editor(&path)
}

/// No-name picker: MultiSelect over registry packages, scoped by `flags` or a
/// prompt in a managed folder. Never receives `flags.all=true` — that path is
/// handled by `install_all`.
fn pick_flow(flags: ScopeFlags, permissions: &PermissionFlags) -> Result<()> {
    let cwd = std::env::current_dir().context("getting current directory")?;
    let in_project = project::discover_root(&cwd).is_some();

    let scope = if flags.global {
        PickScope::Global
    } else if in_project {
        match Select::new(
            "Managed folder detected. Install scope?",
            vec!["this project", "global"],
        )
        .prompt()?
        {
            "global" => PickScope::Global,
            _ => PickScope::Project,
        }
    } else {
        PickScope::Global
    };

    let choices = candidates(scope, &cwd)?;
    if choices.is_empty() {
        println!("Every compatible package is already in use for this scope.");
        return Ok(());
    }

    let picked = MultiSelect::new("Pick packages to install:", choices)
        .with_vim_mode(true)
        .prompt()?;

    let pick_flags = match scope {
        PickScope::Global => ScopeFlags {
            global: true,
            ..ScopeFlags::default()
        },
        PickScope::Project => ScopeFlags::default(),
    };

    for PackageChoice(r) in picked {
        run_named(&r.package.name, pick_flags.clone(), None, permissions)?;
    }
    Ok(())
}

#[derive(Copy, Clone)]
enum PickScope {
    Global,
    Project,
}

fn candidates(scope: PickScope, cwd: &Path) -> Result<Vec<PackageChoice>> {
    let cfg = Config::load()?;
    let all = packages::list()?;
    let filtered: Vec<ResolvedPackage> = match scope {
        PickScope::Global => all
            .into_iter()
            .filter(|r| r.package.scope != Scope::Project)
            .filter(|r| !cfg.install.packages.iter().any(|n| n == &r.package.name))
            .collect(),
        PickScope::Project => {
            let Some(root) = project::discover_root(cwd) else {
                return Ok(Vec::new());
            };
            let intent = ProjectIntent::load(&root)?;
            all.into_iter()
                .filter(|r| !intent.packages.iter().any(|n| n == &r.package.name))
                .collect()
        }
    };
    Ok(filtered.into_iter().map(PackageChoice).collect())
}

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)
    }
}

pub enum Target {
    Project(PathBuf),
    Global,
    All,
}

/// Decide where a scoped verb should act. In the default case (no flags), prefers
/// the nearest project; if cwd is outside a project, asks the user.
pub fn resolve_target(cwd: &Path, global: bool, all: bool) -> Result<Target> {
    if all {
        return Ok(Target::All);
    }
    if global {
        return Ok(Target::Global);
    }
    if let Some(root) = project::discover_root(cwd) {
        return Ok(Target::Project(root));
    }
    if yes::enabled() {
        return Ok(Target::Global);
    }
    let choice = Select::new(
        "Not inside a project. What do you want to do?",
        vec!["apply globally", "init a project here", "cancel"],
    )
    .prompt()?;
    match choice {
        "apply globally" => Ok(Target::Global),
        "init a project here" => {
            let root = cwd.to_path_buf();
            ProjectIntent::default().save(&root)?;
            Ok(Target::Project(root))
        }
        _ => bail!("cancelled"),
    }
}

/// Prompt the user (or follow flags / -y) and apply the package's declared
/// permission grants. Records what was written so unuse/uninstall can undo it.
/// `project_root = Some(_)` means the install was project-scoped.
fn apply_package_permissions(
    package: &crate::registry::Package,
    flags: &PermissionFlags,
    project_root: Option<&Path>,
) -> Result<()> {
    let has_grants = package.permissions.iter().any(|g| !g.is_empty());
    if !has_grants {
        return Ok(());
    }
    let scope = perms::resolve_scope(flags, project_root.is_some(), has_grants)?;
    if scope == EffectiveScope::Skip {
        println!("  {}", style::dim("(skipped permission grants)"));
        return Ok(());
    }

    let file_pref = flags.file.unwrap_or(PermissionFile::Shared);
    let plans = perms::plan(package, scope, project_root, file_pref)?;
    if plans.is_empty() {
        return Ok(());
    }

    // Confirm unless -y or explicit scope flag already committed the choice.
    let auto = yes::enabled() || flags.scope.is_some();
    if !auto {
        let summary = perms::summarize(&plans);
        let ok = inquire::Confirm::new(&format!("Apply permissions: {summary}?"))
            .with_default(true)
            .prompt()?;
        if !ok {
            println!("  {}", style::dim("(skipped permission grants)"));
            return Ok(());
        }
    }

    let records = perms::apply(&plans)?;
    persist_permission_records(&package.name, project_root, &records)
}

fn persist_permission_records(
    name: &str,
    project_root: Option<&Path>,
    records: &[crate::state::PermissionRecord],
) -> Result<()> {
    if records.is_empty() {
        return Ok(());
    }
    match project_root {
        Some(root) => {
            let mut state = ProjectState::load(root)?;
            let entry = state.packages.entry(name.to_string()).or_default();
            merge_permission_records(&mut entry.permissions, records);
            if entry.installed_at.is_empty() {
                entry.installed_at = chrono::Utc::now().to_rfc3339();
            }
            state.save(root)?;
        }
        None => {
            let mut machine = MachineState::load()?;
            let entry = machine.packages.entry(name.to_string()).or_insert_with(|| {
                crate::state::MachineInstall {
                    installed_at: chrono::Utc::now().to_rfc3339(),
                    method: "preexisting".into(),
                    version: None,
                    permissions: Vec::new(),
                }
            });
            merge_permission_records(&mut entry.permissions, records);
            machine.save()?;
        }
    }
    Ok(())
}

fn merge_permission_records(
    dest: &mut Vec<crate::state::PermissionRecord>,
    new: &[crate::state::PermissionRecord],
) {
    for rec in new {
        // Replace any prior record targeting the same (tool, path) so the
        // stored `applied` JSON reflects the latest registry version.
        if let Some(existing) = dest
            .iter_mut()
            .find(|r| r.tool == rec.tool && r.path == rec.path)
        {
            existing.applied = rec.applied.clone();
        } else {
            dest.push(rec.clone());
        }
    }
}

fn add_to_project_intent(root: &Path, name: &str) -> Result<bool> {
    let mut intent = ProjectIntent::load(root)?;
    if intent.packages.iter().any(|p| p == name) {
        return Ok(false);
    }
    intent.packages.push(name.to_string());
    intent.save(root)?;
    Ok(true)
}
