"""Batch executor — run an operation across all projects in a manifest.

Per-project isolation: one failure does not abort others. Results are
collected and a summary JSON is written to *output_dir*.
"""

from __future__ import annotations

import json
import traceback
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    from complyform.core.batch.manifest import BatchManifest, BatchProject


@dataclass
class BatchResult:
    """Aggregated outcome of a batch operation."""

    total: int = 0
    succeeded: int = 0
    failed: int = 0
    skipped: int = 0
    results: list[dict[str, Any]] = field(default_factory=list)


def run_batch_operation(
    manifest: BatchManifest,
    operation: str,
    per_project_fn: Callable[[BatchProject], dict[str, Any]],
    output_dir: Path,
) -> BatchResult:
    """Execute *operation* for each project in *manifest*.

    ``per_project_fn`` receives a single :class:`BatchProject` and must
    return a dict with at least
    ``{"status": "success"|"failed"|"skipped"}``.
    Exceptions are caught per-project so that one failure does not
    abort the remaining projects.

    A ``batch-{operation}-summary.json`` file is written to
    *output_dir* after all projects have been processed.
    """
    result = BatchResult(total=len(manifest.projects))

    for project in manifest.projects:
        entry: dict[str, Any] = {
            "label": project.label,
            "cloud": project.cloud,
        }
        try:
            outcome = per_project_fn(project)
            entry.update(outcome)
            status = outcome.get("status", "success")
        except Exception as exc:
            status = "failed"
            entry["status"] = "failed"
            entry["error"] = str(exc)
            entry["traceback"] = traceback.format_exc()

        if status == "success":
            result.succeeded += 1
        elif status == "skipped":
            result.skipped += 1
        else:
            result.failed += 1

        result.results.append(entry)

    # Write summary
    output_dir.mkdir(parents=True, exist_ok=True)
    summary_path = output_dir / f"batch-{operation}-summary.json"
    summary = {
        "schema_version": 1,
        "operation": operation,
        "timestamp": datetime.now(UTC).isoformat(),
        "total": result.total,
        "succeeded": result.succeeded,
        "failed": result.failed,
        "skipped": result.skipped,
        "results": result.results,
    }
    summary_path.write_text(json.dumps(summary, indent=2), encoding="utf-8")

    return result
