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

use core::fmt;
use std::{
    collections::{
        HashMap,
        HashSet,
    },
    fs,
    io::Write,
    path::{
        Path,
        PathBuf,
    },
};

use pgp::{
    types::KeyTrait,
    Deserializable,
};
use yapma_common::http;

pub use ssh_key as ssh;
pub mod gpg {
    pub use pgp::{
        composed::signed_key::SignedPublicKey,
        types::KeyTrait,
        Deserializable,
    };

    pub type Fingerprint = Vec<u8>;
}

mod github;
use github::Github;

mod gitlab;
use gitlab::Gitlab;

mod resolver;
use resolver::Resolver as _;
use tracing::debug;

pub type Error = anyhow::Error;
pub type Result<T> = std::result::Result<T, Error>;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Platform {
    Github,
    Gitlab,
}

impl Platform {
    fn as_path(&self) -> &Path {
        match self {
            Self::Github => Path::new("github.com"),
            Self::Gitlab => Path::new("gitlab.com"),
        }
    }
}

#[derive(Clone, PartialEq)]
pub struct Identity {
    pub platform: Platform,
    pub username: String,
    pub ssh: HashSet<ssh::PublicKey>,
    pub gpg: HashMap<gpg::Fingerprint, gpg::SignedPublicKey>,
}

impl fmt::Debug for Identity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Identity")
            .field("platform", &self.platform)
            .field("username", &self.username)
            .field(
                "ssh",
                &self
                    .ssh
                    .iter()
                    .map(|key| key.to_openssh().unwrap())
                    .collect::<Vec<_>>(),
            )
            .field(
                "gpg",
                &self.gpg.keys().map(hex::encode_upper).collect::<Vec<_>>(),
            )
            .finish()
    }
}

impl Identity {
    pub async fn resolve<S, C>(platform: Platform, username: S, client: &C) -> Result<Self>
    where
        S: AsRef<str> + Into<String>,
        C: http::Client,
    {
        let resolver::Keys { ssh, gpg } = match platform {
            Platform::Github => Github::new(username.as_ref()).resolve(client).await?,
            Platform::Gitlab => Gitlab::new(username.as_ref()).resolve(client).await?,
        };

        Ok(Self {
            platform,
            username: username.into(),
            ssh,
            gpg,
        })
    }

    #[tracing::instrument(skip(root))]
    pub fn load(
        root: impl Into<PathBuf>,
        platform: Platform,
        username: String,
    ) -> Result<Option<Self>> {
        let mut root: PathBuf = root.into();
        root.push("identities");
        root.push(platform.as_path());
        root.push(&username);

        if !root.try_exists()? {
            return Ok(None);
        }

        let mut ssh = HashSet::new();
        let mut gpg = HashMap::new();

        debug!("Loading keys from {}", root.display());
        for entry in fs::read_dir(&root)? {
            let entry = entry?;
            let path = entry.path();
            match path.extension().and_then(|os| os.to_str()) {
                Some("ssh") => {
                    let key = ssh::PublicKey::read_openssh_file(&path)?;
                    debug!("Read SSH key from {}", path.display());
                    ssh.insert(key);
                },
                Some("gpg") => {
                    let ascii = fs::read_to_string(&path)?;
                    let (key, _) = gpg::SignedPublicKey::from_string(&ascii)?;
                    debug!("Read GPG key from {}", path.display());
                    gpg.insert(key.fingerprint(), key);
                },

                _ => {},
            }
        }

        Ok(Some(Self {
            platform,
            username,
            ssh,
            gpg,
        }))
    }

    #[tracing::instrument(skip_all, fields(platform = ?self.platform, username = self.username))]
    pub fn store(&self, root: impl Into<PathBuf>) -> Result<()> {
        let mut root: PathBuf = root.into();
        root.push("identities");
        root.push(self.platform.as_path());
        root.push(&self.username);

        fs::create_dir_all(&root)?;

        for key in &self.ssh {
            let fp = key.fingerprint(ssh::HashAlg::Sha512);
            root.push(bs58::encode(fp.as_bytes()).into_string());
            root.set_extension("tmp");

            let mut file = fs::File::options()
                .create_new(true)
                .write(true)
                .open(&root)?;
            let key = key.to_openssh()?;
            file.write_all(key.as_bytes())?;
            file.sync_all()?;
            drop(file);
            let target = root.with_extension("ssh");
            fs::rename(&root, &target)?;
            debug!("Wrote SSH key to {}", target.display());

            root.pop();
        }

        for (fp, key) in &self.gpg {
            root.push(hex::encode_upper(fp));
            root.set_extension("tmp");

            let mut file = fs::File::options()
                .create_new(true)
                .write(true)
                .open(&root)?;
            key.to_armored_writer(&mut file, None)?;
            file.sync_all()?;
            drop(file);
            let target = root.with_extension("gpg");
            fs::rename(&root, &target)?;
            debug!("Wrote GPG key to {}", target.display());

            root.pop();
        }

        Ok(())
    }
}