"""Derive non-identity manifests from existing doc-manifests."""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List
import shutil
import uuid

from .hash import sha256_hex
from .store import ObjectStore
from .attest import AttestationError, attest_manifest
from ..chunking.registry import get_method
from ..chunking.api import ChunkerContext, ManifestChunk
from ..manifest.codec import load_manifest_bytes, canonical_json_bytes


class DeriveError(Exception):
    pass


@dataclass(frozen=True)
class DeriveResult:
    doc_id: str
    source_manifest: str
    derived_manifest: str
    chunk_count: int
    chunking: Dict[str, Any]


def _ensure_initialized(store: ObjectStore) -> None:
    if not store.objects_dir().exists():
        raise DeriveError("store not initialized; run 'gnos init'")


def _reconstruct_bytes(manifest, store: ObjectStore) -> bytes:
    method = get_method(manifest.chunking.get("method"))
    params = manifest.chunking.get("params", {})
    chunks: list[ManifestChunk] = []
    for entry in manifest.chunks:
        chunk_hash = entry.get("hash")
        chunk = store.get(chunk_hash)
        if chunk is None:
            raise DeriveError("missing chunk in store")
        if sha256_hex(chunk) != chunk_hash:
            raise DeriveError("chunk hash mismatch")
        chunks.append(ManifestChunk(data=chunk, entry=entry))
    return method.reconstruct(chunks, params)


def derive_from_manifest(
    manifest_hash: str,
    store: ObjectStore,
    chunking: str,
    chunking_params: Dict[str, Any] | None = None,
    dry_run: bool = False,
) -> DeriveResult:
    """Derive a non-identity manifest from a doc-manifest."""
    _ensure_initialized(store)
    manifest_bytes = store.get(manifest_hash)
    if manifest_bytes is None:
        raise DeriveError("manifest not found in local store")
    manifest = load_manifest_bytes(manifest_bytes)

    # Ensure the source manifest is coherent before deriving.
    try:
        attest_manifest(manifest, store, expected_doc_id=manifest.doc_id, expected_manifest_hash=manifest_hash)
    except AttestationError as e:
        raise DeriveError(str(e)) from e

    data = _reconstruct_bytes(manifest, store)

    chunking_obj = {"method": chunking, "params": chunking_params or {}}
    method = get_method(chunking_obj["method"])
    ctx = ChunkerContext(path=None, format=manifest.format)
    split_result = method.split(data, chunking_obj.get("params", {}), ctx)
    if not isinstance(split_result.params, dict) or not all(isinstance(k, str) for k in split_result.params.keys()):
        raise DeriveError("chunker returned invalid params (must be dict with string keys)")
    chunking_obj = {"method": chunking_obj["method"], "params": split_result.params}

    staging_root = store.root / "tmp" / f"derive-{uuid.uuid4().hex}"
    staging_store = ObjectStore(staging_root)
    staging_store.init()

    chunk_entries: List[Dict[str, Any]] = []
    for chunk in split_result.chunks:
        if "hash" in chunk.entry:
            raise DeriveError("chunk entry must not contain 'hash' (computed by gnos)")
        if not all(isinstance(k, str) for k in chunk.entry.keys()):
            raise DeriveError("chunk entry keys must be strings")
        h = sha256_hex(chunk.data)
        entry = dict(chunk.entry)
        entry["hash"] = h
        chunk_entries.append(entry)
        if not staging_store.has(h):
            staging_store.put(h, chunk.data)

    derived_obj = {
        "type": "derived-manifest",
        "version": 1,
        "doc_id": manifest.doc_id,
        "source_manifest": manifest_hash,
        "chunking": chunking_obj,
        "chunks": chunk_entries,
    }
    derived_bytes = canonical_json_bytes(derived_obj)
    derived_hash = sha256_hex(derived_bytes)
    if not staging_store.has(derived_hash):
        staging_store.put(derived_hash, derived_bytes)

    try:
        if not dry_run:
            from .add import _commit_staged_objects

            _commit_staged_objects(staging_store, store)
    finally:
        shutil.rmtree(staging_root, ignore_errors=True)

    return DeriveResult(
        doc_id=manifest.doc_id,
        source_manifest=manifest_hash,
        derived_manifest=derived_hash,
        chunk_count=len(chunk_entries),
        chunking=chunking_obj,
    )
