"""Build a verified MusicGridStudio update ZIP from two release folders."""

from __future__ import annotations

import argparse
from datetime import datetime, timezone
import json
from pathlib import Path
import sys
import zipfile

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from musicgridstudio.update_support import (
    ARCHITECTURE,
    MANIFEST_NAME,
    PAYLOAD_PREFIX,
    PRODUCT_ID,
    UPDATE_SCHEMA,
    file_sha256,
    inspect_update_package,
    normalize_relative_path,
)


INSTALL_MANIFEST = "install_manifest.json"


def _release_metadata(directory: Path) -> dict:
    path = directory / INSTALL_MANIFEST
    try:
        metadata = json.loads(path.read_text(encoding="utf-8-sig"))
    except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise ValueError(
            f"Could not read {INSTALL_MANIFEST} in {directory}."
        ) from exc
    if (
        metadata.get("product") != PRODUCT_ID
        or metadata.get("architecture") != ARCHITECTURE
    ):
        raise ValueError(f"Incompatible release folder: {directory}")
    return metadata


def _release_files(directory: Path) -> dict[str, Path]:
    files: dict[str, Path] = {}
    for path in sorted(directory.rglob("*")):
        if not path.is_file():
            continue
        relative_path = normalize_relative_path(
            path.relative_to(directory).as_posix()
        )
        files[relative_path] = path
    return files


def build_update_patch(
    base_directory: str | Path,
    target_directory: str | Path,
    output_path: str | Path,
) -> Path:
    base = Path(base_directory).resolve()
    target = Path(target_directory).resolve()
    output = Path(output_path).resolve()
    base_metadata = _release_metadata(base)
    target_metadata = _release_metadata(target)
    from_version = str(base_metadata.get("version", ""))
    to_version = str(target_metadata.get("version", ""))
    if from_version == to_version:
        raise ValueError("Base and target versions must be different.")

    base_files = _release_files(base)
    target_files = _release_files(target)
    changed: list[dict[str, object]] = []
    for relative_path, target_path in target_files.items():
        target_hash = file_sha256(target_path)
        base_path = base_files.get(relative_path)
        if base_path is not None and file_sha256(base_path) == target_hash:
            continue
        changed.append(
            {
                "path": relative_path,
                "sha256": target_hash,
                "size": target_path.stat().st_size,
            }
        )
    remove = sorted(set(base_files) - set(target_files))
    if not changed and not remove:
        raise ValueError("No file changes were found between the releases.")

    manifest = {
        "schema": UPDATE_SCHEMA,
        "product": PRODUCT_ID,
        "architecture": ARCHITECTURE,
        "publisher": "LeLien Consulting",
        "from_version": from_version,
        "to_version": to_version,
        "created_at": datetime.now(timezone.utc).isoformat(),
        "files": changed,
        "remove": remove,
    }
    output.parent.mkdir(parents=True, exist_ok=True)
    output.unlink(missing_ok=True)
    with zipfile.ZipFile(
        output,
        "w",
        compression=zipfile.ZIP_DEFLATED,
        compresslevel=9,
    ) as archive:
        archive.writestr(
            MANIFEST_NAME,
            json.dumps(manifest, ensure_ascii=False, indent=2),
        )
        for file_entry in changed:
            relative_path = str(file_entry["path"])
            archive.write(
                target_files[relative_path],
                PAYLOAD_PREFIX + relative_path,
            )

    inspect_update_package(output, from_version)
    return output


def _arguments() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Create a verified MusicGridStudio update package."
    )
    parser.add_argument("--base", required=True, help="Previous release folder")
    parser.add_argument("--target", required=True, help="New release folder")
    parser.add_argument("--output", required=True, help="Output update ZIP")
    return parser.parse_args()


def main() -> int:
    arguments = _arguments()
    output = build_update_patch(
        arguments.base,
        arguments.target,
        arguments.output,
    )
    print(f"Created: {output}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
