use filesystem::create_with_600_perms;
use rand::distr::Alphanumeric;
use rand::{Rng, rng};
use std::fs;
use std::path::{Path, PathBuf};
use warp::Filter;

/// The default name of the file which stores the API token.
pub const PK_FILENAME: &str = "api-token.txt";

pub const PK_LEN: usize = 33;

/// Contains a randomly generated string which is used for authorization of requests to the HTTP API.
///
/// Provides convenience functions to ultimately provide:
///
///  - Verification of proof-of-knowledge of the public key in `self` for incoming HTTP requests,
///    via the `Authorization` header.
///
///  The aforementioned scheme was first defined here:
///
///  https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855
///
///  This scheme has since been tweaked to remove VC response signing and secp256k1 key generation.
///  https://github.com/sigp/lighthouse/issues/5423
pub struct ApiSecret {
    pk: String,
    pk_path: PathBuf,
}

impl ApiSecret {
    /// If the public key is already on-disk, use it.
    ///
    /// The provided `pk_path` is a path containing API token.
    ///
    /// If the public key file is missing on disk, create a new key and
    /// write it to disk (over-writing any existing files).
    pub fn create_or_open<P: AsRef<Path>>(pk_path: P) -> Result<Self, String> {
        let pk_path = pk_path.as_ref();

        // Check if the path is a directory
        if pk_path.is_dir() {
            return Err(format!(
                "API token path {:?} is a directory, not a file",
                pk_path
            ));
        }

        if !pk_path.exists() {
            // Create parent directories if they don't exist
            if let Some(parent) = pk_path.parent() {
                std::fs::create_dir_all(parent).map_err(|e| {
                    format!(
                        "Unable to create parent directories for {:?}: {:?}",
                        pk_path, e
                    )
                })?;
            }

            let length = PK_LEN;
            let pk: String = rng()
                .sample_iter(&Alphanumeric)
                .take(length)
                .map(char::from)
                .collect();

            // Create and write the public key to file with appropriate permissions
            create_with_600_perms(pk_path, pk.to_string().as_bytes()).map_err(|e| {
                format!(
                    "Unable to create file with permissions for {:?}: {:?}",
                    pk_path, e
                )
            })?;
        }

        let pk = fs::read(pk_path)
            .map_err(|e| format!("cannot read {}: {}", pk_path.display(), e))?
            .iter()
            .map(|&c| char::from(c))
            .collect();

        Ok(Self {
            pk,
            pk_path: pk_path.to_path_buf(),
        })
    }

    /// Returns the API token.
    pub fn api_token(&self) -> String {
        self.pk.clone()
    }

    /// Returns the path for the API token file
    pub fn api_token_path(&self) -> PathBuf {
        self.pk_path.clone()
    }

    /// Returns the values of the `Authorization` header which indicate a valid incoming HTTP
    /// request.
    ///
    /// For backwards-compatibility we accept the token in a basic authentication style, but this is
    /// technically invalid according to RFC 7617 because the token is not a base64-encoded username
    /// and password. As such, bearer authentication should be preferred.
    fn auth_header_values(&self) -> Vec<String> {
        vec![
            format!("Basic {}", self.api_token()),
            format!("Bearer {}", self.api_token()),
        ]
    }

    /// Returns a `warp` header which filters out request that have a missing or inaccurate
    /// `Authorization` header.
    pub fn authorization_header_filter(&self) -> warp::filters::BoxedFilter<()> {
        let expected = self.auth_header_values();
        warp::any()
            .map(move || expected.clone())
            .and(warp::filters::header::header("Authorization"))
            .and_then(move |expected: Vec<String>, header: String| async move {
                if expected.contains(&header) {
                    Ok(())
                } else {
                    Err(warp_utils::reject::invalid_auth(header))
                }
            })
            .untuple_one()
            .boxed()
    }
}
