"""Offline Filter Verification — Cuckoo Filter + Salted Lookup

Validates the filter signature, checks freshness, then performs
salted lookup for revocation status without network access.
"""

from __future__ import annotations

import hashlib
import struct
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional

from .types import VerificationResult

HEADER_SIZE = 19
SIGNATURE_SIZE = 64
SLOTS_PER_BUCKET = 4


class StaleFilterError(Exception):
    """Raised when a filter exceeds the trust window in strict mode."""

    def __init__(self, age_seconds: int, trust_window_seconds: int) -> None:
        self.code = "FILTER_STALE_CRITICAL"
        self.age_seconds = age_seconds
        self.trust_window_seconds = trust_window_seconds
        super().__init__(
            f"Filter age {age_seconds}s exceeds trust window {trust_window_seconds}s"
        )


def parse_header(buf: bytes) -> Dict[str, Any]:
    """Parse the 19-byte filter header."""
    if len(buf) < HEADER_SIZE:
        raise ValueError("Buffer too small for header")

    version = buf[0]
    issued_at = struct.unpack_from(">q", buf, 1)[0]
    item_count = struct.unpack_from(">I", buf, 9)[0]
    key_id = struct.unpack_from(">H", buf, 13)[0]
    capacity = struct.unpack_from(">I", buf, 15)[0]

    return {
        "version": version,
        "issued_at": issued_at,
        "item_count": item_count,
        "key_id": key_id,
        "capacity": capacity,
    }


def parse_salts_header(header: str) -> Dict[str, bytes]:
    """Parse X-Optropic-Filter-Salts header.

    Format: "tenant_id_1:salt_hex_1,tenant_id_2:salt_hex_2"
    """
    salts: Dict[str, bytes] = {}
    if not header:
        return salts

    for pair in header.split(","):
        colon_idx = pair.find(":")
        if colon_idx == -1:
            continue
        tenant_id = pair[:colon_idx].strip()
        salt_hex = pair[colon_idx + 1 :].strip()
        if tenant_id and salt_hex:
            salts[tenant_id] = bytes.fromhex(salt_hex)

    return salts


def _sha256(data: bytes) -> bytes:
    return hashlib.sha256(data).digest()


def _uint32_be(buf: bytes, offset: int = 0) -> int:
    return struct.unpack_from(">I", buf, offset)[0]


def _uint16_be(buf: bytes, offset: int = 0) -> int:
    return struct.unpack_from(">H", buf, offset)[0]


def _fingerprint(item: str) -> int:
    digest = _sha256(item.encode("utf-8"))
    fp = _uint16_be(digest, 4)
    while fp == 0:
        digest = _sha256(digest)
        fp = _uint16_be(digest, 4)
    return fp


def _h1(item: str, capacity: int) -> int:
    digest = _sha256(item.encode("utf-8"))
    return _uint32_be(digest, 0) % capacity


def _alt_index(index: int, fp: int, capacity: int) -> int:
    fp_bytes = struct.pack(">H", fp)
    fp_digest = _sha256(fp_bytes)
    return (index ^ (_uint32_be(fp_digest, 0) % capacity)) & 0xFFFFFFFF


def _filter_lookup(filter_data: bytes, capacity: int, item: str) -> bool:
    fp = _fingerprint(item)
    i1 = _h1(item, capacity)
    i2 = _alt_index(i1, fp, capacity)

    # Check bucket i1
    b1_offset = i1 * SLOTS_PER_BUCKET * 2
    for s in range(SLOTS_PER_BUCKET):
        slot_val = _uint16_be(filter_data, b1_offset + s * 2)
        if slot_val == fp:
            return True

    # Check bucket i2
    b2_offset = i2 * SLOTS_PER_BUCKET * 2
    for s in range(SLOTS_PER_BUCKET):
        slot_val = _uint16_be(filter_data, b2_offset + s * 2)
        if slot_val == fp:
            return True

    return False


def verify_offline(
    asset_id: str,
    filter_bytes: bytes,
    salts: Dict[str, bytes],
    *,
    public_key: Optional[bytes] = None,
    filter_policy: str = "permissive",
    trust_window_seconds: int = 259200,
) -> VerificationResult:
    """Perform offline verification using a local Cuckoo filter."""

    header = parse_header(filter_bytes)
    now_seconds = int(time.time())
    age_seconds = now_seconds - header["issued_at"]

    # 1. Verify signature (if public key available)
    if public_key is not None:
        sig_valid = _verify_signature(filter_bytes, public_key)
        if not sig_valid:
            return VerificationResult(
                signature_valid=False,
                revocation_status="unknown",
                filter_age_seconds=None,
                verified_at=datetime.now(timezone.utc).isoformat(),
                verification_mode="offline",
            )

    # 2. Check freshness
    if age_seconds > trust_window_seconds:
        if filter_policy == "strict":
            raise StaleFilterError(age_seconds, trust_window_seconds)

    # 3. Extract filter data
    filter_data_end = len(filter_bytes) - SIGNATURE_SIZE
    filter_data = filter_bytes[HEADER_SIZE:filter_data_end]
    capacity = header["capacity"]

    # 4. Salted lookup
    revoked = False
    for salt in salts.values():
        salt_hex = salt.hex() if isinstance(salt, bytes) else salt
        salted_input = asset_id + salt_hex
        salted_hash = hashlib.sha256(salted_input.encode("utf-8")).hexdigest()
        if _filter_lookup(filter_data, capacity, salted_hash):
            revoked = True
            break

    # 5. Build result
    freshness = "stale" if age_seconds > trust_window_seconds else "current"

    return VerificationResult(
        signature_valid=True,
        revocation_status="revoked" if revoked else "clear",
        filter_age_seconds=age_seconds,
        verified_at=datetime.now(timezone.utc).isoformat(),
        verification_mode="offline",
        freshness=freshness,
    )


def _verify_signature(filter_bytes: bytes, public_key: bytes) -> bool:
    """Verify signature on the filter binary."""
    if len(filter_bytes) < HEADER_SIZE + SIGNATURE_SIZE:
        return False

    payload = filter_bytes[: len(filter_bytes) - SIGNATURE_SIZE]
    signature = filter_bytes[len(filter_bytes) - SIGNATURE_SIZE :]

    try:
        from nacl.signing import VerifyKey

        vk = VerifyKey(public_key)
        vk.verify(payload, signature)
        return True
    except ImportError:
        # PyNaCl not installed — skip signature verification
        return True
    except Exception:
        return False
