"""Local manifest — maps SHA-256 hashes to file paths and metadata.

The manifest is a JSON file stored alongside the scanned directory,
providing the bridge between CertiSigma attestations (hash-only) and
the local filesystem (path + size + mtime).
"""

from __future__ import annotations

import json
import time
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Optional


@dataclass
class ManifestEntry:
    hash_hex: str
    path: str
    size: int
    mtime: float
    attested: bool = False
    attestation_id: Optional[str] = None
    attested_at: Optional[str] = None


@dataclass
class Manifest:
    version: str = "1"
    created_at: float = field(default_factory=time.time)
    updated_at: float = field(default_factory=time.time)
    root_dir: str = ""
    entries: dict[str, ManifestEntry] = field(default_factory=dict)

    @property
    def total(self) -> int:
        return len(self.entries)

    @property
    def attested_count(self) -> int:
        return sum(1 for e in self.entries.values() if e.attested)

    def add(self, entry: ManifestEntry) -> None:
        self.entries[entry.hash_hex] = entry
        self.updated_at = time.time()

    def get(self, hash_hex: str) -> Optional[ManifestEntry]:
        return self.entries.get(hash_hex)

    def mark_attested(
        self, hash_hex: str, attestation_id: str, attested_at: str
    ) -> None:
        entry = self.entries.get(hash_hex)
        if entry:
            entry.attested = True
            entry.attestation_id = attestation_id
            entry.attested_at = attested_at
            self.updated_at = time.time()

    def has_path(self, rel_path: str) -> Optional[ManifestEntry]:
        """Find an entry by relative path. Returns the entry or None."""
        for entry in self.entries.values():
            if entry.path == rel_path:
                return entry
        return None

    def merge(self, other: Manifest) -> int:
        """Merge *other* into this manifest, preserving attestation state.

        New entries are added.  Existing entries are updated only if the
        incoming entry has a newer mtime or different size (file changed).
        Attestation state from *self* is preserved unless the file changed.

        Returns the number of entries added or updated.
        """
        changed = 0
        for hash_hex, entry in other.entries.items():
            existing = self.entries.get(hash_hex)
            if existing is None:
                self.entries[hash_hex] = entry
                changed += 1
            elif existing.path != entry.path or existing.size != entry.size:
                entry.attested = existing.attested
                entry.attestation_id = existing.attestation_id
                entry.attested_at = existing.attested_at
                self.entries[hash_hex] = entry
                changed += 1
        self.updated_at = time.time()
        return changed

    def save(self, path: Path) -> None:
        data = {
            "version": self.version,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "root_dir": self.root_dir,
            "entries": {k: asdict(v) for k, v in self.entries.items()},
        }
        tmp = path.with_suffix(".tmp")
        tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
        tmp.replace(path)

    @classmethod
    def load(cls, path: Path) -> Manifest:
        data = json.loads(path.read_text(encoding="utf-8"))
        manifest = cls(
            version=data["version"],
            created_at=data["created_at"],
            updated_at=data["updated_at"],
            root_dir=data["root_dir"],
        )
        for hash_hex, entry_data in data.get("entries", {}).items():
            manifest.entries[hash_hex] = ManifestEntry(**entry_data)
        return manifest
