"""Manifest schema and validation."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, List, Optional


def _normalize_chunking(data: Dict[str, Any]) -> Dict[str, Any]:
    """Normalize and validate the chunking object."""
    method = data.get("method")
    if not isinstance(method, str) or not method:
        raise ValueError("chunking.method must be a non-empty string")
    params = data.get("params", {})
    if params is None:
        params = {}
    if not isinstance(params, dict):
        raise ValueError("chunking.params must be an object")
    if not all(isinstance(k, str) for k in params.keys()):
        raise ValueError("chunking.params keys must be strings")
    return {"method": method, "params": params}


def _normalize_chunks(data: List[Any]) -> List[Dict[str, Any]]:
    chunks: List[Dict[str, Any]] = []
    for entry in data:
        if not isinstance(entry, dict):
            raise ValueError("chunk entry must be an object")
        if not all(isinstance(k, str) for k in entry.keys()):
            raise ValueError("chunk entry keys must be strings")
        if "hash" not in entry:
            raise ValueError("chunk entry missing hash")
        chunk_hash = entry["hash"]
        if not isinstance(chunk_hash, str) or not chunk_hash:
            raise ValueError("chunk hash must be a non-empty string")
        normalized = dict(entry)
        normalized["hash"] = chunk_hash
        chunks.append(normalized)
    return chunks


@dataclass(frozen=True)
class DocManifest:
    type: str
    version: int
    doc_id: str
    format: str
    chunking: Dict[str, Any]
    chunks: List[Dict[str, Any]]

    @staticmethod
    def from_dict(data: Dict[str, Any]) -> "DocManifest":
        """Parse and validate a doc-manifest from a dict."""
        required = ["type", "version", "doc_id", "format", "chunking", "chunks"]
        for key in required:
            if key not in data:
                raise ValueError(f"manifest missing required field: {key}")
        if data["type"] != "doc-manifest":
            raise ValueError("manifest type must be 'doc-manifest'")
        if data["version"] != 1:
            raise ValueError("unsupported manifest version")
        if not isinstance(data["doc_id"], str) or not data["doc_id"]:
            raise ValueError("doc_id must be a non-empty string")
        if not isinstance(data["format"], str) or not data["format"]:
            raise ValueError("format must be a non-empty string")
        if not isinstance(data["chunks"], list):
            raise ValueError("manifest chunks must be a list")
        if not isinstance(data["chunking"], dict):
            raise ValueError("chunking must be an object")
        chunking = _normalize_chunking(data["chunking"])
        chunks = _normalize_chunks(data["chunks"])
        return DocManifest(
            type=data["type"],
            version=int(data["version"]),
            doc_id=str(data["doc_id"]),
            format=str(data["format"]),
            chunking=chunking,
            chunks=chunks,
        )

    def to_dict(self) -> Dict[str, Any]:
        """Serialize this doc-manifest to a dict."""
        return {
            "type": self.type,
            "version": self.version,
            "doc_id": self.doc_id,
            "format": self.format,
            "chunking": {
                "method": self.chunking.get("method"),
                "params": self.chunking.get("params", {}),
            },
            "chunks": [dict(c) for c in self.chunks],
        }


@dataclass(frozen=True)
class CollectionEntry:
    doc_id: str
    path: Optional[str] = None

    @staticmethod
    def from_dict(data: Dict[str, Any]) -> "CollectionEntry":
        """Parse a collection entry from a dict."""
        if "doc_id" not in data:
            raise ValueError("collection entry missing doc_id")
        doc_id = str(data["doc_id"])
        if not doc_id:
            raise ValueError("collection entry doc_id must be a non-empty string")
        path = data.get("path")
        if path is not None:
            path = str(path)
            if not path:
                raise ValueError("collection entry path must be a non-empty string")
        return CollectionEntry(doc_id=doc_id, path=path)

    def to_dict(self) -> Dict[str, Any]:
        """Serialize this collection entry to a dict."""
        out: Dict[str, Any] = {"doc_id": self.doc_id}
        if self.path is not None:
            out["path"] = self.path
        return out


@dataclass(frozen=True)
class CollectionManifest:
    type: str
    version: int
    documents: List[CollectionEntry]

    @staticmethod
    def from_dict(data: Dict[str, Any]) -> "CollectionManifest":
        """Parse and validate a collection-manifest from a dict."""
        required = ["type", "version", "documents"]
        for key in required:
            if key not in data:
                raise ValueError(f"collection manifest missing required field: {key}")
        if data["type"] != "collection-manifest":
            raise ValueError("collection manifest type must be 'collection-manifest'")
        if data["version"] != 1:
            raise ValueError("unsupported collection manifest version")
        if not isinstance(data["documents"], list):
            raise ValueError("collection documents must be a list")
        docs = [CollectionEntry.from_dict(d) for d in data["documents"]]
        return CollectionManifest(type=data["type"], version=int(data["version"]), documents=docs)

    def to_dict(self) -> Dict[str, Any]:
        """Serialize this collection-manifest to a dict."""
        return {
            "type": self.type,
            "version": self.version,
            "documents": [d.to_dict() for d in self.documents],
        }
