use std::{collections::HashMap, path::Path};

use serde::{Deserialize, Serialize};
use toml::Table;

use crate::{
    cli::{
        DeployArgs, DiffArgs, ImportArgs, PackagesListArgs, ProfileRemoveArgs, ProfilesAddArgs,
        RemovePackageArgs, UpdateArgs,
    },
    context::Context,
    package::{BackupDeployResult, Package},
    profile::Profile,
    utils::{LogLevel, cprintln, is_empty_table},
};

#[cfg(test)]
mod tests;

/// JSON Schema for `config.toml`, referenced via a `#:schema` directive at
/// the top of newly-generated config files (see `Config::init`).
const SCHEMA_URL: &str =
    "https://raw.githubusercontent.com/uroybd/DotR/main/schema/config.schema.json";

#[derive(Deserialize, Serialize, Debug, Clone, Default)]
pub struct Config {
    pub banner: bool,
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub packages: HashMap<String, Package>,
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub profiles: HashMap<String, Profile>,
    #[serde(skip_serializing_if = "is_empty_table")]
    pub variables: Table,
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub prompts: HashMap<String, String>,
    pub symlink: bool,
    /// Name of the Bitwarden secure note used when `prompt_backend =
    /// "bitwarden"`. Defaults to `prompt_store::DEFAULT_BITWARDEN_NOTE` when unset.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub bitwarden_note: Option<String>,
    /// Repo-wide default backend for every prompt. A profile's own
    /// `prompt_backend` overrides this when that profile is active.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub prompt_backend: Option<crate::prompt_store::PromptBackend>,
}

pub(crate) enum OpType {
    Backup,
    Deploy,
}

impl Config {
    pub fn from_path(cwd: &Path) -> anyhow::Result<Self> {
        let config_path = cwd.join("config.toml");
        if !config_path.exists() {
            anyhow::bail!("config.toml not found in the current directory");
        }
        let config_content = std::fs::read_to_string(config_path)?;
        let conf_table = config_content.parse::<Table>()?;
        Self::from_table(&conf_table)
    }

    pub fn save(&self, cwd: &Path) -> anyhow::Result<()> {
        let config_content = toml::to_string_pretty(self)?;
        // Every write needs the `#:schema` directive, not just the one from
        // `init()` - otherwise the very next save (import, deploy, profiles
        // add/remove, ...) drops it, since serializing `Config` back to TOML
        // has no way to preserve a leading comment on its own.
        let config_content = format!("#:schema {}\n{}", SCHEMA_URL, config_content);
        std::fs::write(cwd.join("config.toml"), config_content)?;
        Ok(())
    }

    pub fn from_table(table: &Table) -> anyhow::Result<Self> {
        let mut packages: HashMap<String, Package> = HashMap::new();
        let package_confs = table.get("packages").and_then(|v| v.as_table());
        if let Some(pkg_confs) = package_confs {
            for (key, val) in pkg_confs.iter() {
                let pkg_val = val
                    .as_table()
                    .ok_or_else(|| anyhow::anyhow!("Package '{}' must be a table", key))?;
                let pkg = Package::from_table(key, pkg_val)?;
                packages.insert(pkg.name.clone(), pkg);
            }
        }

        let mut profiles: HashMap<String, Profile> = HashMap::new();
        let profile_confs = table.get("profiles").and_then(|v| v.as_table());
        if let Some(prof_confs) = profile_confs {
            for (key, val) in prof_confs.iter() {
                let prof_val = val
                    .as_table()
                    .ok_or_else(|| anyhow::anyhow!("Profile '{}' must be a table", key))?;
                let profile = Profile::from_table(key, prof_val)?;
                profiles.insert(profile.name.clone(), profile);
            }
        }
        let mut variables: Table = Table::new();
        if let Some(vars) = table.get("variables").and_then(|v| v.as_table()) {
            for (k, v) in vars.iter() {
                variables.insert(k.clone(), v.clone());
            }
        }
        let prompts = crate::utils::get_string_hashmap_from_value(table.get("prompts"))?;
        let bitwarden_note = table
            .get("bitwarden_note")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let prompt_backend = table
            .get("prompt_backend")
            .and_then(|v| v.as_str())
            .map(crate::prompt_store::PromptBackend::parse)
            .transpose()?;
        Ok(Self {
            banner: table
                .get("banner")
                .and_then(|v| v.as_bool())
                .unwrap_or(false),
            symlink: table
                .get("symlink")
                .and_then(|v| v.as_bool())
                .unwrap_or(false),
            packages,
            profiles,
            variables,
            prompts,
            bitwarden_note,
            prompt_backend,
        })
    }

