// Copyright 2026 Thomas Zuyev

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(feature = "encryption")]
use crate::crypto::{CipherSuite, EncryptedSecret};

#[cfg(feature = "serde")]
use serde_big_array::BigArray;

use crate::{encoding::base64, random};

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[cfg(feature = "encryption")]
    #[error("Crypto error: {0}")]
    Crypto(#[from] crate::crypto::Error),

    #[error("Random error: {0}")]
    Random(#[from] crate::random::Error),
    #[error("Length mismatch: expected = {expected}; provided = {provided}")]
    LengthMismatch { expected: usize, provided: usize },
    #[error("Invalid base64: {0}")]
    Base64(#[from] base64::DecodeSliceError),
    #[error("Hash algorithm is missing")]
    HashAlgorithmMissing,
    #[error("Hash algorithm is unknown or invalid: {0}")]
    HashAlgorithmInvalid(u8),
}

pub type Result<T> = std::result::Result<T, Error>;

/// Cryptographically-random, fixed-size token material.
///
/// # Security notes
/// - Intended for high-entropy secrets such as session IDs, API tokens,
///   CSRF tokens, and refresh tokens.
/// - NOT suitable for passwords or other low-entropy user-provided secrets.
/// - Should never be stored directly; store only a cryptographic hash
///   (e.g. BLAKE3 with a server-side pepper).
///
/// Equality comparison should be performed on hashes, not tokens.
/// Original token material cannot be recovered.
#[derive(zeroize::ZeroizeOnDrop, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Token<const LEN: usize>(
    #[cfg_attr(feature = "serde", serde(with = "BigArray"))] [u8; LEN],
);

impl<const LEN: usize> Token<LEN> {
    #[must_use]
    pub fn random() -> Result<Self> {
        let mut s = [0u8; LEN];
        random::fill_bytes(&mut s)?;
        Ok(Self(s))
    }

    #[must_use]
    pub fn from_bytes(data: [u8; LEN]) -> Self {
        Self(data)
    }

    #[must_use]
    pub fn to_base64(&self) -> String {
        base64::encode(&self.0)
    }

    pub fn as_bytes(&self) -> &[u8] {
        &self.0
    }

    /// Decodes a URL-safe, unpadded base64 token.
    ///
    /// Fails if the decoded byte length does not exactly match `LEN`,
    /// preventing truncated or partially-zeroed secrets.
    #[must_use]
    pub fn from_base64(data: &str) -> Result<Self> {
        let mut s = [0u8; LEN];
        let written = base64::decode_slice(data, &mut s)?;

        if written == LEN {
            Ok(Self(s))
        } else {
            Err(Error::LengthMismatch {
                expected: LEN,
                provided: written,
            })
        }
    }

    #[cfg(feature = "encryption")]
    #[must_use]
    pub fn encrypt(&self, key: &[u8], algo: CipherSuite) -> Result<EncryptedToken<LEN>> {
        let enc = EncryptedSecret::encrypt(self.as_bytes(), key, algo)?;
        Ok(EncryptedToken::from_secret(enc))
    }

    /// Hashes the token using the currently selected default hashing algorithm.
    ///
    /// This method exists as a stable indirection point:
    /// - New tokens should always be hashed using this function.
    /// - The underlying algorithm may change over time (e.g. when introducing
    ///   a new hash version), without requiring call-site changes.
    /// - Existing stored hashes remain verifiable via `TokenHash`.
    ///
    /// The pepper must be a secret value stored separately from the database,
    /// typically in configuration or a secret manager.
    /// Compromise of both database and pepper allows token forgery.
    ///
    /// The default algorithm MUST remain suitable only for high-entropy,
    /// randomly generated tokens.
    #[must_use]
    pub fn to_default_hash(&self, pepper: &[u8]) -> TokenHash {
        self.to_blake3(pepper)
    }

    /// Hashes the token using BLAKE3 with an external pepper.
    ///
    /// # Warning
    /// This is NOT a password hashing function.
    /// BLAKE3 is intentionally fast and must only be used with
    /// high-entropy, randomly generated secrets.
    #[must_use]
    pub fn to_blake3(&self, pepper: &[u8]) -> TokenHash {
        let mut hasher = blake3::Hasher::new();
        hasher.update(pepper);
        hasher.update(&self.0);
        let hash = hasher.finalize();

        TokenHash::Blake3V1(hash)
    }
}

impl<const LEN: usize> std::fmt::Debug for Token<LEN> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "<redacted>")
    }
}

