"""Mechanical ZIP assembly for document packages.

PackageWriter takes a PackageManifest plus a customization callback
and produces a complete ZIP package in memory. It handles:
- Entry ordering (ODF mimetype-first)
- Compression modes (stored vs deflated)
- Extra entries (fonts, generated content)

This module does NO template loading or DOM manipulation.
"""

from __future__ import annotations

import io
import logging
import zipfile
from typing import Callable, Protocol

from tokenmoulds.packaging.manifest import PackageManifest, ManifestEntry
from tokenmoulds.packaging.types import CompressionMode, PackagedEntry

logger = logging.getLogger(__name__)


class TemplateLoader(Protocol):
    """Protocol for loading template content."""

    def read_text(self, rel_path: str, **kwargs: object) -> str: ...


EntryCustomizer = Callable[[str, bytes], bytes | None]


class PackageWriter:
    """Assemble a ZIP package from a manifest and customization hooks."""

    def __init__(self, *, compress_level: int = 9) -> None:
        self.compress_level = compress_level

    def build(
        self,
        manifest: PackageManifest,
        template_loader: TemplateLoader,
        customize: EntryCustomizer | None = None,
        extra_entries: list[PackagedEntry] | None = None,
    ) -> bytes:
        """Build a complete ZIP package.

        Args:
            manifest: Declarative package description.
            template_loader: TemplateResolver (or compatible) for templates.
            customize: Optional callback for entry customization.
            extra_entries: Additional entries (fonts, generated content).

        Returns:
            Complete ZIP package as bytes.
        """
        output = io.BytesIO()

        with zipfile.ZipFile(
            output,
            "w",
            compression=zipfile.ZIP_DEFLATED,
            compresslevel=self.compress_level,
        ) as zout:
            # ODF mimetype must be first and uncompressed
            if manifest.odf_mimetype is not None:
                zout.writestr(
                    "mimetype",
                    manifest.odf_mimetype,
                    compress_type=zipfile.ZIP_STORED,
                )

            for entry in manifest.sorted_entries():
                self._write_manifest_entry(zout, entry, template_loader, customize)

            if extra_entries:
                for extra in extra_entries:
                    compress_type = (
                        zipfile.ZIP_STORED
                        if extra.compression == CompressionMode.STORED
                        else zipfile.ZIP_DEFLATED
                    )
                    zout.writestr(
                        extra.archive_path, extra.data, compress_type=compress_type
                    )

        return output.getvalue()

    def _write_manifest_entry(
        self,
        zout: zipfile.ZipFile,
        entry: ManifestEntry,
        template_loader: TemplateLoader,
        customize: EntryCustomizer | None,
    ) -> None:
        """Load, optionally customize, and write a single entry."""
        try:
            content = template_loader.read_text(entry.template_name)
        except FileNotFoundError:
            logger.warning("Template not found: %s", entry.template_name)
            return

        data = content.encode("utf-8")

        if entry.customizable and customize is not None:
            result = customize(entry.archive_path, data)
            if result is None:
                return
            data = result

        compress_type = (
            zipfile.ZIP_STORED
            if entry.compression == CompressionMode.STORED
            else zipfile.ZIP_DEFLATED
        )
        zout.writestr(entry.archive_path, data, compress_type=compress_type)
