"""Manifest HMAC seal — tamper-evidence for local manifest files.

Generates an HMAC-SHA256 seal of the manifest database file, stored as
a JSON sidecar (``.seal``).  Before trusting a manifest (e.g. ``--resume``),
Census can verify the seal to detect offline tampering.

Follows the Tripwire/AIDE pattern of signing the integrity database.
The seal proves the manifest has not been modified since it was last
sealed by a key-holder.
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
import re
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from . import __version__


@dataclass
class SealResult:
    """Outcome of a seal verification."""

    valid: bool
    seal_path: str
    manifest_path: str
    error: Optional[str] = None


def seal_path_for(manifest_path: Path) -> Path:
    """Return the ``.seal`` sidecar path for a manifest."""
    return manifest_path.with_suffix(manifest_path.suffix + ".seal")


def _compute_hmac(manifest_path: Path, key_bytes: bytes) -> str:
    """Compute HMAC-SHA256 of the manifest file, streamed in 8 KB chunks."""
    h = hmac.new(key_bytes, digestmod=hashlib.sha256)
    with manifest_path.open("rb") as f:
        while True:
            chunk = f.read(8192)
            if not chunk:
                break
            h.update(chunk)
    return h.hexdigest()


def _parse_key(key_hex: str) -> bytes:
    """Parse and validate a 256-bit hex key, returning raw bytes.

    Uses strict regex to reject whitespace (which ``bytes.fromhex()``
    silently strips, producing a shorter-than-expected key).
    """
    if not re.fullmatch(r"[0-9a-fA-F]{64}", key_hex):
        raise ValueError("Seal key must be exactly 64 hex characters (256 bits)")
    return bytes.fromhex(key_hex)


def create_seal(manifest_path: Path, key_hex: str) -> Path:
    """Create an HMAC-SHA256 seal for a manifest file.

    The seal is written to a ``.seal`` sidecar alongside the manifest.
    Returns the seal file path.
    """
    key_bytes = _parse_key(key_hex)
    manifest_path = manifest_path.resolve()
    if not manifest_path.exists():
        raise FileNotFoundError(f"Manifest not found: {manifest_path}")

    sp = seal_path_for(manifest_path)
    mac = _compute_hmac(manifest_path, key_bytes)

    seal_data = {
        "algorithm": "HMAC-SHA256",
        "manifest": manifest_path.name,
        "hmac": mac,
        "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "census_version": __version__,
    }

    fd = os.open(str(sp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write(json.dumps(seal_data, indent=2, default=str) + "\n")
    return sp


def verify_seal(manifest_path: Path, key_hex: str) -> SealResult:
    """Verify the HMAC-SHA256 seal of a manifest file.

    Uses ``hmac.compare_digest`` for constant-time comparison
    (timing-attack resistant).
    """
    key_bytes = _parse_key(key_hex)
    manifest_path = manifest_path.resolve()
    sp = seal_path_for(manifest_path)

    if not manifest_path.exists():
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error=f"Manifest not found: {manifest_path}",
        )

    if not sp.exists():
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error=f"Seal file not found: {sp}",
        )

    try:
        seal_data = json.loads(sp.read_text(encoding="utf-8"))
    except (json.JSONDecodeError, OSError) as exc:
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error=f"Cannot read seal: {exc}",
        )

    if not isinstance(seal_data, dict):
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error="Invalid seal format: not a JSON object",
        )

    stored_mac = seal_data.get("hmac", "")
    if not isinstance(stored_mac, str):
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error="Invalid seal format: 'hmac' field is not a string",
        )
    try:
        actual_mac = _compute_hmac(manifest_path, key_bytes)
    except OSError as exc:
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error=f"Cannot read manifest: {exc}",
        )

    if not hmac.compare_digest(stored_mac, actual_mac):
        return SealResult(
            valid=False,
            seal_path=str(sp),
            manifest_path=str(manifest_path),
            error="HMAC mismatch — manifest may have been tampered with",
        )

    return SealResult(
        valid=True,
        seal_path=str(sp),
        manifest_path=str(manifest_path),
    )