    pub fn import_package(&mut self, args: &ImportArgs, ctx: &Context) -> anyhow::Result<()> {
        let mut profile = ctx.profile.clone();
        let profile_name = profile.name.clone();
        cprintln(&format!("Importing from {}", args.path), &LogLevel::Info);
        let mut package = Package::from_path(args, &ctx.working_dir)?;
        let pkg_name = package.name.clone();

        let backup_args = crate::cli::UpdateArgs {
            packages: None,
            profile: Some(profile_name.clone()),
            ignore_errors: false,
            clean: Some(false),
            dry_run: false,
        };
        package.backup(ctx, &backup_args)?;
        profile.dependencies.push(pkg_name.clone());
        if profile_name != "default" {
            package
                .targets
                .insert(profile_name.clone(), package.dest.clone());
        }
        let should_deploy = args.symlink;
        self.packages.insert(pkg_name.clone(), package);
        self.profiles.insert(profile_name.clone(), profile);
        self.save(&ctx.working_dir)?;
        if should_deploy {
            let pkg = self.packages.get(&pkg_name).ok_or_else(|| {
                anyhow::anyhow!("Package '{}' not found after insertion", pkg_name)
            })?;
            pkg.deploy(
                ctx,
                &crate::cli::DeployArgs {
                    packages: Some(vec![pkg_name.clone()]),
                    profile: Some(profile_name),
                    ignore_errors: false,
                    clean: Some(false),
                    dry_run: false,
                    skip_actions: false,
                    skip_pre_actions: false,
                    skip_post_actions: false,
                    ignore_dependencies: false,
                },
            )?;
        }
        cprintln(&format!("Package '{}' imported", pkg_name), &LogLevel::Info);
        Ok(())
    }

    pub fn filter_packages(
        &self,
        ctx: &Context,
        names: &Option<Vec<String>>,
        ignore_dependencies: bool,
    ) -> anyhow::Result<HashMap<String, Package>> {
        let mut packages: HashMap<String, Package> = HashMap::new();
        if let Some(pkg_names) = names {
            for name in pkg_names {
                if let Some(pkg) = self.packages.get(name) {
                    packages.insert(name.clone(), pkg.clone());
                } else {
                    return Err(anyhow::anyhow!("Package '{}' not found", name));
                }
            }
        } else {
            for dep in &ctx.profile.dependencies {
                if let Some(pkg) = self.packages.get(dep) {
                    if !pkg.skip {
                        packages.insert(dep.clone(), pkg.clone());
                    }
                } else {
                    anyhow::bail!(
                        "Package '{}' not found for profile '{}'",
                        dep,
                        ctx.profile.name
                    );
                }
            }
        }
        if ignore_dependencies {
            return Ok(packages);
        }
        let mut dependencies: HashMap<String, Package> = HashMap::new();
        for pkg in packages.values() {
            if let Some(deps) = &pkg.dependencies {
                for dep in deps {
                    if let Some(dep_pkg) = self.packages.get(dep) {
                        dependencies.insert(dep.clone(), dep_pkg.clone());
                    } else {
                        anyhow::bail!("Dependency package '{}' not found in configuration", dep);
                    }
                }
            }
        }
        packages.extend(dependencies);
        Ok(packages)
    }

