"""UploadPlan — parse and iterate upload plans produced by BackupWatchman."""

from __future__ import annotations

import json
from pathlib import Path

from vcm_file_uploader._types import UploadFileEntry

_REQUIRED_FILE_FIELDS = {"path", "full_path", "size"}


class UploadPlanError(Exception):
    """Raised when an upload plan is invalid."""


class UploadPlan:
    """Lightweight wrapper around the JSON upload plan produced by backup_watchman.

    Provides validated, ordered iteration over files (largest first)
    and exposes plan metadata.
    """

    def __init__(
        self,
        files: list[UploadFileEntry],
        deletions: list[str] | None = None,
        timestamp: str | None = None,
    ) -> None:
        self.files = sorted(files, key=lambda f: f.size, reverse=True)
        self.deletions = deletions or []
        self.timestamp = timestamp

    @classmethod
    def from_json(cls, path: str | Path) -> UploadPlan:
        """Load and validate an upload plan from a JSON file."""
        path = Path(path)
        try:
            data = json.loads(path.read_text())
        except (json.JSONDecodeError, OSError) as exc:
            raise UploadPlanError(f"Failed to read upload plan: {exc}") from exc
        return cls.from_dict(data)

    @classmethod
    def from_dict(cls, data: dict) -> UploadPlan:
        """Load and validate an upload plan from a dictionary."""
        if not isinstance(data, dict):
            raise UploadPlanError("Upload plan must be a JSON object")
        if "files" not in data:
            raise UploadPlanError("Upload plan missing required key: 'files'")
        if not isinstance(data["files"], list):
            raise UploadPlanError("'files' must be a list")

        files: list[UploadFileEntry] = []
        for i, raw in enumerate(data["files"]):
            if not isinstance(raw, dict):
                raise UploadPlanError(f"files[{i}]: expected object, got {type(raw).__name__}")
            missing = _REQUIRED_FILE_FIELDS - raw.keys()
            if missing:
                raise UploadPlanError(f"files[{i}]: missing required fields: {sorted(missing)}")
            if not isinstance(raw["size"], (int, float)) or raw["size"] < 0:
                raise UploadPlanError(f"files[{i}]: 'size' must be a non-negative number")
            files.append(
                UploadFileEntry(
                    path=raw["path"],
                    full_path=raw["full_path"],
                    size=int(raw["size"]),
                    category=raw.get("category"),
                    sha256=raw.get("sha256"),
                    reason=raw.get("reason", "unknown"),
                    multipart=bool(raw.get("multipart", False)),
                )
            )

        return cls(
            files=files,
            deletions=data.get("deletions"),
            timestamp=data.get("timestamp"),
        )

    def __iter__(self):
        """Iterate over files in descending size order."""
        return iter(self.files)

    def __len__(self) -> int:
        return len(self.files)

    @property
    def total_bytes(self) -> int:
        return sum(f.size for f in self.files)

    @property
    def multipart_count(self) -> int:
        return sum(1 for f in self.files if f.multipart)
