// SPDX-FileCopyrightText: 2021-2023 Heiko Schaefer <heiko@schaefer.name>
// SPDX-FileCopyrightText: 2022 Nora Widdecke <mail@nora.pink>
// SPDX-FileCopyrightText: 2023 David Runge <dave@sleepmap.de>
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::path::PathBuf;

use anyhow::{Result, anyhow};
use clap::Parser;
use openpgp_card::{Card, state::Transaction};

use crate::{
    ENTER_ADMIN_PIN,
    ENTER_USER_PIN,
    util,
    util::{load_pin, print_gnuk_note},
};

#[derive(Parser, Debug)]
pub struct PinCommand {
    #[arg(
        name = "card ident",
        short = 'c',
        long = "card",
        help = "Identifier of the card to use"
    )]
    pub ident: String,

    #[command(subcommand)]
    pub cmd: PinSubCommand,
}

#[derive(Parser, Debug)]
pub enum PinSubCommand {
    /// Set User PIN
    ///
    /// Set a new User PIN by providing the current User PIN.
    SetUser {
        #[arg(
            name = "User PIN file old",
            short = 'p',
            long = "user-pin-old",
            help = "Optionally, get old User PIN from a file"
        )]
        user_pin_old: Option<PathBuf>,

        #[arg(
            name = "User PIN file new",
            short = 'q',
            long = "user-pin-new",
            help = "Optionally, get new User PIN from a file"
        )]
        user_pin_new: Option<PathBuf>,
    },

    /// Set Admin PIN
    ///
    /// Set a new Admin PIN by providing the current Admin PIN.
    SetAdmin {
        #[arg(
            name = "Admin PIN file old",
            short = 'P',
            long = "admin-pin-old",
            help = "Optionally, get old Admin PIN from a file"
        )]
        admin_pin_old: Option<PathBuf>,

        #[arg(
            name = "Admin PIN file new",
            short = 'Q',
            long = "admin-pin-new",
            help = "Optionally, get new Admin PIN from a file"
        )]
        admin_pin_new: Option<PathBuf>,
    },

    /// Reset User PIN with Admin PIN
    ///
    /// Set a new User PIN by providing the Admin PIN. This can also be used if the User PIN has
    /// been blocked.
    ResetUser {
        #[arg(
            name = "Admin PIN file",
            short = 'P',
            long = "admin-pin",
            help = "Optionally, get Admin PIN from a file"
        )]
        admin_pin: Option<PathBuf>,

        #[arg(
            name = "User PIN file new",
            short = 'p',
            long = "user-pin-new",
            help = "Optionally, get new User PIN from a file"
        )]
        user_pin_new: Option<PathBuf>,
    },

    /// Reset User PIN with Resetting Code
    ///
    /// Set a new User PIN by providing the Resetting Code. This can also be used if the User PIN
    /// has been blocked.
    ResetUserRc {
        #[arg(
            name = "Resetting Code file",
            short = 'r',
            long = "reset-code",
            help = "Optionally, get the Resetting Code from a file"
        )]
        reset_code: Option<PathBuf>,

        #[arg(
            name = "User PIN file new",
            short = 'p',
            long = "user-pin-new",
            help = "Optionally, get new User PIN from a file"
        )]
        user_pin_new: Option<PathBuf>,
    },

    /// Set Resetting Code
    ///
    /// Set a Resetting Code by providing the Admin PIN.
    SetReset {
        #[arg(
            name = "Admin PIN file",
            short = 'P',
            long = "admin-pin",
            help = "Optionally, get Admin PIN from a file"
        )]
        admin_pin: Option<PathBuf>,

        #[arg(
            name = "Resetting Code file",
            short = 'r',
            long = "reset-code",
            help = "Optionally, get the Resetting Code from a file"
        )]
        reset_code: Option<PathBuf>,
    },
}

pub fn pin(ident: &str, cmd: PinSubCommand) -> Result<()> {
    let mut open = util::open_card(ident)?;
    let card = open.transaction()?;

    match cmd {
        PinSubCommand::SetUser {
            user_pin_old,
            user_pin_new,
        } => set_user(user_pin_old, user_pin_new, card),

        PinSubCommand::SetAdmin {
            admin_pin_old,
            admin_pin_new,
        } => set_admin(admin_pin_old, admin_pin_new, card),

        PinSubCommand::ResetUser {
            admin_pin,
            user_pin_new,
        } => reset_user(admin_pin, user_pin_new, card),

        PinSubCommand::SetReset {
            admin_pin,
            reset_code,
        } => set_reset(admin_pin, reset_code, card),

        PinSubCommand::ResetUserRc {
            reset_code,
            user_pin_new,
        } => reset_user_rc(reset_code, user_pin_new, card),
    }
}

