"""Backend helpers implementing Firebase-backed storage."""
from __future__ import annotations

import base64
import functools
import hashlib
import json
import os
import re
from typing import Dict, Iterable, Optional, Tuple

import gnupg
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

try:
    from . import firebase_sync
except ImportError:  # pragma: no cover - direct execution support
    import firebase_sync  # type: ignore

try:
    from gnupg import _parsers as _gnupg_parsers
except ImportError:
    _gnupg_parsers = None
else:
    def _wrap_unknown_status(cls):
        original = cls._handle_status

        def _patched(self, key, value, _orig=original):
            if key == 'PINENTRY_LAUNCHED':
                return
            try:
                return _orig(self, key, value)
            except ValueError as exc:
                if 'Unknown status message' in str(exc):
                    return
                raise

        cls._handle_status = _patched

    for _name in dir(_gnupg_parsers):
        _obj = getattr(_gnupg_parsers, _name)
        if isinstance(_obj, type) and hasattr(_obj, '_handle_status'):
            _wrap_unknown_status(_obj)

# Public objects intentionally exposed for UI modules.
gpg = gnupg.GPG()

salt = b'my_app_master_salt'
ACCOUNT_ID: Optional[str] = None
master_password: Optional[str] = None
fernet: Optional[Fernet] = None
aes_secret_key: Optional[str] = None
session: Optional[firebase_sync.FirebaseSession] = None

_remote_store: Dict[str, str] = firebase_sync.DEFAULT_REMOTE_STORE.copy()

_EXP_RE = re.compile(r'^\s*(\d{1,2})\D*(\d{2})\s*$')

