import hashlib
import hmac
import json
import os
import re
import secrets
import struct
import time
from base64 import b32decode, b32encode
from typing import Any

from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from pwdlib import PasswordHash

from .config import get_settings

password_hash = PasswordHash.recommended()


def hash_password(password: str) -> str:
    if len(password) < 12:
        raise ValueError("Password must contain at least 12 characters")
    return password_hash.hash(password)


def verify_password(password: str, encoded: str) -> bool:
    return password_hash.verify(password, encoded)


def canonical_payload_hash(payload: dict[str, Any]) -> str:
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(encoded.encode("utf-8")).hexdigest()


def payload_matches(payload: dict[str, Any], expected_hash: str) -> bool:
    return hmac.compare_digest(canonical_payload_hash(payload), expected_hash)


def generate_token() -> str:
    return secrets.token_urlsafe(32)


def hash_token(token: str) -> str:
    return hashlib.sha256(token.encode("utf-8")).hexdigest()


def privacy_hash(secret: str, value: str | None) -> str | None:
    if not value:
        return None
    return hmac.new(
        secret.encode("utf-8"), value.encode("utf-8"), digestmod=hashlib.sha256
    ).hexdigest()


def encrypt_secret(value: str, *, context: str) -> tuple[bytes, int]:
    settings = get_settings()
    key = settings.read_data_encryption_key()
    if not key:
        raise ValueError("The data-encryption key is unavailable or invalid")
    nonce = os.urandom(12)
    ciphertext = AESGCM(key).encrypt(nonce, value.encode("utf-8"), context.encode("utf-8"))
    return bytes([1]) + nonce + ciphertext, settings.data_encryption_key_version


def decrypt_secret(value: bytes, *, context: str, key_version: int) -> str:
    settings = get_settings()
    key = settings.read_data_encryption_key()
    if not key or key_version != settings.data_encryption_key_version or len(value) < 30:
        raise ValueError("The encrypted value cannot be opened with the active key")
    if value[0] != 1:
        raise ValueError("The encrypted value uses an unsupported format")
    try:
        plaintext = AESGCM(key).decrypt(value[1:13], value[13:], context.encode("utf-8"))
    except InvalidTag as error:
        raise ValueError("The encrypted value failed integrity verification") from error
    return plaintext.decode("utf-8")


def generate_totp_secret() -> str:
    return b32encode(os.urandom(20)).decode("ascii").rstrip("=")


def _totp_value(secret: str, step: int, *, digits: int = 6) -> str:
    padded = secret + "=" * (-len(secret) % 8)
    key = b32decode(padded, casefold=True)
    digest = hmac.new(key, struct.pack(">Q", step), hashlib.sha1).digest()
    offset = digest[-1] & 0x0F
    binary = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
    return str(binary % (10**digits)).zfill(digits)


def match_totp_step(
    secret: str, code: str, *, now: float | None = None, last_used_step: int | None = None
) -> int | None:
    normalized = re.sub(r"[\s-]+", "", code)
    if not normalized.isdigit() or len(normalized) != 6:
        return None
    current_step = int((now if now is not None else time.time()) // 30)
    for step in (current_step - 1, current_step, current_step + 1):
        if last_used_step is not None and step <= last_used_step:
            continue
        if hmac.compare_digest(_totp_value(secret, step), normalized):
            return step
    return None


def generate_recovery_codes(count: int = 10) -> list[str]:
    return ["-".join([secrets.token_hex(2).upper() for _ in range(4)]) for _ in range(count)]