impl<const LEN: usize> std::fmt::Display for Token<LEN> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "<redacted>")
    }
}

/// Algorithm identifier for BLAKE3-based token hashes (version 1).
///
/// Stored as the first byte of the serialized representation.
/// Must never be changed or reused for a different algorithm.
const ALG_BLAKE3_V1: u8 = 0;

/// Stored verifier for high-entropy authentication tokens.
///
/// This type is intentionally:
/// - self-describing (algorithm ID is embedded in serialized form)
/// - forward-compatible (new algorithms can be added)
/// - non-reversible (no access to original token material)
///
/// # Storage format (BYTEA)
/// ```text
/// [1 byte algorithm id][algorithm-specific payload]
/// ```
///
/// This type is suitable for database storage and comparison,
/// but MUST NOT be used for password hashing.
#[derive(Clone)]
#[non_exhaustive]
pub enum TokenHash {
    /// BLAKE3-based token hash, version 1.
    ///
    /// Output size is fixed to 32 bytes.
    Blake3V1(blake3::Hash),
}

impl TokenHash {
    /// Serializes the token hash into a self-describing binary format.
    ///
    /// The resulting bytes are suitable for storage in a `BYTEA` column.
    pub fn to_bytes(&self) -> Vec<u8> {
        match &self {
            Self::Blake3V1(h) => {
                let mut res = Vec::with_capacity(1 + blake3::OUT_LEN);
                res.push(ALG_BLAKE3_V1);
                res.extend_from_slice(h.as_bytes());
                res
            }
        }
    }

    /// Deserializes a token hash from its binary representation.
    ///
    /// Fails if:
    /// - the input is empty
    /// - the algorithm identifier is unknown
    /// - the payload length does not match the expected format
    pub fn from_bytes(data: &[u8]) -> Result<Self> {
        if data.len() < 1 {
            return Err(Error::HashAlgorithmMissing);
        }

        match data[0] {
            ALG_BLAKE3_V1 => Ok(Self::blake3_from_bytes(&data[1..])?),
            algo => Err(Error::HashAlgorithmInvalid(algo)),
        }
    }

    /// Decodes a BLAKE3 v1 hash payload.
    ///
    /// The payload must be exactly `blake3::OUT_LEN` bytes.
    fn blake3_from_bytes(data: &[u8]) -> Result<Self> {
        let hash = blake3::Hash::from_slice(data).map_err(|_| Error::LengthMismatch {
            expected: blake3::OUT_LEN,
            provided: data.len(),
        })?;

        Ok(Self::Blake3V1(hash))
    }
}

impl std::fmt::Debug for TokenHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "<redacted>")
    }
}

impl std::fmt::Display for TokenHash {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "<redacted>")
    }
}

// Compiled only when encryption support is enabled.
// This allows the core domain model to exist without crypto.
#[cfg(feature = "encryption")]
// When serde is enabled, this type serializes exactly like `EncryptedSecret`.
// No additional wrapper structure is introduced in the serialized form.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
// When sqlx is enabled, this maps directly to the same DB representation
// as the inner encrypted bytes (typically BLOB/BYTEA).
#[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
#[cfg_attr(feature = "sqlx", sqlx(transparent))]
#[derive(Clone, Debug)]
/// Strongly-typed encrypted token wrapper.
///
/// `LEN` is a compile-time constant defining the exact size
/// of the decrypted token in bytes.
///
/// This gives:
///
/// - Static length guarantees
/// - Zero runtime polymorphism
/// - Separate types for different token sizes
///
/// Example:
///     EncryptedToken<16> != EncryptedToken<32>
///
/// Even though both wrap the same `EncryptedSecret`, they are
/// completely distinct types at compile time.
pub struct EncryptedToken<const LEN: usize>(EncryptedSecret);

#[cfg(feature = "encryption")]
impl<const LEN: usize> EncryptedToken<LEN> {
    /// Construct from serialized bytes:
    ///
    /// ```text
    /// [ algorithm (1 byte) | nonce | ciphertext | tag ]
    /// ```
    ///
    /// This does not decrypt or validate token length.
    /// It only reconstructs the encrypted container.
    #[must_use]
    pub fn from_encrypted_bytes(input: &[u8]) -> Result<Self> {
        let secret = EncryptedSecret::from_encrypted_bytes(input)?;
        Ok(Self(secret))
    }

