"""
utils/secrets_storage.py — зашифрованное хранение секретов (bot-токены и т.п.)

Клиент вводит токен бота в UI. Мы:
  1) Шифруем HMAC-SHA256-stream + HMAC-tag (encrypt-then-MAC) машинным ключом
     (деривируется из HWID).
  2) Сохраняем base64-строку в user_settings.json.
  3) При отправке сообщения расшифровываем в памяти и используем.

Использовали `cryptography.hazmat.primitives.ciphers.aead.AESGCM`, но в
PyInstaller-сборке нативные расширения этого модуля терялись и шифрование
падало с `No module named cryptography.hazmat.primitives.ciphers.aead` →
токен сохранялся в plaintext. Перешли на stdlib-only реализацию: HMAC-SHA256
keystream + HMAC-SHA256 auth tag. Не AES, но криптографически достаточно для
бот-токенов и не зависит от внешних библиотек — никогда не отвалится в exe.
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import logging
import os
from typing import Optional

logger = logging.getLogger("telepush.secrets")

_MAGIC = b"TPS2"  # TelePush Secret v2 — HMAC-stream
_MAGIC_LEGACY = b"TPS1"  # старые контейнеры на AES-GCM (могут лежать в user_settings)


def _derive_key() -> bytes:
    """Ключ из HWID. Stable per machine — один и тот же токен можно открыть
    только на том ПК где зашифрован."""
    try:
        from utils.hwid import generate_hwid
        seed = (generate_hwid() or "fallback").encode("utf-8")
    except Exception:
        seed = b"telepush-default-seed"
    return hashlib.sha256(b"telepush-secret-v1:" + seed).digest()


def _keystream(key: bytes, nonce: bytes, length: int) -> bytes:
    """HMAC-SHA256-CTR: keystream = HMAC(key, nonce || counter) для counter = 0..N.
    Безопасно как PRF (HMAC) при условии что nonce уникальный."""
    out = bytearray()
    counter = 0
    while len(out) < length:
        block = hmac.new(key, nonce + counter.to_bytes(8, "big"), hashlib.sha256).digest()
        out.extend(block)
        counter += 1
    return bytes(out[:length])


def encrypt_secret(plaintext: str) -> str:
    """Шифрует строку, возвращает base64-контейнер
    (magic + nonce + ciphertext + auth_tag). Пустая строка → пустая.

    Формат: TPS2 (4) | nonce (16) | ct (N) | tag (32)
    """
    if not plaintext:
        return ""
    try:
        key = _derive_key()
        nonce = os.urandom(16)
        pt = plaintext.encode("utf-8")
        ks = _keystream(key, nonce, len(pt))
        ct = bytes(a ^ b for a, b in zip(pt, ks))
        # encrypt-then-MAC: tag покрывает magic + nonce + ct
        tag = hmac.new(key, _MAGIC + nonce + ct, hashlib.sha256).digest()
        blob = _MAGIC + nonce + ct + tag
        return base64.b64encode(blob).decode("ascii")
    except Exception as e:
        logger.warning("[secrets] encrypt failed: %s — возвращаю plaintext", e)
        return plaintext


def _decrypt_v1_legacy(blob: bytes) -> Optional[str]:
    """Старые контейнеры на AESGCM. Если cryptography доступна — расшифруем.
    Если нет (frozen exe без модуля) — отдаём None и зовущий вернёт plaintext."""
    try:
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM
        key = _derive_key()
        nonce = blob[4:16]
        ct = blob[16:]
        aes = AESGCM(key)
        pt = aes.decrypt(nonce, ct, associated_data=_MAGIC_LEGACY)
        return pt.decode("utf-8")
    except Exception:
        return None


def decrypt_secret(ciphertext: str) -> str:
    """Расшифровывает base64-контейнер. Если строка не выглядит как наш
    контейнер (например уже plaintext) — возвращаем как есть. Это безопасно
    для миграции старых настроек где поле хранилось открытым."""
    if not ciphertext:
        return ""
    import re as _re
    if not _re.match(r'^[A-Za-z0-9+/=]+$', ciphertext) or len(ciphertext) % 4 != 0:
        return ciphertext
    try:
        blob = base64.b64decode(ciphertext, validate=True)
    except Exception:
        return ciphertext
    if blob.startswith(_MAGIC):
        # v2 формат
        try:
            if len(blob) < 4 + 16 + 32:
                return ciphertext
            key = _derive_key()
            nonce = blob[4:20]
            tag = blob[-32:]
            ct = blob[20:-32]
            expected_tag = hmac.new(key, _MAGIC + nonce + ct, hashlib.sha256).digest()
            if not hmac.compare_digest(tag, expected_tag):
                logger.warning("[secrets] auth tag mismatch — повреждён или чужой ключ")
                return ""
            ks = _keystream(key, nonce, len(ct))
            pt = bytes(a ^ b for a, b in zip(ct, ks))
            return pt.decode("utf-8")
        except Exception as e:
            logger.warning("[secrets] decrypt v2 failed: %s", e)
            return ""
    if blob.startswith(_MAGIC_LEGACY):
        # v1 (AESGCM) — пытаемся расшифровать; если cryptography нет в сборке,
        # вернём пустую строку (клиенту придётся ввести токен заново — лучше
        # чем тихо отдавать ciphertext как plaintext).
        legacy = _decrypt_v1_legacy(blob)
        if legacy is not None:
            return legacy
        logger.warning(
            "[secrets] старый AES-GCM контейнер не расшифровывается (нет cryptography) — "
            "введи токен бота заново, он сохранится в новом формате"
        )
        return ""
    # Валидный base64 но без нашего magic — скорее всего не наш секрет, отдаём как есть
    return ciphertext


def mask_secret(plaintext: str) -> str:
    """Маска для UI: показываем только последние 4 символа + длину."""
    if not plaintext:
        return ""
    n = len(plaintext)
    if n <= 6:
        return "*" * n
    return "*" * (n - 4) + plaintext[-4:]
