# Copyright (c) Huawei Technologies Co., Ltd. 2025-2026. All rights reserved.
# MindIE is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         http://license.coscl.org.cn/MulanPSL2
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

import hashlib
import hmac
import secrets
from abc import ABC, abstractmethod
from typing import Iterable


class KeyEncryptionBase(ABC):
    """Base class for API key encryption strategies"""

    @classmethod
    @abstractmethod
    def encrypt_key(cls, plain_key: str) -> str:
        """
        Encrypt a plain API key

        Args:
            plain_key: The plain text API key to encrypt

        Returns:
            The encrypted key as a string
        """
        pass

    @abstractmethod
    def verify_key(self, plain_key: str, encrypted_key: str) -> bool:
        """
        Verify if a plain key matches an encrypted key

        Args:
            plain_key: The plain text key to verify
            encrypted_key: The encrypted key to compare against

        Returns:
            True if keys match, False otherwise
        """
        pass

    @abstractmethod
    def get_algorithm_name(self) -> str:
        """
        Get the name of the encryption algorithm

        Returns:
            Algorithm name as string
        """
        pass


class PBKDF2KeyEncryption(KeyEncryptionBase):
    """PBKDF2-based key encryption using salt and iterations"""

    def __init__(self, salt: str | None = None, iterations: int | None = 100000):
        """
        Initialize PBKDF2 encryption

        Args:
            salt: Salt for hashing. Must be provided before encryption
            iterations: Number of iterations for PBKDF2 (default: 100000)
        """
        self.salt = salt
        self.iterations = iterations

    @classmethod
    def generate_salt(cls) -> str:
        """
        Generate a random salt for PBKDF2

        Returns:
            Random salt as a hex string
        """
        return secrets.token_hex(16)

    @classmethod
    def encrypt_key(cls, plain_key: str, salt: str | None = None, iterations: int | None = 100000) -> str:
        """
        Encrypt key using PBKDF2 with salt and iterations

        Args:
            plain_key: Plain text key
            salt: Salt for hashing. If None, a random salt will be generated
            iterations: Number of iterations for PBKDF2 (default: 100000)

        Returns:
            Encrypted key in format: salt:iterations:pbkdf2_hash
        
        Raises:
            ValueError: If salt is not provided or plain_key is empty
        """
        if not plain_key:
            raise ValueError("Plain key cannot be empty")
        
        if not salt:
            salt = cls.generate_salt()

        salt_bytes = salt.encode('utf-8')
        key_bytes = plain_key.encode('utf-8')
        derived_key = hashlib.pbkdf2_hmac(
            'sha256',
            key_bytes,
            salt_bytes,
            iterations,
            dklen=32
        )
        encrypted = derived_key.hex()

        return f"{salt}:{iterations}:{encrypted}"

    @classmethod
    def get_salt(cls, encrypted_key: str) -> str:
        """
        Get salt from encrypted key
        Args:
            encrypted_key: Stored encrypted key in format salt:iterations:hash
        Returns:
            Salt used for encryption
        """
        parts = encrypted_key.split(':', 2)
        if len(parts) != 3:
            raise ValueError("Invalid encrypted key format")

        stored_salt, _, _ = parts
        return stored_salt

    def verify_key(self, plain_key: str, encrypted_key: str) -> bool:
        """
        Verify key against encrypted version

        Args:
            plain_key: Plain text key to verify
            encrypted_key: Stored encrypted key in format salt:iterations:hash

        Returns:
            True if keys match
        """
        if not plain_key or not encrypted_key:
            return False

        try:
            parts = encrypted_key.split(':', 2)
            if len(parts) != 3:
                return False

            stored_salt, stored_iterations, stored_hash = parts

            iterations = int(stored_iterations)

            salt_bytes = stored_salt.encode('utf-8')
            key_bytes = plain_key.encode('utf-8')
            derived_key = hashlib.pbkdf2_hmac(
                'sha256',
                key_bytes,
                salt_bytes,
                iterations,
                dklen=32
            )
            computed_hash = derived_key.hex()

            return hmac.compare_digest(computed_hash, stored_hash)

        except Exception:
            return False

    def get_algorithm_name(self) -> str:
        return "PBKDF2_SHA256"


_builtin_algorithms = {
    "PBKDF2_SHA256": PBKDF2KeyEncryption,
}

_encryption_registry: dict[str, type[KeyEncryptionBase]] = {}
_default_encryption: KeyEncryptionBase | None = None


def register_encryption_algorithm(name: str, algorithm_class: type[KeyEncryptionBase]) -> None:
    """
    Register an encryption algorithm

    Args:
        name: Algorithm name (e.g., "PBKDF2_SHA256")
        algorithm_class: The encryption class
    """
    _encryption_registry[name] = algorithm_class


def register_algorithm_from_config(algorithm_name: str) -> None:
    """
    Register an algorithm based on configuration name

    Args:
        algorithm_name: The algorithm name from configuration

    Raises:
        ValueError: If algorithm is not supported
    """
    if algorithm_name in _builtin_algorithms:
        register_encryption_algorithm(algorithm_name, _builtin_algorithms[algorithm_name])
    else:
        raise ValueError(f"Unsupported encryption algorithm: {algorithm_name}. "
                        f"Supported: {list(_builtin_algorithms.keys())}")


def get_encryption_algorithm(name: str) -> KeyEncryptionBase:
    """
    Get an encryption algorithm instance by name

    Args:
        name: Algorithm name

    Returns:
        Encryption algorithm instance

    Raises:
        ValueError: If algorithm is not registered
    """
    if name not in _encryption_registry:
        available = list(_encryption_registry.keys())
        raise ValueError(f"Unknown encryption algorithm '{name}'. Available: {available}")

    return _encryption_registry[name]()


def set_default_key_encryption(encryption: KeyEncryptionBase) -> None:
    """Set the default key encryption instance"""
    global _default_encryption
    _default_encryption = encryption


def set_default_key_encryption_by_name(name: str) -> None:
    """
    Set the default key encryption by algorithm name.
    This will register the algorithm if not already registered.

    Args:
        name: Algorithm name

    Raises:
        ValueError: If algorithm is not supported
    """
    if name not in _encryption_registry:
        register_algorithm_from_config(name)

    encryption = get_encryption_algorithm(name)
    set_default_key_encryption(encryption)


def get_default_key_encryption() -> KeyEncryptionBase:
    """Get the default key encryption instance"""
    if _default_encryption is None:
        set_default_key_encryption_by_name("PBKDF2_SHA256")
    return _default_encryption


def encrypt_api_key(plain_key: str) -> str:
    """Encrypt an API key using the default encryption"""
    return get_default_key_encryption().encrypt_key(plain_key)


def verify_api_key(plain_key: str, encrypted_key: str) -> bool:
    """Verify an API key against its encrypted version"""
    return get_default_key_encryption().verify_key(plain_key, encrypted_key)


def verify_api_key_against_valid_keys(plain_key: str, valid_keys: Iterable[str]) -> bool:
    """
    Verify a plain API key against a collection of encrypted keys (e.g. from config).

    Args:
        plain_key: The plain text key from the request.
        valid_keys: Encrypted keys to check against (e.g. api_key_config.valid_keys).

    Returns:
        True if the plain key matches any of the encrypted keys.
    """
    for encrypted_key in valid_keys:
        if verify_api_key(plain_key, encrypted_key):
            return True
    return False


def get_supported_algorithms() -> list[str]:
    """Get list of supported algorithm names"""
    return list(_builtin_algorithms.keys())