    #[must_use]
    pub fn from_secret(secret: EncryptedSecret) -> Self {
        Self(secret)
    }

    /// Serialize into stable binary format:
    ///
    /// ```text
    /// [ algorithm (1 byte) | payload... ]
    /// ```
    ///
    /// Safe for:
    /// - Database storage
    /// - Network transfer
    /// - File persistence
    #[must_use]
    pub fn to_encrypted_bytes(&self) -> Vec<u8> {
        self.0.to_encrypted_bytes()
    }

    /// Decrypt the token and enforce its compile-time length.
    ///
    /// Steps:
    /// 1. Perform authenticated decryption (AEAD verification).
    /// 2. Ensure decrypted plaintext length == `LEN`.
    /// 3. Convert into strongly-typed `Token<LEN>`.
    ///
    /// Failure cases:
    /// - Wrong key
    /// - Tampered ciphertext
    /// - Length mismatch
    ///
    /// Length validation is critical here:
    /// it prevents truncated or malformed decrypted data
    /// from entering the domain layer.
    #[must_use]
    pub fn decrypt(&self, key: &[u8]) -> Result<Token<LEN>> {
        let plaintext = self.0.decrypt(key)?;
        let provided_len = plaintext.len();

        // Try converting Vec<u8> → [u8; LEN].
        // This enforces exact size equality at runtime.
        let arr = plaintext.try_into().map_err(|_| Error::LengthMismatch {
            expected: LEN,
            provided: provided_len,
        })?;
        Ok(Token::from_bytes(arr))
    }

    /// Returns the cipher suite used to encrypt this token.
    ///
    /// This is metadata only and does not expose token content.
    pub fn algorithm(&self) -> CipherSuite {
        self.0.algorithm()
    }
}

#[cfg(feature = "encryption")]
impl<const LEN: usize> std::fmt::Display for EncryptedToken<LEN> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// SQLx mapping for Postgres `BYTEA`.
///
/// This allows `TokenHash` to be used directly in `FromRow` structs
/// and query parameters without exposing its internal format.
#[cfg(feature = "sqlx")]
mod sqlx_impl {
    use super::*;
    use sqlx::{Database, Decode, Encode, Type, encode::IsNull, error::BoxDynError};

    #[cfg(feature = "sqlx_postgres")]
    impl sqlx::postgres::PgHasArrayType for TokenHash {
        fn array_type_info() -> sqlx::postgres::PgTypeInfo {
            // Under the hood, we encode this as a byte array,
            // so our Postgres array type is an array of BYTEA.
            <Vec<u8> as sqlx::postgres::PgHasArrayType>::array_type_info()
        }
    }

    impl<'a, DB: Database> Type<DB> for TokenHash
    where
        &'a [u8]: Type<DB>,
    {
        fn type_info() -> <DB as Database>::TypeInfo {
            <&[u8] as Type<DB>>::type_info()
        }

        fn compatible(ty: &<DB as Database>::TypeInfo) -> bool {
            <&[u8] as Type<DB>>::compatible(ty)
        }
    }

    impl<'a, DB: Database> Encode<'a, DB> for TokenHash
    where
        Vec<u8>: Encode<'a, DB>,
    {
        fn encode_by_ref(
            &self,
            buf: &mut <DB as Database>::ArgumentBuffer<'a>,
        ) -> std::result::Result<IsNull, BoxDynError> {
            <Vec<u8> as Encode<'a, DB>>::encode_by_ref(&self.to_bytes(), buf)
        }
        fn produces(&self) -> Option<<DB as Database>::TypeInfo> {
            <Vec<u8> as Encode<'a, DB>>::produces(&self.to_bytes())
        }
        fn size_hint(&self) -> usize {
            <Vec<u8> as Encode<'a, DB>>::size_hint(&self.to_bytes())
        }
    }

    impl<'a, DB: Database> Decode<'a, DB> for TokenHash
    where
        Vec<u8>: Decode<'a, DB>,
    {
        fn decode(value: <DB as Database>::ValueRef<'a>) -> std::result::Result<Self, BoxDynError> {
            Ok(Self::from_bytes(&<Vec<u8> as Decode<'a, DB>>::decode(
                value,
            )?)?)
        }
    }
}