fn set_user(
    user_pin_old: Option<PathBuf>,
    user_pin_new: Option<PathBuf>,
    mut card: Card<Transaction>,
) -> Result<()> {
    let pinpad_modify = card.feature_pinpad_modify();

    let res = if !pinpad_modify {
        // get current user pin
        let user_pin1 = util::get_pin(&mut card, user_pin_old, ENTER_USER_PIN)?
            .ok_or_else(|| anyhow!("We have a PIN pad"))?;

        // verify pin
        card.verify_user_pin(user_pin1.clone())?;
        println!("PIN was accepted by the card.\n");

        let pin_new = match user_pin_new {
            None => {
                // ask user for new user pin
                util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?
            }
            Some(path) => load_pin(&path)?,
        };

        // set new user pin
        card.change_user_pin(user_pin1, pin_new)
    } else {
        // set new user pin via pinpad
        card.change_user_pin_pinpad(&|| {
            println!("Enter old User PIN on card reader pinpad, then new User PIN (twice).")
        })
    };

    match res {
        Err(err) => {
            println!("\nFailed to change the User PIN!");
            println!("{err:?}");
            print_gnuk_note(err, &mut card)?;
        }
        Ok(_) => println!("\nUser PIN has been set."),
    }
    Ok(())
}

fn set_admin(
    admin_pin_old: Option<PathBuf>,
    admin_pin_new: Option<PathBuf>,
    mut card: Card<Transaction>,
) -> Result<()> {
    let pinpad_modify = card.feature_pinpad_modify();

    if !pinpad_modify {
        // get current admin pin
        let admin_pin1 = util::get_pin(&mut card, admin_pin_old, ENTER_ADMIN_PIN)?
            .ok_or_else(|| anyhow!("We have a PIN pad"))?;

        // verify pin
        card.verify_admin_pin(admin_pin1.clone())?;
        // (Verifying the PIN here fixes this class of problems:
        // https://developers.yubico.com/PGP/PGP_PIN_Change_Behavior.html
        // It is also just generally more user friendly than failing later)
        println!("PIN was accepted by the card.\n");

        let pin_new = match admin_pin_new {
            None => {
                // ask user for new admin pin
                util::input_pin_twice("Enter new Admin PIN: ", "Repeat the new Admin PIN: ")?
            }
            Some(path) => load_pin(&path)?,
        };

        // set new admin pin
        card.change_admin_pin(admin_pin1, pin_new)?;
    } else {
        // set new admin pin via pinpad
        card.change_admin_pin_pinpad(&|| {
            println!("Enter old Admin PIN on card reader pinpad, then new Admin PIN (twice).")
        })?;
    };

    println!("\nAdmin PIN has been set.");
    Ok(())
}

fn reset_user(
    admin_pin: Option<PathBuf>,
    user_pin_new: Option<PathBuf>,
    mut card: Card<Transaction>,
) -> Result<()> {
    // verify admin pin
    match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? {
        Some(admin_pin) => {
            // verify pin
            card.verify_admin_pin(admin_pin)?;
        }
        None => {
            card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
        }
    }
    println!("PIN was accepted by the card.\n");

    // ask user for new user pin
    let pin = match user_pin_new {
        None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
        Some(path) => load_pin(&path)?,
    };

    let res = if let Ok(mut admin) = card.as_admin_card(None) {
        admin.reset_user_pin(pin)
    } else {
        return Err(anyhow::anyhow!("Failed to use card in admin-mode."));
    };

    match res {
        Err(err) => {
            println!("\nFailed to change the User PIN!");
            print_gnuk_note(err, &mut card)?;
        }
        Ok(_) => println!("\nUser PIN has been set."),
    }
    Ok(())
}

fn set_reset(
    admin_pin: Option<PathBuf>,
    reset_code: Option<PathBuf>,
    mut card: Card<Transaction>,
) -> Result<()> {
    // verify admin pin
    match util::get_pin(&mut card, admin_pin, ENTER_ADMIN_PIN)? {
        Some(admin_pin) => {
            // verify pin
            card.verify_admin_pin(admin_pin)?;
        }
        None => {
            card.verify_admin_pinpad(&|| println!("Enter Admin PIN on pinpad."))?;
        }
    }
    println!("PIN was accepted by the card.\n");

    // ask user for new resetting code
    let code = match reset_code {
        None => util::input_pin_twice(
            "Enter new resetting code: ",
            "Repeat the new resetting code: ",
        )?,
        Some(path) => load_pin(&path)?,
    };

    if let Ok(mut admin) = card.as_admin_card(None) {
        admin.set_resetting_code(code)?;
        println!("\nResetting code has been set.");
        Ok(())
    } else {
        Err(anyhow::anyhow!("Failed to use card in admin-mode."))
    }
}

fn reset_user_rc(
    reset_code: Option<PathBuf>,
    user_pin_new: Option<PathBuf>,
    mut card: Card<Transaction>,
) -> Result<()> {
    // reset by presenting resetting code

    let rst = if let Some(path) = reset_code {
        // load resetting code from file
        load_pin(&path)?
    } else {
        // input resetting code
        rpassword::prompt_password("Enter resetting code: ")?.into()
    };

    // ask user for new user pin
    let pin = match user_pin_new {
        None => util::input_pin_twice("Enter new User PIN: ", "Repeat the new User PIN: ")?,
        Some(path) => load_pin(&path)?,
    };

    // reset to new user pin
    match card.reset_user_pin(rst, pin) {
        Err(err) => {
            println!("\nFailed to change the User PIN!");
            print_gnuk_note(err, &mut card)
        }
        Ok(_) => {
            println!("\nUser PIN has been set.");
            Ok(())
        }
    }
}
