# -*- coding:utf-8 -*-


import asyncio
import base64
import os
import re
from datetime import UTC, datetime, timedelta
from functools import lru_cache

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import jwt

from odoo_api.core.connection import call_xmlrpc, sanitize_error_message
from odoo_api.core.config import get_settings
from odoo_api.exceptions import UnauthorizedException, DecryptionError


def create_access_token(user_data):
    settings = get_settings()
    expire_minutes = settings.access_token_expire_minutes
    user_password = user_data.get('password')
    encrypted_user_password = encrypt(user_password, settings.aes_secret_key)
    user_data.update({
        "password": encrypted_user_password
    })
    expire_datetime = datetime.now(UTC) + timedelta(minutes=expire_minutes)
    user_data.update({
        "exp": expire_datetime
    })
    return jwt.encode(user_data, settings.jwt_secret_key, algorithm=settings.hash_algorithm)


async def authenticate(db, username, password):
    """Authenticate Odoo user with provided API key, using Odoo external API

    Args:
        db (str): Odoo database name
        username (str): Odoo username
        password (str): Odoo user API key (or user password)

    Returns:
        str: Access token
    """
    try:
        host = get_settings().odoo_host
        common_url = re.sub(r'(?<!:)/+', '/', f"{host}/xmlrpc/2/common")
        loop = asyncio.get_running_loop()
        uid = await loop.run_in_executor(
            None, lambda: call_xmlrpc(common_url, 'authenticate', db, username, password, {})
        )
        if uid:
            user_data = {
                "host": host,
                "db": db,
                "uid": uid,
                "password": password,
            }
            token = create_access_token(user_data)
            return token
    except Exception as e:
        error_message = sanitize_error_message(e)
        raise UnauthorizedException(message="Could not validate credentials: %s" % error_message)


# ========== Encryption / Decryption ===============

@lru_cache(maxsize=256)
def generate_key(aes_secret_key: str, salt: bytes) -> bytes:
    """Derives a 32-byte key from an aes_secret_key and salt."""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=100000,
        backend=default_backend()
    )
    return kdf.derive(aes_secret_key.encode())


def encrypt(text: str, aes_secret_key: str) -> str:
    """
    Encrypts a text string using AES-256.
    Works with text of any length, including very short strings.
    """
    # Even empty strings can be encrypted
    text = text or ""
    salt = os.urandom(16)  # Generate a random salt
    key = generate_key(aes_secret_key, salt)
    iv = os.urandom(16)  # Generate a random IV
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()

    # Pad text to be a multiple of 16 bytes (AES block size) using PKCS#7
    # For a very short text, this ensures it's at least 16 bytes after padding
    padding_length = 16 - (len(text) % 16)
    padded_text = text + chr(padding_length) * padding_length

    ciphertext = encryptor.update(padded_text.encode()) + encryptor.finalize()

    # For very short inputs, the final structure is still:
    # 16 bytes (salt) + 16 bytes (IV) + at least 16 bytes (padded ciphertext)
    return base64.b64encode(salt + iv + ciphertext).decode()


def decrypt(encrypted_text: str, aes_secret_key: str) -> str:
    """Decrypts a previously encrypted text string using AES-256."""
    try:
        # Decode the base64 string to raw bytes
        encrypted_data = base64.b64decode(encrypted_text)

        # Ensure we have enough data (at minimum: salt + iv + 16 bytes of content)
        if len(encrypted_data) < 48:
            raise DecryptionError("Encrypted data is too short")

        # Extract components
        salt, iv, ciphertext = encrypted_data[:16], encrypted_data[16:32], encrypted_data[32:]
        key = generate_key(aes_secret_key, salt)

        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
        decryptor = cipher.decryptor()
        decrypted_padded_text = decryptor.update(ciphertext) + decryptor.finalize()

        # Validate and remove PKCS#7 padding
        padding_length = decrypted_padded_text[-1]

        # Check if padding_length is valid
        if padding_length > 16 or padding_length < 1:
            raise DecryptionError("Invalid padding")

        # Verify the padding is consistent (all padding bytes should be equal to padding_length)
        padding = decrypted_padded_text[-padding_length:]
        if not all(p == padding_length for p in padding):
            raise DecryptionError("Padding validation failed")

        # Remove padding to get the original text (could be empty string)
        return decrypted_padded_text[:-padding_length].decode('utf-8')

    except Exception as e:
        raise DecryptionError(f"Decryption failed: {str(e)}")
