"""Manifest generation for Mira builds."""

from __future__ import annotations

import json
import os
from typing import Any


class ManifestGenerator:
    """Generate and persist manifest.json for a Mira build."""

    def __init__(
        self,
        name: str,
        version: str,
        description: str | None,
        python_version: str,
        python_implementation: str,
        os_name: str,
        arch: str,
        pointer_size: int,
        include: str,
        entry_point: str | None,
        dependencies: dict[str, str],
        hash_files: bool = True,
    ) -> None:
        """Create a generator with build metadata.

        Args:
            name: Project name.
            version: Project version.
            description: Project description.
            python_version: Python version (e.g., 3.11.5).
            python_implementation: Python implementation (e.g., CPython).
            os_name: Target operating system identifier.
            arch: Target architecture identifier.
            pointer_size: Pointer size in bits.
            include: Root package directory name inside the archive.
            entry_point: Optional entry point.
            dependencies: Mapping of dependency name to version.
            hash_files: Whether to compute file hashes for integrity checks.
        """
        self.name = name
        self.version = version
        self.description = description or ""
        self.python_version = python_version
        self.python_implementation = python_implementation
        self.os_name = os_name
        self.arch = arch
        self.pointer_size = pointer_size
        self.include = include
        self.entry_point = entry_point
        self.dependencies = dependencies
        self.hash_files = hash_files

    def generate(self, build_dir: str) -> dict[str, Any]:
        """Generate a manifest dictionary.

        Args:
            build_dir: Build directory that contains libs/, dlls/, and project code.

        Returns:
            Manifest dictionary.
        """
        dlls_dir = os.path.join(build_dir, "dlls")
        libs_dir = os.path.join(build_dir, "libs")
        project_dir = os.path.join(build_dir, self.include)

        files = {
            "dlls": self._list_files(dlls_dir),
            "libs": self._list_files(libs_dir),
            "project": self._list_files(project_dir),
        }
        manifest: dict[str, Any] = {
            "manifest_version": 1,
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "python_version": self.python_version,
            "python_implementation": self.python_implementation,
            "os": self.os_name,
            "arch": self.arch,
            "pointer_size": self.pointer_size,
            "include": self.include,
            "entry_point": self.entry_point,
            "dependencies": dict(sorted(self.dependencies.items())),
            "files": files,
        }
        if self.hash_files:
            manifest["file_hashes"] = self._hash_files(build_dir, files, self.include)
        return manifest

    def save(self, build_dir: str, manifest: dict[str, Any]) -> None:
        """Save the manifest.json file.

        Args:
            build_dir: Build directory.
            manifest: Manifest data.
        """
        path = os.path.join(build_dir, "manifest.json")
        with open(path, "w", encoding="utf-8") as handle:
            json.dump(manifest, handle, indent=2, ensure_ascii=False)

    @staticmethod
    def _list_files(base_dir: str) -> list[str]:
        """List files under a directory with paths normalized to POSIX style."""
        if not os.path.isdir(base_dir):
            return []

        results: list[str] = []
        for root, dirs, files in os.walk(base_dir):
            dirs[:] = [d for d in dirs if d != "__pycache__"]
            for filename in files:
                full_path = os.path.join(root, filename)
                rel_path = os.path.relpath(full_path, base_dir)
                results.append(rel_path.replace(os.sep, "/"))

        results.sort()
        return results

    @staticmethod
    def _hash_files(
        build_dir: str, files: dict[str, list[str]], include: str
    ) -> dict[str, str]:
        """Compute SHA-256 hashes for files included in the archive."""
        hashes: dict[str, str] = {}
        for section, items in files.items():
            for rel_path in items:
                if section == "project":
                    archive_path = f"{include}/{rel_path}"
                    full_path = os.path.join(build_dir, include, rel_path)
                else:
                    archive_path = f"{section}/{rel_path}"
                    full_path = os.path.join(build_dir, section, rel_path)
                if not os.path.isfile(full_path):
                    continue
                hashes[archive_path.replace(os.sep, "/")] = _sha256_file(full_path)
        return hashes


def _sha256_file(path: str) -> str:
    import hashlib

    digest = hashlib.sha256()
    with open(path, "rb") as handle:
        while True:
            chunk = handle.read(64 * 1024)
            if not chunk:
                break
            digest.update(chunk)
    return digest.hexdigest()
