use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};

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

#[derive(Debug)]
pub struct Registry {
    pub deps: IndexMap<String, Dep>,
}

#[derive(Debug, Clone, Default)]
pub struct Dep {
    pub version: Option<String>,
    pub path: Option<String>,
    pub features: Vec<String>,
    pub default_features: Option<bool>,
    /// When true (default), `path` is rebased relative to the target's Cargo.toml
    /// during sync. When false, `path` is rebased relative to the rustelo
    /// workspace root and the same string ships to every target — meant for
    /// paths that point outside the rustelo workspace.
    pub rebase: bool,
}

impl Dep {
    pub fn validate(&self, name: &str) -> Result<()> {
        if self.version.is_none() && self.path.is_none() {
            bail!("dep `{name}` declares neither `version` nor `path`");
        }
        Ok(())
    }

    pub fn is_simple_inline(&self) -> bool {
        self.path.is_none()
            && self.features.is_empty()
            && self.default_features.is_none()
            && self.version.is_some()
    }
}

/// Accepts both Cargo's compact form (`name = "x.y"`) and table form
/// (`name = { version = "x.y", features = [...] }`). The `rebase` field is
/// non-Cargo metadata used only by overlays' `[extra]` table; the registry
/// loader sources it from `registry/sync.toml` instead.
impl<'de> Deserialize<'de> for Dep {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum Value {
            Version(String),
            Table(Inner),
        }

        #[derive(Deserialize, Default)]
        #[serde(rename_all = "kebab-case")]
        struct Inner {
            #[serde(default)]
            version: Option<String>,
            #[serde(default)]
            path: Option<String>,
            #[serde(default)]
            features: Vec<String>,
            #[serde(default)]
            default_features: Option<bool>,
            #[serde(default = "default_rebase")]
            rebase: bool,
        }

        match Value::deserialize(deserializer)? {
            Value::Version(v) => Ok(Dep {
                version: Some(v),
                rebase: true,
                ..Dep::default()
            }),
            Value::Table(t) => Ok(Dep {
                version: t.version,
                path: t.path,
                features: t.features,
                default_features: t.default_features,
                rebase: t.rebase,
            }),
        }
    }
}

fn default_rebase() -> bool {
    true
}

#[derive(Deserialize)]
struct CargoManifest {
    workspace: WorkspaceSection,
}

#[derive(Deserialize)]
struct WorkspaceSection {
    #[serde(default)]
    dependencies: IndexMap<String, Dep>,
}

#[derive(Deserialize, Default)]
struct SyncSidecar {
    #[serde(default)]
    no_rebase: Vec<String>,
    #[serde(default)]
    impls: ImplsSection,
}

#[derive(Deserialize, Default)]
struct ImplsSection {
    #[serde(default)]
    known: Vec<String>,
}

/// Loaded view of `registry/sync.toml` — non-Cargo metadata used by the xtask.
#[derive(Debug, Default)]
pub struct Sidecar {
    /// Names of registry deps whose `path` is NOT rebased per target.
    pub no_rebase: HashSet<String>,
    /// Paths (relative to the rustelo workspace root) of implementations that
    /// consume this registry. Used by `sync-deps --all` to fan out.
    pub known_impls: Vec<String>,
}

/// Strips the leading `../` from a path stored in `registry/Cargo.toml`,
/// yielding a path relative to the rustelo workspace root. The leading `../`
/// is mandatory: `registry/Cargo.toml` lives one level inside the workspace,
/// so Cargo expects every path entry to begin with `../`. This normalization
/// lets the renderer treat `Dep.path` as rustelo-root-relative just like the
/// previous custom-schema format did, keeping the path-rebase logic stable
/// across the schema change.
fn normalize_registry_path(name: &str, raw: &str) -> Result<String> {
    let stripped = Path::new(raw).strip_prefix("..").map_err(|_| {
        anyhow::anyhow!(
            "registry path `{raw}` for `{name}` must start with `../` (registry/Cargo.toml is one level inside the workspace)"
        )
    })?;
    if stripped == Path::new("") {
        bail!("registry path `{raw}` for `{name}` resolves to the workspace root itself");
    }
    Ok(PathBuf::from(stripped).to_string_lossy().into_owned())
}

/// Loads `registry/sync.toml` once. Returns an empty Sidecar if the file is
/// absent (no_rebase + no impls).
pub fn load_sidecar(rustelo_root: &Path) -> Result<Sidecar> {
    let path = rustelo_root.join("registry/sync.toml");
    if !path.exists() {
        return Ok(Sidecar::default());
    }
    let text = fs::read_to_string(&path)
        .with_context(|| format!("reading {}", path.display()))?;
    let raw: SyncSidecar = toml::from_str(&text)
        .with_context(|| format!("parsing {}", path.display()))?;
    Ok(Sidecar {
        no_rebase: raw.no_rebase.into_iter().collect(),
        known_impls: raw.impls.known,
    })
}

pub fn load(rustelo_root: &Path) -> Result<Registry> {
    let manifest_path = rustelo_root.join("registry/Cargo.toml");
    let text = fs::read_to_string(&manifest_path)
        .with_context(|| format!("reading {}", manifest_path.display()))?;
    let manifest: CargoManifest = toml::from_str(&text)
        .with_context(|| format!("parsing {}", manifest_path.display()))?;

    let sidecar = load_sidecar(rustelo_root)?;

    let mut deps: IndexMap<String, Dep> =
        IndexMap::with_capacity(manifest.workspace.dependencies.len());
    for (name, mut dep) in manifest.workspace.dependencies {
        if sidecar.no_rebase.contains(&name) {
            dep.rebase = false;
        }
        if let Some(p) = &dep.path {
            dep.path = Some(normalize_registry_path(&name, p)?);
        }
        dep.validate(&name)?;
        deps.insert(name, dep);
    }

    for n in &sidecar.no_rebase {
        match deps.get(n) {
            Some(d) if d.path.is_some() => {}
            Some(_) => bail!(
                "registry/sync.toml lists `{n}` in no_rebase but it has no `path` in registry/Cargo.toml"
            ),
            None => bail!(
                "registry/sync.toml lists `{n}` in no_rebase but it is not in registry/Cargo.toml"
            ),
        }
    }

    Ok(Registry { deps })
}
