use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::{bail, Context, Result};
use serde::Deserialize;

use crate::project;

const CONFIG_FILE: &str = "donion.toml";
const DEFAULT_POLL_RATE_SECS: u64 = 10;

#[derive(Debug, Deserialize)]
struct RawConfig {
    #[serde(default)]
    branches: HashMap<String, Vec<RawBranchConfig>>,
}

#[derive(Debug, Deserialize)]
struct RawBranchConfig {
    build: Option<String>,
    deploy: Option<String>,
    poll_rate: Option<u64>,
    directory: Option<PathBuf>,
    discord: Option<String>,
    name: Option<String>,
}

#[derive(Debug, Clone)]
pub struct BranchConfig {
    pub branch: String,
    pub build: String,
    pub deploy: String,
    pub poll_rate: Duration,
    pub directory: PathBuf,
    pub discord: Option<String>,
    pub name: Option<String>,
}

pub fn load(base_dir: &Path) -> Result<Vec<BranchConfig>> {
    let config_path = base_dir.join(CONFIG_FILE);

    let raw = if config_path.exists() {
        let content = std::fs::read_to_string(&config_path)
            .with_context(|| format!("failed to read {}", config_path.display()))?;
        let cfg: RawConfig = toml::from_str(&content)
            .with_context(|| format!("failed to parse {}", config_path.display()))?;
        if cfg.branches.is_empty() {
            bail!("donion.toml exists but has no [[branches.*]] entries");
        }
        cfg
    } else {
        RawConfig {
            branches: HashMap::from([("main".to_string(), vec![RawBranchConfig {
                build: None,
                deploy: None,
                poll_rate: None,
                directory: None,
                discord: None,
                name: None,
            }])]),
        }
    };

    let mut resolved = Vec::new();
    for (branch_name, entries) in &raw.branches {
        for raw_branch in entries {
            let directory = match &raw_branch.directory {
                Some(d) if d.is_absolute() => d.clone(),
                Some(d) => base_dir.join(d),
                None => base_dir.to_path_buf(),
            };

            let directory = directory
                .canonicalize()
                .unwrap_or_else(|_| directory.clone());

            let (build, deploy) = resolve_commands(
                raw_branch.build.as_deref(),
                raw_branch.deploy.as_deref(),
                &directory,
            )?;

            resolved.push(BranchConfig {
                branch: branch_name.clone(),
                build,
                deploy,
                poll_rate: Duration::from_secs(raw_branch.poll_rate.unwrap_or(DEFAULT_POLL_RATE_SECS)),
                directory,
                discord: raw_branch.discord.clone(),
                name: raw_branch.name.clone(),
            });
        }
    }

    validate(&resolved)?;
    Ok(resolved)
}

fn resolve_commands(
    build: Option<&str>,
    deploy: Option<&str>,
    dir: &Path,
) -> Result<(String, String)> {
    match (build, deploy) {
        (Some(b), Some(d)) => Ok((b.to_string(), d.to_string())),
        _ => {
            let def = project::detect(dir)
                .ok_or_else(|| anyhow::anyhow!("no build/deploy specified and project detection failed for {}", dir.display()))?;
            let b = build
                .map(String::from)
                .unwrap_or_else(|| def.build_cmd.join(" "));
            let d = deploy
                .map(String::from)
                .unwrap_or_else(|| def.run_cmd.join(" "));
            Ok((b, d))
        }
    }
}

fn validate(configs: &[BranchConfig]) -> Result<()> {
    let mut seen_names: HashSet<&str> = HashSet::new();
    let mut seen_unnamed: HashSet<(&str, &Path)> = HashSet::new();

    for cfg in configs {
        if let Some(ref name) = cfg.name {
            if !seen_names.insert(name) {
                bail!("duplicate name '{}'", name);
            }
        } else {
            let key = (cfg.branch.as_str(), cfg.directory.as_path());
            if !seen_unnamed.insert(key) {
                bail!(
                    "duplicate branch '{}' targeting directory '{}' — add a name to distinguish",
                    cfg.branch,
                    cfg.directory.display()
                );
            }
        }
    }

    Ok(())
}

