use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;

use anyhow::{bail, format_err, Error};
use nix::sys::stat::Mode;

use proxmox_sys::error::SysError;
use proxmox_sys::fs::CreateOptions;
use proxmox_tfa::totp::Totp;

pub use proxmox_tfa::api::{
    TfaChallenge, TfaConfig, TfaResponse, WebauthnConfig, WebauthnConfigUpdater,
};

use pbs_api_types::{User, Userid};
use pbs_buildcfg::configdir;
use pbs_config::{open_backup_lockfile, BackupLockGuard};

const CONF_FILE: &str = configdir!("/tfa.json");
const LOCK_FILE: &str = configdir!("/tfa.json.lock");

const CHALLENGE_DATA_PATH: &str = pbs_buildcfg::rundir!("/tfa/challenges");

pub fn read_lock() -> Result<BackupLockGuard, Error> {
    open_backup_lockfile(LOCK_FILE, None, false)
}

pub fn write_lock() -> Result<BackupLockGuard, Error> {
    open_backup_lockfile(LOCK_FILE, None, true)
}

/// Read the TFA entries.
pub fn read() -> Result<TfaConfig, Error> {
    let file = match File::open(CONF_FILE) {
        Ok(file) => file,
        Err(ref err) if err.not_found() => return Ok(TfaConfig::default()),
        Err(err) => return Err(err.into()),
    };

    Ok(serde_json::from_reader(file)?)
}

pub(crate) fn webauthn_config_digest(config: &WebauthnConfig) -> Result<[u8; 32], Error> {
    let digest_data = pbs_tools::json::to_canonical_json(&serde_json::to_value(config)?)?;
    Ok(openssl::sha::sha256(&digest_data))
}

/// Get the webauthn config with a digest.
///
/// This is meant only for configuration updates, which currently only means webauthn updates.
/// Since this is meant to be done only once (since changes will lock out users), this should be
/// used rarely, since the digest calculation is currently a bit more involved.
pub fn webauthn_config() -> Result<Option<(WebauthnConfig, [u8; 32])>, Error> {
    Ok(match read()?.webauthn {
        Some(wa) => {
            let digest = webauthn_config_digest(&wa)?;
            Some((wa, digest))
        }
        None => None,
    })
}

/// Requires the write lock to be held.
pub fn write(data: &TfaConfig) -> Result<(), Error> {
    let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));

    let json = serde_json::to_vec(data)?;
    proxmox_sys::fs::replace_file(CONF_FILE, &json, options, true)
}

/// Cleanup non-existent users from the tfa config.
pub fn cleanup_users(data: &mut TfaConfig, config: &proxmox_section_config::SectionConfigData) {
    data.users
        .retain(|user, _| config.lookup::<User>("user", user.as_str()).is_ok());
}

/// Container of `TfaUserChallenges` with the corresponding file lock guard.
///
/// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
pub struct TfaUserChallengeData {
    inner: proxmox_tfa::api::TfaUserChallenges,
    path: PathBuf,
    lock: File,
}

fn challenge_data_path_str(userid: &str) -> PathBuf {
    PathBuf::from(format!("{}/{}", CHALLENGE_DATA_PATH, userid))
}

impl TfaUserChallengeData {
    /// Rewind & truncate the file for an update.
    fn rewind(&mut self) -> Result<(), Error> {
        let pos = self.lock.seek(SeekFrom::Start(0))?;
        if pos != 0 {
            bail!(
                "unexpected result trying to rewind file, position is {}",
                pos
            );
        }

        proxmox_sys::c_try!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) });

        Ok(())
    }

    /// Save the current data. Note that we do not replace the file here since we lock the file
    /// itself, as it is in `/run`, and the typical error case for this particular situation
    /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
    /// other reasons then...
    ///
    /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
    /// way also unlocks early.
    fn save(mut self) -> Result<(), Error> {
        self.rewind()?;

        serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {
            format_err!("failed to update challenge file {:?}: {}", self.path, err)
        })?;

        Ok(())
    }
}

/// Get an optional TFA challenge for a user.
pub fn login_challenge(userid: &Userid) -> Result<Option<TfaChallenge>, Error> {
    let _lock = write_lock()?;
    read()?.authentication_challenge(UserAccess, userid.as_str(), None)
}

/// Add a TOTP entry for a user. Returns the ID.
pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result<String, Error> {
    let _lock = write_lock();
    let mut data = read()?;
    let id = data.add_totp(userid.as_str(), description, value);
    write(&data)?;
    Ok(id)
}

/// Add recovery tokens for the user. Returns the token list.
pub fn add_recovery(userid: &Userid) -> Result<Vec<String>, Error> {
    let _lock = write_lock();

    let mut data = read()?;
    let out = data.add_recovery(userid.as_str())?;
    write(&data)?;
    Ok(out)
}

