from __future__ import annotations

import json
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

from src.errors import UserError


MANIFEST_SCHEMA_VERSION = 1


@dataclass
class Manifest:
    schema_version: int
    inbox_root: str
    created_at: str
    updated_at: str
    files: dict[str, dict[str, object]]


def new_manifest(inbox_root: Path) -> Manifest:
    now = datetime.now(timezone.utc).isoformat()
    return Manifest(
        schema_version=MANIFEST_SCHEMA_VERSION,
        inbox_root=inbox_root.as_posix(),
        created_at=now,
        updated_at=now,
        files={},
    )


def load_manifest(path: Path) -> Manifest:
    try:
        raw = json.loads(path.read_text(encoding="utf-8"))
    except OSError as exc:
        raise UserError(f"Failed to read manifest: {path} ({exc})") from exc
    except json.JSONDecodeError as exc:
        raise UserError(f"Manifest is not valid JSON: {path} ({exc})") from exc
    return _manifest_from_dict(raw, path)


def save_manifest_atomic(path: Path, manifest: Manifest) -> None:
    payload = json.dumps(_manifest_to_dict(manifest), indent=2, sort_keys=True) + "\n"
    tmp = path.with_suffix(path.suffix + ".tmp")
    try:
        tmp.write_text(payload, encoding="utf-8", newline="\n")
        tmp.replace(path)
    except OSError as exc:
        raise UserError(f"Failed to write manifest: {path} ({exc})") from exc


def _manifest_to_dict(manifest: Manifest) -> dict[str, object]:
    return {
        "schema_version": manifest.schema_version,
        "inbox_root": manifest.inbox_root,
        "created_at": manifest.created_at,
        "updated_at": manifest.updated_at,
        "files": manifest.files,
    }


def _manifest_from_dict(raw: object, path: Path) -> Manifest:
    if not isinstance(raw, dict):
        raise UserError(f"Manifest root must be an object: {path}")
    schema_version = raw.get("schema_version")
    if schema_version != MANIFEST_SCHEMA_VERSION:
        raise UserError(
            f"Unsupported manifest schema_version={schema_version!r} (expected {MANIFEST_SCHEMA_VERSION}): {path}"
        )
    files = raw.get("files")
    if not isinstance(files, dict):
        raise UserError(f"Manifest 'files' must be an object: {path}")
    return Manifest(
        schema_version=MANIFEST_SCHEMA_VERSION,
        inbox_root=str(raw.get("inbox_root") or ""),
        created_at=str(raw.get("created_at") or ""),
        updated_at=str(raw.get("updated_at") or ""),
        files=files,
    )