_TABLE0: list[Tuple[int, int]] = []
_TABLE1: list[Tuple[int, int]] = []
for v in range(10000):
    digs = [v // 1000, (v // 100) % 10, (v // 10) % 10, v % 10]
    for s, tbl in ((0, _TABLE0), (1, _TABLE1)):
        sm2 = sm5 = 0
        dbl = bool(s)
        for d in reversed(digs):
            n = d * 2 - 9 if dbl and d >= 5 else d * 2 if dbl else d
            dbl = not dbl
            sm2 ^= n & 1
            sm5 = (sm5 + n) % 5
        tbl.append((sm2, sm5))

SECURE_KEYS = ('private_key', 'sign_private_key', 'credentials', 'pgp_keyvault', 'cards')


def _sync_remote_store() -> None:
    if not session:
        raise RuntimeError('Firebase session not initialised')
    firebase_sync.replace_user_store(session, _remote_store)


def _initialise_session(sess: firebase_sync.FirebaseSession) -> None:
    """Hydrate local caches from Firestore for the authenticated session."""
    global session, ACCOUNT_ID, master_password, fernet
    store = firebase_sync.fetch_user_store(sess)
    session = sess
    ACCOUNT_ID = sess.email or sess.user_id
    master_password = None
    fernet = None
    _remote_store.clear()
    _remote_store.update(firebase_sync.DEFAULT_REMOTE_STORE)
    _remote_store.update(store)

def authenticate_with_email(email: str, password: str) -> None:
    """Authenticate against Firebase using email/password."""
    sess = firebase_sync.sign_in_with_email(email, password)
    _initialise_session(sess)


def register_account(email: str, password: str) -> None:
    """Register a new Firebase account and prompt email verification."""
    sess = firebase_sync.sign_up_with_email(email, password)
    try:
        firebase_sync.create_user_store(sess, firebase_sync.DEFAULT_REMOTE_STORE.copy())
    except Exception:
        pass
    try:
        firebase_sync.send_verification_email(sess)
    except Exception:
        pass
    raise firebase_sync.EmailNotVerifiedError(
        "Account created. A verification email has been sent. Please verify before signing in.",
        sess,
    )


def resend_verification_email(temp_session: firebase_sync.FirebaseSession) -> None:
    """Resend a Firebase email verification using the provided session."""
    firebase_sync.send_verification_email(temp_session)


def has_master_password() -> bool:
    return bool(_remote_store.get('master_pw_hash'))


def store_master_hash(password: str) -> None:
    _remote_store['master_pw_hash'] = hashlib.sha256(password.encode()).hexdigest()


@functools.lru_cache(maxsize=128)
def derive_master_key(password: str) -> bytes:
    return base64.urlsafe_b64encode(
        PBKDF2HMAC(hashes.SHA256(), 32, salt, 100000).derive(password.encode())
    )


@functools.lru_cache(maxsize=128)
def derive_aes_key(password: str) -> bytes:
    return PBKDF2HMAC(hashes.SHA256(), 32, salt, 100000).derive(password.encode())


def initialise_master_password(password: str) -> None:
    """Set the initial master password for a newly authenticated user."""
    global master_password, fernet
    fernet = Fernet(derive_master_key(password))
    master_password = password
    store_master_hash(password)
    _sync_remote_store()


def verify_master_password(password: str) -> bool:
    """Validate the provided password against the stored hash and initialise Fernet."""
    global fernet, master_password
    stored = _remote_store.get('master_pw_hash')
    if not stored or hashlib.sha256(password.encode()).hexdigest() != stored:
        return False
    fernet = Fernet(derive_master_key(password))
    master_password = password
    return True


def rotate_master_password(new_password: str) -> None:
    """Re-encrypt secure payloads using a new master password."""
    global master_password, fernet
    if not fernet:
        raise RuntimeError('Master password not initialised')
    old = fernet
    new_fernet = Fernet(derive_master_key(new_password))
    for key in SECURE_KEYS:
        encrypted = _remote_store.get(key)
        if not encrypted:
            continue
        try:
            plaintext = old.decrypt(encrypted.encode()).decode()
        except Exception:
            continue
        _remote_store[key] = new_fernet.encrypt(plaintext.encode()).decode()
    fernet = new_fernet
    master_password = new_password
    store_master_hash(new_password)
    _sync_remote_store()


def _encrypt_if_needed(key: str, text: str) -> str:
    enc = key in SECURE_KEYS
    if enc:
        if not fernet:
            raise RuntimeError('Master password context is not initialised')
        return fernet.encrypt(text.encode()).decode()
    return text


def store_key(key: str, text: str) -> None:
    _remote_store[key] = _encrypt_if_needed(key, text)
    _sync_remote_store()


def get_stored_key(key: str) -> Optional[str]:
    data = _remote_store.get(key)
    if not data:
        return None
    if key in SECURE_KEYS:
        if not fernet:
            return None
        try:
            return fernet.decrypt(data.encode()).decode()
        except Exception:
            return None
    return data


def _load_json_field(field: str) -> Dict[str, Dict[str, str]]:
    payload = get_stored_key(field)
    if not payload:
        return {}
    try:
        return json.loads(payload)
    except json.JSONDecodeError:
        return {}


def _store_json_field(field: str, data: Dict[str, Dict[str, str]]) -> None:
    store_key(field, json.dumps(data))


def load_credentials() -> Dict[str, Dict[str, str]]:
    return _load_json_field('credentials')


def save_credentials(creds: Dict[str, Dict[str, str]]) -> None:
    _store_json_field('credentials', creds)


def load_keyvault() -> Dict[str, Dict[str, str]]:
    return _load_json_field('pgp_keyvault')


def save_keyvault(vault: Dict[str, Dict[str, str]]) -> None:
    _store_json_field('pgp_keyvault', vault)


def persist_public_key(fingerprint: str, key_text: str) -> bool:
    key_text = (key_text or '').strip()
    if not key_text:
        return False
    vault = load_keyvault()
    entry = vault.get(fingerprint, {})
    if entry.get('public') == key_text:
        return False
    entry['public'] = key_text
    vault[fingerprint] = entry
    save_keyvault(vault)
    return True


def persist_private_key(fingerprint: str, key_text: str, passphrase: Optional[str] = None) -> bool:
    key_text = (key_text or '').strip()
    if not key_text:
        return False
    vault = load_keyvault()
    entry = vault.get(fingerprint, {})
    updated = False
    if entry.get('private') != key_text:
        entry['private'] = key_text
        updated = True
    if passphrase is not None:
        if passphrase:
            if entry.get('passphrase') != passphrase:
                entry['passphrase'] = passphrase
                updated = True
        elif 'passphrase' in entry:
            entry.pop('passphrase')
            updated = True
    if 'public' not in entry:
        try:
            exported = gpg.export_keys(fingerprint)
        except Exception:
            exported = None
        if exported:
            entry['public'] = exported
            updated = True
    if updated:
        vault[fingerprint] = entry
        save_keyvault(vault)
    return updated


def store_passphrase(fingerprint: str, passphrase: Optional[str]) -> bool:
    vault = load_keyvault()
    entry = vault.get(fingerprint)
    if not entry:
        return False
    changed = False
    if passphrase:
        if entry.get('passphrase') != passphrase:
            entry['passphrase'] = passphrase
            changed = True
    elif 'passphrase' in entry:
        entry.pop('passphrase')
        changed = True
    if changed:
        vault[fingerprint] = entry
        save_keyvault(vault)
    return changed


def load_passphrase_from_vault(fingerprint: str) -> Optional[str]:
    return load_keyvault().get(fingerprint, {}).get('passphrase')


def load_key_from_vault(fingerprint: str, kind: str) -> Optional[str]:
    return load_keyvault().get(fingerprint, {}).get(kind, '')


def list_vault_keys(kind: Optional[str] = None) -> list[str]:
    vault = load_keyvault()
    if kind is None:
        return sorted(vault.keys())
    return sorted(fp for fp, data in vault.items() if kind in data)


def load_cards() -> Dict[str, Dict[str, str]]:
    return _load_json_field('cards')


def save_cards(cards: Dict[str, Dict[str, str]]) -> None:
    _store_json_field('cards', cards)


def delete_account_data() -> None:
    if not session:
        raise RuntimeError('Firebase session not initialised')
    firebase_sync.delete_user_store(session)
    _remote_store.clear()
    _remote_store.update(firebase_sync.DEFAULT_REMOTE_STORE)
    _sync_remote_store()


def aes_encrypt(password: str, data: str) -> str:
    key, iv = derive_aes_key(password), os.urandom(16)
    padder = padding.PKCS7(128).padder()
    cipher = Cipher(
        algorithms.AES(key),
        modes.CBC(iv),
        backend=default_backend()
    ).encryptor()
    ct = cipher.update(padder.update(data.encode()) + padder.finalize()) + cipher.finalize()
    return base64.b64encode(iv + ct).decode()


def aes_decrypt(password: str, token: str) -> bytes:
    raw = base64.b64decode(token)
    iv, ct = raw[:16], raw[16:]
    cipher = Cipher(
        algorithms.AES(derive_aes_key(password)),
        modes.CBC(iv),
        backend=default_backend()
    ).decryptor()
    padded = cipher.update(ct) + cipher.finalize()
    unpadder = padding.PKCS7(128).unpadder()
    return unpadder.update(padded) + unpadder.finalize()


def load_public_key_from_text(text: str, persist: bool = True) -> str:
    key_text = (text or '').strip()
    if not key_text:
        raise ValueError('Empty public key')
    result = gpg.import_keys(key_text)
    if not _import_result_ok(result):
        raise ValueError('Failed to import public key')
    fingerprint = _latest_fingerprint(result)
    if not fingerprint:
        raise ValueError('No fingerprint returned for public key')
    if persist:
        persist_public_key(fingerprint, key_text)
    return fingerprint


def load_private_key_from_text(
    text: str,
    persist: bool = True,
    passphrase: Optional[str] = None,
    persist_bucket: Optional[str] = None,
) -> str:
    key_text = (text or '').strip()
    if not key_text:
        raise ValueError('Empty private key')
    result = gpg.import_keys(key_text)
    if not _import_result_ok(result):
        raise ValueError('Failed to import private key')
    fingerprint = _latest_fingerprint(result)
    if not fingerprint:
        raise ValueError('No fingerprint returned for private key')
    if persist:
        persist_private_key(fingerprint, key_text, passphrase)
    if persist_bucket:
        try:
            store_key(persist_bucket, key_text)
        except Exception:
            pass
    return fingerprint


def encrypt_message(pub_fpr: str, message: str) -> str:
    encrypted = gpg.encrypt(message, recipients=[pub_fpr], always_trust=True)
    if not encrypted.ok:
        raise ValueError('Encryption failed: ' + encrypted.status)
    return str(encrypted)


def decrypt_message(priv_fpr: str, ciphertext: str, passphrase: Optional[str]) -> str:
    decrypted = gpg.decrypt(ciphertext, passphrase=passphrase)
    if not decrypted.ok:
        raise ValueError('Decryption failed: ' + decrypted.status)
    return str(decrypted)


def sign_message_detached(priv_fpr: str, message: str, passphrase: Optional[str]) -> str:
    signature = gpg.sign(message, detach=True, passphrase=passphrase, default_key=priv_fpr)
    if not signature:
        raise ValueError('Signing failed')
    return str(signature)


def verify_signature(pub_fpr: str, signed_message: str) -> Tuple[bool, str]:
    verification = gpg.verify(signed_message)
    return bool(verification.valid), (verification.data.decode('utf-8', errors='ignore') if verification.data else '')


def generate_keypair(name: str, email: str, passphrase: Optional[str]) -> Tuple[str, str, str]:
    params = gpg.gen_key_input(
        name_real=name,
        name_email=email,
        passphrase=passphrase,
        key_type='RSA',
        key_length=2048
    )
    key = gpg.gen_key(params)
    if not key:
        raise ValueError('Key generation failed')
    fingerprint = str(key)
    public = gpg.export_keys(fingerprint)
    private = gpg.export_keys(fingerprint, True, passphrase=passphrase)
    return private, public, fingerprint


def valid_card(number: str) -> bool:
    digits = ''.join(filter(str.isdigit, number))
    sm2 = sm5 = 0
    rv = digits[::-1]
    for i in range(0, len(rv), 4):
        chunk = rv[i:i + 4]
        if len(chunk) == 4:
            tbl = _TABLE0 if (i & 1) == 0 else _TABLE1
            t = tbl[int(chunk[::-1])]
            sm2 ^= t[0]
            sm5 = (sm5 + t[1]) % 5
        else:
            dbl = bool(i & 1)
            for c in chunk:
                d = int(c)
                n = d * 2 - 9 if dbl and d >= 5 else d * 2 if dbl else d
                dbl = not dbl
                sm2 ^= n & 1
                sm5 = (sm5 + n) % 5
    return sm2 == 0 and sm5 == 0


def parse_expiration(text: Optional[str]) -> Optional[str]:
    match = _EXP_RE.match(text or '')
    if not match:
        return None
    month, year = int(match.group(1)), int(match.group(2))
    return f'{month:02d}/{year:02d}' if 1 <= month <= 12 else None


def _import_result_ok(result: object) -> bool:
    ok_attr = getattr(result, 'import_ok', None)
    if ok_attr is not None:
        return bool(ok_attr)
    counts = getattr(result, 'counts', None) or {}
    for key in ('imported', 'imported_rsa', 'sec_imported', 'imported_secret'):
        if counts.get(key):
            return True
    return bool(getattr(result, 'fingerprints', ()))


def _latest_fingerprint(result: object) -> Optional[str]:
    fingerprints = getattr(result, 'fingerprints', None)
    return fingerprints[-1] if fingerprints else None


__all__ = [
    'ACCOUNT_ID',
    'aes_decrypt',
    'aes_encrypt',
    'aes_secret_key',
    'authenticate_with_email',
    'delete_account_data',
    'derive_aes_key',
    'derive_master_key',
    'encrypt_message',
    'decrypt_message',
    'fernet',
    'generate_keypair',
    'get_stored_key',
    'gpg',
    'has_master_password',
    'initialise_master_password',
    'list_vault_keys',
    'load_cards',
    'load_credentials',
    'load_key_from_vault',
    'load_keyvault',
    'load_passphrase_from_vault',
    'load_private_key_from_text',
    'load_public_key_from_text',
    'master_password',
    'parse_expiration',
    'persist_private_key',
    'persist_public_key',
    'register_account',
    'resend_verification_email',
    'save_cards',
    'save_credentials',
    'save_keyvault',
    'session',
    'sign_message_detached',
    'store_key',
    'store_master_hash',
    'store_passphrase',
    'valid_card',
    'verify_master_password',
    'verify_signature',
    'rotate_master_password',
]
