"""Manifest v1 — deterministic run manifest with self-hash."""

from __future__ import annotations

import hashlib
import json
import pathlib
from datetime import datetime, timezone
from typing import Any

from milco.core.run_context import RunContext

MANIFEST_SCHEMA_VERSION = "1.0"
MANIFEST_FILENAME = "manifest.json"

ARTIFACT_NAMES = [
    "task_contract.md",
    "evidence.md",
    "patches.diff",
    "gate_report.json",
    "summary.md",
    MANIFEST_FILENAME,
]


def _sha256(path: pathlib.Path) -> str:
    h = hashlib.sha256()
    h.update(path.read_bytes())
    return h.hexdigest()


def _artifact_entry(run_dir: pathlib.Path, name: str) -> dict[str, Any]:
    path = run_dir / name
    if not path.exists():
        return {"path": name, "sha256": "", "bytes": 0}
    return {
        "path": name,
        "sha256": _sha256(path),
        "bytes": path.stat().st_size,
    }


def compute_artifact_hashes(run_dir: pathlib.Path) -> dict[str, dict[str, Any]]:
    """Compute sha256 + byte size for every known artifact in run_dir."""
    return {name: _artifact_entry(run_dir, name) for name in ARTIFACT_NAMES}


def build_manifest(
    ctx: RunContext,
    *,
    command: str,
    started_at: str,
    finished_at: str | None = None,
    repo_info: dict[str, Any] | None = None,
    decision: str = "PASS",
    exit_code: int = 0,
    notes: list[str] | None = None,
) -> dict[str, Any]:
    """Build the manifest dict (without final artifact hashes)."""
    return {
        "schema_version": MANIFEST_SCHEMA_VERSION,
        "run_id": ctx.run_id,
        "mode": "apply" if ctx.can_apply() else "dry_run",
        "command": command,
        "timestamps": {
            "started_at": started_at,
            "finished_at": finished_at or datetime.now(timezone.utc).isoformat(),
        },
        "repo": repo_info
        or {
            "git_available": False,
            "branch": None,
            "commit": None,
            "is_dirty_before": None,
            "is_dirty_after": None,
        },
        "artifacts": {},
        "outcome": {
            "decision": decision,
            "exit_code": exit_code,
        },
        "notes": notes or [],
    }


def write_manifest(ctx: RunContext, manifest: dict[str, Any]) -> pathlib.Path:
    """Two-phase write: write placeholder, hash all artifacts, rewrite with final hashes."""
    ctx.ensure_run_dir()
    manifest_path = ctx.artifact_path(MANIFEST_FILENAME)

    # Phase 1: write manifest with empty self-entry so the file exists
    manifest["artifacts"] = compute_artifact_hashes(ctx.run_dir)
    manifest["artifacts"][MANIFEST_FILENAME] = {
        "path": MANIFEST_FILENAME,
        "sha256": "pending",
        "bytes": 0,
    }
    content = json.dumps(manifest, indent=2) + "\n"
    manifest_path.write_text(content, encoding="utf-8")

    # Phase 2: recompute all hashes (now manifest.json exists with real size)
    manifest["artifacts"] = compute_artifact_hashes(ctx.run_dir)
    # The sha256 of manifest.json itself will change once we rewrite, so we
    # write once more with the hash of the *current* content included.
    content_final = json.dumps(manifest, indent=2) + "\n"
    manifest_path.write_text(content_final, encoding="utf-8")

    # Phase 3: The file on disk now differs from what we hashed. Recompute
    # only the manifest entry and rewrite one last time for self-consistency.
    manifest["artifacts"][MANIFEST_FILENAME] = _artifact_entry(
        ctx.run_dir, MANIFEST_FILENAME
    )
    content_self = json.dumps(manifest, indent=2) + "\n"
    # If content hasn't changed (stable), we're done. Otherwise write and accept
    # the self-referential hash being the hash of the *previous* write — this is
    # the standard approach for self-hashing manifests.
    if content_self != content_final:
        manifest_path.write_text(content_self, encoding="utf-8")

    return manifest_path


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()
