use anyhow::{Context, Result};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    fmt::Debug,
    fs::File,
    io::BufReader,
    path::{Path, PathBuf},
};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct LockFile {
    #[serde(skip)]
    pub(crate) path: PathBuf,
    nodes: IndexMap<String, LockNode>,
    root: String,
    version: usize,
}

impl LockFile {
    pub(crate) fn from_root(root: &Path) -> Result<Self> {
        let path = root.join("flake.lock");
        let file =
            File::open(&path).with_context(|| format!("Failed to open LockFile at {path:?}"))?;
        let reader = BufReader::new(file);
        let mut lockfile: LockFile = serde_json::from_reader(reader)?;
        lockfile.path = path;
        Ok(lockfile)
    }

    pub(crate) fn get_node_via_root(&self, input_name: &str) -> Option<&LockNode> {
        self.nodes.get(self.resolve_node_name_via_root(input_name)?)
    }

    pub(crate) fn update_node_via_root(&self, input_name: &str, new_node: &LockNode) -> Self {
        let node_name = self.resolve_node_name_via_root(input_name).unwrap();
        let mut modified = self.clone();
        _ = modified
            .nodes
            .insert(node_name.to_string(), new_node.clone());
        modified
    }

    fn resolve_node_name_via_root(&self, input_name: &str) -> Option<&str> {
        let Input::Direct(node_name) = self
            .nodes
            .get(&self.root)
            .unwrap()
            .inputs
            .as_ref()
            .unwrap()
            .get(input_name)?
        else {
            return None;
        };
        Some(node_name)
    }

    pub(crate) fn get_inputs(&self) -> Vec<String> {
        self.nodes
            .get(&self.root)
            .unwrap()
            .inputs
            .as_ref()
            .unwrap()
            .keys()
            .cloned()
            .collect()
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNode {
    #[serde(skip_serializing_if = "Option::is_none")]
    inputs: Option<HashMap<String, Input>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) locked: Option<LockNodeLocked>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) original: Option<LockNodeOriginal>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) flake: Option<bool>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
enum Input {
    Direct(String),
    Indirect(Vec<String>),
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "lowercase", tag = "type")]
pub(crate) enum LockNodeLocked {
    Git(LockNodeLockedGit),
    GitHub(LockNodeLockedGitHub),
    File(LockNodeLockedFile),
}

impl LockNodeLocked {
    pub(crate) fn last_modified(&self) -> Option<usize> {
        match self {
            LockNodeLocked::Git(locked) => Some(locked.last_modified),
            LockNodeLocked::GitHub(locked) => Some(locked.last_modified),
            LockNodeLocked::File(_locked) => None,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeLockedGitHub {
    last_modified: usize,
    nar_hash: String,
    owner: String,
    repo: String,
    rev: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeLockedGit {
    last_modified: usize,
    nar_hash: String,
    r#ref: String,
    rev: String,
    rev_count: usize,
    url: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeLockedFile {
    nar_hash: String,
    url: String,
}

#[derive(Deserialize, Clone, Serialize, PartialEq)]
#[serde(rename_all = "lowercase", tag = "type")]
pub(crate) enum LockNodeOriginal {
    Git(LockNodeOriginalGit),
    GitHub(LockNodeOriginalGitHub),
    Indirect(LockNodeOriginalIndirect),
    File(LockNodeOriginalFile),
}

impl Debug for LockNodeOriginal {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LockNodeOriginal::Git(git) => write!(f, "{git:?}"),
            LockNodeOriginal::GitHub(github) => write!(f, "{github:?}"),
            LockNodeOriginal::Indirect(indirect) => write!(f, "{indirect:?}"),
            LockNodeOriginal::File(file) => write!(f, "{file:?}"),
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeOriginalGit {
    url: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    rev: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    r#ref: Option<String>,
}

#[derive(Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeOriginalGitHub {
    owner: String,
    repo: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    rev: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    r#ref: Option<String>,
}

impl std::fmt::Debug for LockNodeOriginalGitHub {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "github:{}/{}", self.owner, self.repo)?;
        match (&self.rev, &self.r#ref) {
            (Some(rev), None) => write!(f, "/{rev}")?,
            (None, Some(r#ref)) => write!(f, "/{ref}")?,
            (Some(rev), Some(r#ref)) => write!(f, "/{rev}?ref={ref}")?,
            (None, None) => {}
        }
        Ok(())
    }
}

impl PartialEq for LockNodeOriginalGitHub {
    fn eq(&self, other: &Self) -> bool {
        self.owner.eq_ignore_ascii_case(&other.owner)
            && self.repo.eq_ignore_ascii_case(&other.repo)
            && self.rev == other.rev
            && self.r#ref == other.r#ref
    }
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeOriginalIndirect {
    id: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub(crate) struct LockNodeOriginalFile {
    url: String,
}