    pub fn backup_packages(&self, ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow::Error> {
        cprintln("Backing up packages...", &LogLevel::Info);
        let mut stats: HashMap<BackupDeployResult, u32> = HashMap::new();
        for pkg in self.filter_packages(ctx, &args.packages, false)?.values() {
            match pkg.backup(ctx, args) {
                Err(e) => {
                    if args.ignore_errors {
                        cprintln(
                            &format!("Error backing up package '{}': {}", pkg.name, e),
                            &LogLevel::Error,
                        );
                        *stats.entry(BackupDeployResult::Failed).or_insert(0) += 1;
                    } else {
                        return Err(e);
                    }
                }
                Ok(res) => {
                    *stats.entry(res).or_insert(0) += 1;
                }
            }
        }
        print_stats(&stats, OpType::Backup);
        Ok(())
    }

    pub fn deploy_packages(&self, ctx: &Context, args: &DeployArgs) -> Result<(), anyhow::Error> {
        cprintln("Deploying packages...", &LogLevel::Info);
        let mut stats: HashMap<BackupDeployResult, u32> = HashMap::new();
        for pkg in self
            .filter_packages(ctx, &args.packages, args.ignore_dependencies)?
            .values()
        {
            match pkg.deploy(ctx, args) {
                Err(e) => {
                    if args.ignore_errors {
                        cprintln(
                            &format!("Error deploying package '{}': {}", pkg.name, e),
                            &LogLevel::Error,
                        );
                        *stats.entry(BackupDeployResult::Failed).or_insert(0) += 1;
                    } else {
                        return Err(e);
                    }
                }
                Ok(res) => {
                    *stats.entry(res).or_insert(0) += 1;
                }
            }
        }
        print_stats(&stats, OpType::Deploy);
        Ok(())
    }

    pub fn diff_packages(&self, ctx: &Context, args: &DiffArgs) -> Result<(), anyhow::Error> {
        cprintln("Checking differences...", &LogLevel::Info);
        for pkg in self.filter_packages(ctx, &args.packages, false)?.values() {
            cprintln(&format!("Package: {}", pkg.name), &LogLevel::Info);
            if let Err(e) = pkg.diff(ctx) {
                if args.ignore_errors {
                    cprintln(
                        &format!("Error diffing package '{}': {}", pkg.name, e),
                        &LogLevel::Error,
                    );
                } else {
                    return Err(e);
                }
            }
        }
        Ok(())
    }

    pub fn update_profiles(&mut self, profile: &Profile, ctx: &Context) -> anyhow::Result<()> {
        self.profiles
            .entry(profile.name.clone())
            .or_insert_with(|| {
                cprintln(
                    &format!(
                        "Profile '{}' not found in configuration, creating empty profile",
                        profile.name
                    ),
                    &LogLevel::Warning,
                );
                profile.clone()
            });
        self.save(&ctx.working_dir)?;
        Ok(())
    }

    pub fn init(cwd: &Path) -> Result<Self, anyhow::Error> {
        let config_path = cwd.join("config.toml");
        if config_path.exists() {
            cprintln("config.toml exists, skipping", &LogLevel::Warning);
            return Self::from_path(cwd);
        }
        let default_config = Config::new();
        // `save()` prepends the `#:schema` directive understood by taplo (the
        // LSP behind VS Code's "Even Better TOML", Neovim's nvim-lspconfig
        // taplo preset, and other taplo-based editor integrations) to
        // associate this file with a JSON Schema for validation and
        // autocomplete, without relying on ambiguous filename-based catalog
        // matching.
        default_config.save(cwd)?;
        std::fs::create_dir_all(cwd.join("dotfiles"))?;

        let gitignore_path = cwd.join(".gitignore");
        let gitignore_content = ".uservariables.toml\ndeployed";
        std::fs::write(gitignore_path, gitignore_content)?;

        cprintln("Repository initialized", &LogLevel::Info);
        Ok(default_config)
    }

    pub fn new() -> Self {
        let mut profiles: HashMap<String, Profile> = HashMap::new();
        profiles.insert("default".to_string(), Profile::new("default"));
        Self {
            banner: true,
            profiles,
            ..Default::default()
        }
    }

    pub fn list_packages(&self, ctx: &Context, args: &PackagesListArgs) -> anyhow::Result<()> {
        let packages = self.filter_packages(ctx, &None, false)?;
        if args.plain {
            for pkg in packages.values() {
                println!("{}", pkg.name);
            }
        } else if packages.is_empty() {
            cprintln("No packages found.", &LogLevel::Info);
        } else {
            for pkg in packages.values() {
                println!("{}", pkg.name);
                if args.verbose {
                    print!(
                        "    Source: {}\n    Destination: {}\n    skipped: {}\n",
                        pkg.src, pkg.dest, pkg.skip
                    );
                    if let Some(deps) = &pkg.dependencies {
                        println!("    Dependencies: {:?}", deps);
                    }
                    if !pkg.targets.is_empty() {
                        println!("    Targets:");
                        for (target_name, target_dest) in pkg.targets.iter() {
                            println!("      - {}: {}", target_name, target_dest);
                        }
                    }
                }
            }
        }
        Ok(())
    }

    pub fn list_profiles(&self, args: &crate::cli::ProfilesListArgs) -> anyhow::Result<()> {
        if args.plain {
            for profile in self.profiles.values() {
                println!("{}", profile.name);
            }
        } else if self.profiles.is_empty() {
            cprintln("No profiles found.", &LogLevel::Info);
        } else {
            for profile in self.profiles.values() {
                println!("{}", profile.name);
                if args.verbose {
                    println!("    Dependencies: {:?}", profile.dependencies);
                    println!("    Variables: {:?}", profile.variables);
                    if let Some(backend) = profile.prompt_backend {
                        println!("    Prompt backend: {}", backend.as_str());
                    }
                    if !profile.prompts.is_empty() {
                        println!("    Prompts:");
                        for (var, prompt) in profile.prompts.iter() {
                            println!("      - {}: {}", var, prompt);
                        }
                    }
                }
            }
        }
        Ok(())
    }

    pub fn add_profile(&mut self, args: &ProfilesAddArgs, ctx: &mut Context) -> anyhow::Result<()> {
        if self.profiles.contains_key(&args.name) {
            anyhow::bail!("Profile '{}' already exists", args.name);
        }
        let profile = Profile::new(&args.name);
        self.profiles.insert(args.name.clone(), profile.clone());
        self.save(&ctx.working_dir)?;
        cprintln(&format!("Profile '{}' added", args.name), &LogLevel::Info);
        if args.set_as_current {
            ctx.save_to_uservariables("DOTR_PROFILE", toml::Value::String(profile.name.clone()))?;
            cprintln(
                &format!("Setting profile '{}' as current", args.name),
                &LogLevel::Info,
            );
        }
        Ok(())
    }

    pub fn get_orphan_packages(&self) -> Vec<String> {
        self.packages
            .keys()
            .filter_map(
                |name| match self.is_package_safe_to_remove(name, &[], &[]) {
                    (true, _, _) => Some(name.clone()),
                    _ => None,
                },
            )
            .collect()
    }

    pub fn remove_packages(
        &mut self,
        args: &RemovePackageArgs,
        ctx: &Context,
    ) -> anyhow::Result<()> {
        let packages = match &args.packages {
            Some(pkgs) => pkgs.clone(),
            None => {
                if args.remove_orphans {
                    vec![]
                } else {
                    anyhow::bail!("No packages specified for removal");
                }
            }
        };
        let ignored_profiles: Vec<String> = vec![ctx.profile.name.clone()];
        let mut dirty = false;
        let mut to_remove = HashMap::new();
        for package_name in packages.iter() {
            if !self.packages.contains_key(package_name) {
                anyhow::bail!("Package '{}' not found in configuration", package_name);
            }
            let (is_safe, dependent_profiles, dependent_packages) =
                self.is_package_safe_to_remove(package_name, &ignored_profiles, &packages);
            if !is_safe && !args.force {
                anyhow::bail!(
                    "Package '{}' cannot be removed because it is depended on by profiles: {:?} and packages: {:?}. Use --force to override.",
                    package_name,
                    dependent_profiles,
                    dependent_packages
                );
            }
            to_remove.insert(
                package_name.clone(),
                self.packages
                    .get(package_name)
                    .ok_or_else(|| {
                        anyhow::anyhow!("Package '{}' not found in configuration", package_name)
                    })?
                    .clone(),
            );
        }
        if to_remove.is_empty() && !args.remove_orphans {
            cprintln("No packages to remove.", &LogLevel::Info);
            return Ok(());
        }
        for (package_name, pkg) in to_remove.iter() {
            if args.dry_run {
                cprintln(
                    &format!("Package '{}' would be removed (dry run)", package_name),
                    &LogLevel::Info,
                );
                continue;
            }
            match self.remove_package(pkg, ctx) {
                Err(e) => {
                    anyhow::bail!("Error removing package '{}': {}", package_name, e);
                }
                Ok(_) => {
                    dirty = true;
                    cprintln(
                        &format!("Package '{}' removed", package_name),
                        &LogLevel::Info,
                    );
                }
            }
        }
        if args.remove_orphans {
            let orphan_packages = self.get_orphan_packages();
            for orphan in orphan_packages.iter() {
                if args.dry_run {
                    cprintln(
                        &format!("Orphan package '{}' would be removed (dry run)", orphan),
                        &LogLevel::Info,
                    );
                    continue;
                }
                let pkg = self
                    .packages
                    .get(orphan)
                    .ok_or_else(|| {
                        anyhow::anyhow!("Orphan package '{}' not found in configuration", orphan)
                    })?
                    .clone();
                match self.remove_package(&pkg, ctx) {
                    Err(e) => {
                        anyhow::bail!("Error removing orphan package '{}': {}", orphan, e);
                    }
                    Ok(_) => {
                        dirty = true;
                        cprintln(
                            &format!("Orphan package '{}' removed", orphan),
                            &LogLevel::Info,
                        );
                    }
                }
            }
        }
        if dirty {
            self.save(&ctx.working_dir)?;
        }
        Ok(())
    }

    pub fn remove_package(&mut self, pkg: &Package, ctx: &Context) -> anyhow::Result<()> {
        let src = ctx.working_dir.join(&pkg.src);
        let name = pkg.name.clone();
        self.packages.remove(&pkg.name);
        for profile in self.profiles.values_mut() {
            profile.dependencies.retain(|dep| dep != &name);
        }
        for pkg in self.packages.values_mut() {
            if let Some(deps) = &mut pkg.dependencies {
                deps.retain(|dep| dep != &name);
            }
        }
        if src.exists() {
            if src.is_dir() {
                if src.read_dir()?.next().is_some() {
                    std::fs::remove_dir_all(&src)?;
                } else {
                    std::fs::remove_dir(&src)?;
                }
            } else {
                std::fs::remove_file(&src)?;
            }
        }
        Ok(())
    }

    pub fn is_package_safe_to_remove(
        &self,
        package_name: &str,
        ignored_profiles: &[String],
        ignored_packages: &[String],
    ) -> (bool, Vec<String>, Vec<String>) {
        let mut dependent_profiles: Vec<String> = vec![];
        let mut dependent_packages: Vec<String> = vec![];
        let mut is_safe = true;
        for profile in self.profiles.values() {
            if ignored_profiles.contains(&profile.name) {
                continue;
            }
            if profile.dependencies.contains(&package_name.to_string()) {
                dependent_profiles.push(profile.name.clone());
                is_safe = false;
            }
        }
        for pkg in self.packages.values() {
            if ignored_packages.contains(&pkg.name) {
                continue;
            }
            if let Some(deps) = &pkg.dependencies
                && deps.contains(&package_name.to_string())
            {
                dependent_packages.push(pkg.name.clone());
                is_safe = false;
            }
        }
        (is_safe, dependent_profiles, dependent_packages)
    }

    pub fn remove_profile(
        &mut self,
        args: &ProfileRemoveArgs,
        ctx: &Context,
    ) -> anyhow::Result<()> {
        if !self.profiles.contains_key(&args.name) {
            anyhow::bail!("Profile '{}' not found in configuration", args.name);
        }
        if args.name == "default" {
            anyhow::bail!("Cannot remove the default profile");
        }
        if args.dry_run {
            cprintln(
                &format!("Profile '{}' would be removed (dry run)", args.name),
                &LogLevel::Info,
            );
            return Ok(());
        }
        self.profiles.remove(&args.name);
        self.save(&ctx.working_dir)?;
        cprintln(&format!("Profile '{}' removed", args.name), &LogLevel::Info);
        if args.remove_orphans {
            let orphan_packages = self.get_orphan_packages();
            let mut dirty = false;
            for orphan in orphan_packages.iter() {
                let pkg = self
                    .packages
                    .get(orphan)
                    .ok_or_else(|| {
                        anyhow::anyhow!("Orphan package '{}' not found in configuration", orphan)
                    })?
                    .clone();
                match self.remove_package(&pkg, ctx) {
                    Err(e) => {
                        anyhow::bail!("Error removing orphan package '{}': {}", orphan, e);
                    }
                    Ok(_) => {
                        dirty = true;
                        cprintln(
                            &format!("Orphan package '{}' removed", orphan),
                            &LogLevel::Info,
                        );
                    }
                }
            }
            if dirty {
                self.save(&ctx.working_dir)?;
            }
        }
        Ok(())
    }
}

pub(crate) fn print_stats(stats: &HashMap<BackupDeployResult, u32>, op_type: OpType) {
    let (op_name, op_success_name) = match op_type {
        OpType::Backup => ("Backup", "backed up"),
        OpType::Deploy => ("Deployment", "deployed"),
    };
    let mut summary_parts = vec![];
    if let Some(count) = stats.get(&BackupDeployResult::Success) {
        summary_parts.push(format!("✅ {} {}", count, op_success_name));
    }
    if let Some(count) = stats.get(&BackupDeployResult::Skipped) {
        summary_parts.push(format!("🔄 {} no changes", count));
    }
    if let Some(count) = stats.get(&BackupDeployResult::Failed) {
        summary_parts.push(format!("❌ {} failed", count));
    }
    if summary_parts.is_empty() {
        cprintln(
            &format!("No packages processed for {}", op_name),
            &LogLevel::Info,
        );
    } else {
        let mut summary_string = summary_parts.join(", ");
        summary_string.push('.');
        cprintln(&format!("{} summary:", op_name), &LogLevel::Info);
        cprintln(&summary_string, &LogLevel::Info);
    }
}
