// Copyright © 2023 Kim Altintop <kim@eagain.io>
// SPDX-License-Identifier: GPL-2.0-only

use std::{
    env,
    fs,
    path::PathBuf,
};

use anyhow::{
    anyhow,
    ensure,
    Context as _,
};
use chrono::{
    DateTime,
    Utc,
};
use tracing::debug;

#[cfg(test)]
mod tests;

#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct Identity {
    pub display_name: String,
    pub email: String,
    pub last_modified: DateTime<Utc>,
    pub public_key: PublicKey,
}

impl Identity {
    pub fn load(name: &str) -> anyhow::Result<Self> {
        ensure!(!name.is_empty(), "identity name cannot be empty");

        let path = config_dir()
            .context("could not find global config dir")?
            .join("identities")
            .join(name)
            .join("identity.toml");
        let src = fs::read_to_string(&path)
            .with_context(|| format!("failed to read identity from {}", path.display()))?;

        Ok(toml::from_str(&src)?)
    }

    pub fn load_all() -> LoadAll {
        LoadAll { dir: None }
    }

    pub fn list() -> List {
        List { dir: None }
    }
}

#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct PublicKey {
    pub version: u64,
    pub algorithm: Algorithm,
    #[serde(default)]
    pub expires: Option<DateTime<Utc>>,
    pub signature: String,
    pub key: String,
}

#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Deserialize)]
#[cfg_attr(test, derive(serde::Serialize))]
#[repr(u8)]
pub enum Algorithm {
    Ed25519,
}

// TODO: Move to some shim for the pijul crate (i.e. stuff not in libpijul)
pub fn config_dir() -> Option<PathBuf> {
    env::var("PIJUL_CONFIG_DIR")
        .map(PathBuf::from)
        .ok()
        .or_else(|| {
            dirs::config_dir().map(|mut cfg| {
                cfg.push("pijul");
                cfg
            })
        })
}

/// Iterator for [`Identity::list`].
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct List {
    dir: Option<fs::ReadDir>,
}

impl Iterator for List {
    type Item = anyhow::Result<String>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(dir) = self.dir.as_mut() {
            let entry = dir.next()?;
            let item = entry.map_err(Into::into).and_then(|entry| {
                let name = entry
                    .file_name()
                    .into_string()
                    .map_err(|os| anyhow!("invalid name for identity: `{os:?}`"))?;
                let path = config_dir()
                    .expect("config dir must be valid")
                    .join("identities")
                    .join(&name)
                    .join("identity.toml");

                debug!("probe: {}", path.display());
                if path.try_exists()? {
                    Ok(Some(name))
                } else {
                    Ok(None)
                }
            });

            item.transpose()
        } else {
            match read_identities_dir() {
                Ok(dir) => {
                    self.dir = Some(dir);
                    self.next()
                },

                Err(e) => Some(Err(e)),
            }
        }
    }
}

/// Iterator for [`Identity::load_all`].
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct LoadAll {
    dir: Option<fs::ReadDir>,
}

impl Iterator for LoadAll {
    type Item = anyhow::Result<(String, Identity)>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(dir) = self.dir.as_mut() {
            let entry = dir.next()?;
            let item = entry.map_err(Into::into).and_then(|entry| {
                let name = entry
                    .file_name()
                    .into_string()
                    .map_err(|os| anyhow!("invalid name for identity: `{os:?}`"))?;
                let id = Identity::load(&name)
                    .with_context(|| format!("failed to load identity `{name}`"))?;

                Ok((name, id))
            });

            Some(item)
        } else {
            match read_identities_dir() {
                Ok(dir) => {
                    self.dir = Some(dir);
                    self.next()
                },
                Err(e) => Some(Err(e)),
            }
        }
    }
}

fn read_identities_dir() -> anyhow::Result<fs::ReadDir> {
    let path = config_dir()
        .context("could not find global config dir")?
        .join("identities");
    fs::read_dir(&path).with_context(|| format!("failed to read directory {}", path.display()))
}