// SPDX-FileCopyrightText: Heiko Schaefer <heiko@schaefer.name>
// SPDX-License-Identifier: MIT OR Apache-2.0

use serde::{Deserialize, Serialize};

const CONFIG_APP: &str = "openpgp-card-state";
const CONFIG_FILE_NAME: &str = "config";

#[derive(Default, Serialize, Deserialize)]
pub(crate) struct Config {
    cards: Vec<CardSettings>,

    /// If set, this applies when putting a PIN for a new card
    pub(crate) default_pin_storage: Option<PinStorageType>,
}

#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct CardSettings {
    pub(crate) ident: String,
    pub(crate) pin_storage: Option<PinStorage>,
    pub(crate) nickname: Option<String>,
}

/// Specifies a type of PIN storage backend, optionally combined with global configuration for
/// that backend (e.g. a path to a binary)
#[derive(Clone, Serialize, Deserialize, PartialEq)]
pub enum PinStorageType {
    /// The keyring crate is used to store and retrieve the User PIN
    Keyring,

    /// The User PIN is stored directly in this config file.
    Direct,
}

/// Specifies a type of PIN storage backend, optionally combined with configuration for that
/// backend, and/or a card-specific payload
#[derive(Clone, Serialize, Deserialize)]
pub(crate) enum PinStorage {
    /// The keyring crate is used to store and retrieve the User PIN
    Keyring,

    /// The User PIN is stored directly in this config file.
    ///
    /// An empty string signifies use of the "Direct" backend, but with no PIN currently stored
    /// (e.g. after the PIN has been dropped because it failed verification on the card)
    Direct(String),
}

impl Config {
    #[cfg(unix)]
    fn set_config_permissions(path: &std::path::Path) -> anyhow::Result<()> {
        let metadata = path.metadata()?;
        let mut permissions = metadata.permissions();

        use std::os::unix::fs::PermissionsExt;
        permissions.set_mode(0o600); // Read/write for owner, no permissions for others.

        std::fs::set_permissions(path, permissions)?;

        Ok(())
    }

    #[cfg(not(unix))]
    fn set_config_permissions(_: &std::path::Path) -> anyhow::Result<()> {
        Ok(())
    }

    pub(crate) fn load() -> anyhow::Result<Self> {
        let path = confy::get_configuration_file_path(CONFIG_APP, Some(CONFIG_FILE_NAME))?;

        // Are we about to create a new config file?
        let exists = path.try_exists()?;

        let cfg = confy::load_path(&path)?;

        // TODO: this is a hack.
        // Ideally `confy` would expose a way to atomically set `PermissionsExt`.
        if !exists {
            // When creating a new config file, set permissions to 0600 on "unix"
            Self::set_config_permissions(&path)?;
        }

        Ok(cfg)
    }

    pub(crate) fn save(&self) -> anyhow::Result<()> {
        let path = confy::get_configuration_file_path(CONFIG_APP, Some(CONFIG_FILE_NAME))?;

        // Are we about to create a new config file?
        let exists = path.try_exists()?;

        confy::store_path(&path, self)?;

        // TODO: this is a hack.
        // Ideally `confy` would expose a way to atomically set `PermissionsExt`.
        //
        // (A very eager adversary could race for reading a new config file between the "store"
        // and permission setting calls).
        //
        if !exists {
            // When creating a new config file, set permissions to 0600 on "unix"
            Self::set_config_permissions(&path)?;
        }

        Ok(())
    }

    pub(crate) fn push(&mut self, cs: CardSettings) -> anyhow::Result<()> {
        // don't allow duplicate entries for one card ident
        if self.cards.iter().any(|old| old.ident == cs.ident) {
            return Err(anyhow::anyhow!(
                "Trying to insert duplicate entry for {}",
                cs.ident
            ));
        }

        self.cards.push(cs);
        Ok(())
    }

    pub(crate) fn remove(&mut self, ident: &str) -> anyhow::Result<()> {
        if let Some((pos, _)) = self
            .cards
            .iter()
            .enumerate()
            .find(|(_, cs)| cs.ident == ident)
        {
            self.cards.remove(pos);
        }

        Ok(())
    }

    pub(crate) fn ident(&self, ident: &str) -> Option<&CardSettings> {
        self.cards.iter().find(|card| card.ident == ident)
    }

    pub(crate) fn ident_mut(&mut self, ident: &str) -> Option<&mut CardSettings> {
        self.cards.iter_mut().find(|card| card.ident == ident)
    }
}