/// Add a u2f registration challenge for a user.
pub fn add_u2f_registration(userid: &Userid, description: String) -> Result<String, Error> {
    let _lock = crate::config::tfa::write_lock();
    let mut data = read()?;
    let challenge = data.u2f_registration_challenge(UserAccess, userid.as_str(), description)?;
    write(&data)?;
    Ok(challenge)
}

/// Finish a u2f registration challenge for a user.
pub fn finish_u2f_registration(
    userid: &Userid,
    challenge: &str,
    response: &str,
) -> Result<String, Error> {
    let _lock = crate::config::tfa::write_lock();
    let mut data = read()?;
    let id = data.u2f_registration_finish(UserAccess, userid.as_str(), challenge, response)?;
    write(&data)?;
    Ok(id)
}

/// Add a webauthn registration challenge for a user.
pub fn add_webauthn_registration(userid: &Userid, description: String) -> Result<String, Error> {
    let _lock = crate::config::tfa::write_lock();
    let mut data = read()?;
    let challenge =
        data.webauthn_registration_challenge(UserAccess, userid.as_str(), description, None)?;
    write(&data)?;
    Ok(challenge)
}

/// Finish a webauthn registration challenge for a user.
pub fn finish_webauthn_registration(
    userid: &Userid,
    challenge: &str,
    response: &str,
) -> Result<String, Error> {
    let _lock = crate::config::tfa::write_lock();
    let mut data = read()?;
    let id =
        data.webauthn_registration_finish(UserAccess, userid.as_str(), challenge, response, None)?;
    write(&data)?;
    Ok(id)
}

/// Verify a TFA challenge.
pub fn verify_challenge(
    userid: &Userid,
    challenge: &TfaChallenge,
    response: TfaResponse,
) -> Result<(), Error> {
    let _lock = crate::config::tfa::write_lock();
    let mut data = read()?;
    if data
        .verify(UserAccess, userid.as_str(), challenge, response, None)?
        .needs_saving()
    {
        write(&data)?;
    }
    Ok(())
}

#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct UserAccess;

/// Build th
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
    type Data = TfaUserChallengeData;

    /// Load the user's current challenges with the intent to create a challenge (create the file
    /// if it does not exist), and keep a lock on the file.
    fn open(&self, userid: &str) -> Result<Self::Data, Error> {
        crate::server::create_run_dir()?;
        let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600));
        proxmox_sys::fs::create_path(CHALLENGE_DATA_PATH, Some(options.clone()), Some(options))
            .map_err(|err| {
                format_err!(
                    "failed to crate challenge data dir {:?}: {}",
                    CHALLENGE_DATA_PATH,
                    err
                )
            })?;

        let path = challenge_data_path_str(userid);

        let mut file = std::fs::OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .truncate(false)
            .mode(0o600)
            .open(&path)
            .map_err(|err| format_err!("failed to create challenge file {:?}: {}", path, err))?;

        proxmox_sys::fs::lock_file(&mut file, true, None)?;

        // the file may be empty, so read to a temporary buffer first:
        let mut data = Vec::with_capacity(4096);

        file.read_to_end(&mut data).map_err(|err| {
            format_err!("failed to read challenge data for user {}: {}", userid, err)
        })?;

        let inner = if data.is_empty() {
            Default::default()
        } else {
            match serde_json::from_slice(&data) {
                Ok(inner) => inner,
                Err(err) => {
                    eprintln!(
                        "failed to parse challenge data for user {}: {}",
                        userid, err
                    );
                    Default::default()
                }
            }
        };

        Ok(TfaUserChallengeData {
            inner,
            path,
            lock: file,
        })
    }

    /// `open` without creating the file if it doesn't exist, to finish WA authentications.
    fn open_no_create(&self, userid: &str) -> Result<Option<Self::Data>, Error> {
        let path = challenge_data_path_str(userid);
        let mut file = match std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .truncate(false)
            .mode(0o600)
            .open(&path)
        {
            Ok(file) => file,
            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
            Err(err) => return Err(err.into()),
        };

        proxmox_sys::fs::lock_file(&mut file, true, None)?;

        let inner = serde_json::from_reader(&mut file).map_err(|err| {
            format_err!("failed to read challenge data for user {}: {}", userid, err)
        })?;

        Ok(Some(TfaUserChallengeData {
            inner,
            path,
            lock: file,
        }))
    }

    /// `remove` user data if it exists.
    fn remove(&self, userid: &str) -> Result<bool, Error> {
        let path = challenge_data_path_str(userid);
        match std::fs::remove_file(&path) {
            Ok(()) => Ok(true),
            Err(err) if err.not_found() => Ok(false),
            Err(err) => Err(err.into()),
        }
    }
}

impl proxmox_tfa::api::UserChallengeAccess for TfaUserChallengeData {
    fn get_mut(&mut self) -> &mut proxmox_tfa::api::TfaUserChallenges {
        &mut self.inner
    }

    fn save(self) -> Result<(), Error> {
        TfaUserChallengeData::save(self)
    }
}
