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

pub use config::PinStorageType;

use crate::config::{CardSettings, Config, PinStorage};

const DEFAULT_PIN_BACKEND: PinStorageType = PinStorageType::Keyring;

mod config;
mod keyring;

/// Get persisted User PIN information about one card "ident".
pub fn get_pin(ident: &str) -> anyhow::Result<Option<String>> {
    let cfg = Config::load()?;

    let cs = cfg.ident(ident);
    let pinback_card = cs.and_then(|card| card.pin_storage.clone());

    let pw = match pinback_card {
        Some(PinStorage::Keyring) | None => {
            let pin = keyring::get(ident)?;

            if pin.is_some() {
                if cs.is_none() {
                    // FIXME: add config entry for this card to show we're using Keyring
                } else if pinback_card.is_none() {
                    // FIXME: set backend in this config entry to show we're using Keyring
                }
            }

            pin
        }
        Some(PinStorage::Direct(pw)) => {
            if !pw.is_empty() {
                Some(pw.clone())
            } else {
                None
            }
        }
    };

    Ok(pw)
}

/// Get persisted nickname for the card "ident".
pub fn get_nickname(ident: &str) -> anyhow::Result<Option<String>> {
    let cfg = Config::load()?;
    let cs = cfg.ident(ident);

    Ok(cs.and_then(|cs| cs.nickname.clone()))
}

/// Persist User PIN for a card.
pub fn set_pin(ident: &str, pin: &str, backend: Option<PinStorageType>) -> anyhow::Result<()> {
    let mut cfg = Config::load()?;

    // the target pin storage backend depends on:
    // - the explicit "backend" parameter, if set
    // - the global default in the config file, if set
    // - the hard coded default backend
    let pinback_target = match backend {
        Some(ref typ) => typ.clone(),
        None => cfg
            .default_pin_storage
            .clone()
            .unwrap_or(DEFAULT_PIN_BACKEND),
    };

    if let Some(cs) = cfg.ident_mut(ident) {
        // A configuration entry for this card exists. We update it.

        match cs.pin_storage {
            Some(PinStorage::Keyring) => {
                if backend.is_some() && backend != Some(PinStorageType::Keyring) {
                    // switching to a different PinStorageType is currently unsupported
                    return Err(anyhow::anyhow!("PinStorageType doesn't match existing setting. Please delete the current settings to start over."));
                }

                keyring::set(ident, pin)?
            }
            Some(PinStorage::Direct(ref mut pw)) => {
                if backend.is_some() && backend != Some(PinStorageType::Direct) {
                    // switching to a different PinStorageType is currently unsupported
                    return Err(anyhow::anyhow!("PinStorageType doesn't match existing setting. Please delete the current settings to start over."));
                }

                *pw = pin.to_string();
                cfg.save()?;
            }
            None => {
                // pin_storage for this CardSettings is unset. We:
                // - persist the pin, and
                // - set pin_storage to `Keyring`.
                keyring::set(ident, pin)?;
                cs.pin_storage = Some(PinStorage::Keyring);
                cfg.save()?;
            }
        }
    } else {
        // we're making a new configuration entry for this card

        // Follow global PIN storage backend setting, if any
        let pin_storage = match pinback_target {
            PinStorageType::Keyring => {
                keyring::set(ident, pin)?; // store PIN in default backend
                Some(PinStorage::Keyring)
            }
            PinStorageType::Direct => Some(PinStorage::Direct(pin.to_string())),
        };

        // add a new entry for this card to the config file
        let cs = CardSettings {
            ident: ident.to_string(),
            pin_storage,
            nickname: None,
        };
        cfg.push(cs)?;

        cfg.save()?;
    }

    Ok(())
}

/// Store a nickname for a card in the configuration file.
pub fn set_nickname(ident: &str, nickname: Option<&str>) -> anyhow::Result<()> {
    let mut cfg = Config::load()?;

    if let Some(settings) = cfg.ident_mut(ident) {
        // updating an existing configuration entry for this card
        settings.nickname = nickname.map(ToString::to_string)
    } else {
        // the configuration entry for this card is new
        let cs = CardSettings {
            ident: ident.to_string(),
            pin_storage: None,
            nickname: nickname.map(|s| s.to_string()),
        };
        cfg.push(cs)?;
    }

    cfg.save()?;

    Ok(())
}

/// Forget the persisted User PIN for a card.
///
/// This is the suggested action for applications when PIN validation fails.
///
/// This should be done to avoid locking a card accidentally: If an application re-tries an invalid
/// PIN repeatedly, the OpenPGP card will count down the allowed retries and eventually lock the
/// User PIN.
pub fn drop_pin(ident: &str) -> anyhow::Result<()> {
    let mut cfg = Config::load()?;

    if let Some(settings) = cfg.ident_mut(ident) {
        match settings.pin_storage {
            Some(PinStorage::Keyring) | None => keyring::delete(ident)?,
            Some(PinStorage::Direct(ref mut pw)) => {
                *pw = String::default();
                cfg.save()?;
            }
        }
    } else {
        keyring::delete(ident)? // try deleting in the default backend, just to be safe
    }
    Ok(())
}

/// Delete all config and User PIN storage for `ident`
pub fn delete(ident: &str) -> anyhow::Result<()> {
    let mut cfg = Config::load()?;

    // if the pin is stored in a backend that persists it, drop it from there!
    match cfg.ident(ident).map(|cs| &cs.pin_storage) {
        None | Some(None) | Some(Some(PinStorage::Keyring)) => {
            let _ = keyring::delete(ident).map_err(|e| {
                eprintln!(
                    "WARN: failed to drop pin for {} from PIN storage:\n{}\n",
                    ident, e
                )
            });
        }
        _ => {}
    }

    // drop the entry for "ident" from our config file
    let _ = cfg.remove(ident);
    cfg.save()?;

    Ok(())
}
