"""Run report for the migration transforms (05 step 2 logging rules).

Every step logs (source count, written, skipped with reasons); the
reconciliation accounts for every legacy table row as
migrated / dropped-allowlisted / merged / skipped-with-reason, and the
run FAILS on any unexplained delta. Per-currency row counts ride along
until rian's final currency confirmation (07 #21).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any


@dataclass
class StepReport:
    name: str
    source_count: int = 0
    written: int = 0
    skipped: dict[str, int] = field(default_factory=dict)  # reason -> count
    warnings: list[str] = field(default_factory=list)
    details: dict[str, Any] = field(default_factory=dict)

    def skip(self, reason: str, count: int = 1) -> None:
        self.skipped[reason] = self.skipped.get(reason, 0) + count

    def warn(self, message: str) -> None:
        self.warnings.append(message)

    def as_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "source_count": self.source_count,
            "written": self.written,
            "skipped": self.skipped,
            "warnings": self.warnings,
            "details": self.details,
        }


@dataclass
class RunReport:
    started_at: str = field(
        default_factory=lambda: datetime.now(UTC).isoformat(timespec="seconds")
    )
    steps: list[StepReport] = field(default_factory=list)
    # legacy table -> {source, dispositions: {name: count}, delta}
    reconciliation: dict[str, dict[str, Any]] = field(default_factory=dict)
    # column-level dispositions verification result
    column_check: dict[str, Any] = field(default_factory=dict)
    # 07 #21: per-currency row counts, per stamped column
    currency_counts: dict[str, dict[str, int]] = field(default_factory=dict)
    straggler_count: int | None = None
    straggler_entry_ids: list[str] = field(default_factory=list)
    warnings: list[str] = field(default_factory=list)
    errors: list[str] = field(default_factory=list)
    finished_at: str | None = None

    def step(self, name: str) -> StepReport:
        step = StepReport(name=name)
        self.steps.append(step)
        return step

    def reconcile(
        self, table: str, source: int, dispositions: dict[str, int]
    ) -> None:
        delta = source - sum(dispositions.values())
        self.reconciliation[table] = {
            "source": source,
            "dispositions": dispositions,
            "delta": delta,
        }
        if delta != 0:
            self.errors.append(
                f"reconciliation: legacy.{table} has {delta} unaccounted row(s) "
                f"(source {source}, dispositions {dispositions})"
            )

    @property
    def ok(self) -> bool:
        return not self.errors

    def finish(self) -> None:
        self.finished_at = datetime.now(UTC).isoformat(timespec="seconds")

    def as_dict(self) -> dict[str, Any]:
        return {
            "started_at": self.started_at,
            "finished_at": self.finished_at,
            "ok": self.ok,
            "steps": [s.as_dict() for s in self.steps],
            "reconciliation": self.reconciliation,
            "column_check": self.column_check,
            "currency_counts": self.currency_counts,
            "straggler_count": self.straggler_count,
            "straggler_entry_ids": self.straggler_entry_ids,
            "warnings": self.warnings,
            "errors": self.errors,
        }

    def render(self) -> str:
        lines = [f"migration run (started {self.started_at})"]
        for step in self.steps:
            skipped = (
                "; skipped " + ", ".join(f"{n}={c}" for n, c in step.skipped.items())
                if step.skipped
                else ""
            )
            lines.append(
                f"  [{step.name}] source={step.source_count}"
                f" written={step.written}{skipped}"
            )
            lines.extend(f"    WARNING: {w}" for w in step.warnings)
        lines.append("  reconciliation:")
        for table, entry in sorted(self.reconciliation.items()):
            disp = ", ".join(f"{k}={v}" for k, v in entry["dispositions"].items())
            flag = "" if entry["delta"] == 0 else f"  !! delta={entry['delta']}"
            lines.append(f"    legacy.{table}: {entry['source']} -> {disp}{flag}")
        if self.currency_counts:
            lines.append("  per-currency stamped-row counts (07 #21):")
            for column, counts in sorted(self.currency_counts.items()):
                per = ", ".join(f"{c}={n}" for c, n in sorted(counts.items()))
                lines.append(f"    {column}: {per or '(none)'}")
        lines.append(
            f"  transferred-straggler check (judgment #22): {self.straggler_count}"
            " row(s) transferred-but-not-invoiced (expected 0)"
        )
        if self.straggler_entry_ids:
            lines.append(f"    straggler entry ids: {self.straggler_entry_ids}")
        lines.extend(f"  WARNING: {w}" for w in self.warnings)
        lines.extend(f"  ERROR: {e}" for e in self.errors)
        lines.append(f"RESULT: {'OK' if self.ok else 'FAILED'}")
        return "\n".join(lines)
