"""역할별 실행 입력을 최종 리포트 조각으로 바꾸는 순수 투영."""
from __future__ import annotations

import json
from copy import deepcopy
from typing import Any, Mapping, Sequence

from .design_surfaces import DesignSurfaceTrigger, detect_design_surfaces
from .report_contract import execution_roles_from_manifest
from .usage_cells import duration_ms_from_bounds


class ReportProjectionError(ValueError):
    """소유자 입력을 정본 조각으로 투영할 수 없다."""


_AGENT_LABELS = {
    "claude": "Claude Code",
    "claude-code": "Claude Code",
    "codex": "Codex",
    "antigravity": "Antigravity",
    "grok": "Grok",
    "kimi": "Kimi",
}
_STATUS = {
    "done": "completed",
    "passed": "completed",
    "failed": "error",
    "blocked": "error",
    "pending": "not-run",
    "prepared": "not-run",
}


def _text(value: object, fallback: str) -> str:
    text = str(value or "").strip()
    return text or fallback


def _status(value: object) -> str:
    text = str(value or "not-run").strip().lower()
    return _STATUS.get(text, text if text in {
        "completed", "error", "timeout", "not-run", "synthesis-only"
    } else "not-run")


def _agent_label(row: Mapping[str, Any]) -> str:
    value = _text(row.get("agent") or row.get("provider"), "codex")
    return _AGENT_LABELS.get(value.lower(), value)


def _execution_row(
    row: Mapping[str, Any],
    *,
    lead: bool = False,
    usage: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
    role = _text(row.get("role"), "Okstra lead" if lead else "Worker")
    source = usage if usage is not None else (
        row.get("usage") if isinstance(row.get("usage"), Mapping) else {}
    )
    status = _status(row.get("status"))
    if lead and status == "not-run" and _usage_ran(source):
        status = "completed"
    result = {
        "agent": _agent_label(row),
        "role": role,
        "model": _text(row.get("model") or row.get("modelExecutionValue"), "unknown"),
        "status": status,
        "summary": _text(
            row.get("summary") or row.get("reason"),
            "Run coordination recorded by team state." if lead
            else "Worker execution recorded by team state.",
        ),
    }
    _populate_usage(result, source)
    if not result.get("durationMs"):
        duration = _row_duration_ms(row, source)
        if duration is not None:
            result["durationMs"] = duration
    return result


def _usage_ran(usage: Mapping[str, Any]) -> bool:
    if usage.get("source") == "unavailable":
        return False
    tokens = usage.get("totalTokens") or usage.get("cliTotalTokens") or 0
    duration = usage.get("durationMs") or 0
    return bool(tokens or duration)


def _row_duration_ms(row: Mapping[str, Any], usage: Mapping[str, Any]) -> int | None:
    value = usage.get("durationMs") if usage.get("source") != "unavailable" else None
    if isinstance(value, (int, float)) and not isinstance(value, bool) and value > 0:
        return int(value)
    duration = duration_ms_from_bounds(row.get("startedAt"), row.get("endedAt"))
    if duration is not None:
        return duration
    if usage.get("source") == "unavailable":
        return None
    return duration_ms_from_bounds(usage.get("startedAt"), usage.get("endedAt"))


def _populate_usage(row: dict[str, Any], usage: object) -> None:
    source = usage if isinstance(usage, Mapping) else {}
    mapping = {
        "totalTokens": "totalTokens",
        "cacheReadTokens": "cacheReadTokens",
        "billableEquivalentTokens": "billableTokens",
        "estimatedCostUsd": "costUsd",
        "cliTotalTokens": "cliTotalTokens",
        "cliEstimatedCostUsd": "cliCostUsd",
        "durationMs": "durationMs",
    }
    for source_key, target_key in mapping.items():
        if source_key in source and source.get("source") != "unavailable":
            row[target_key] = source[source_key]


def project_execution(
    manifest: Mapping[str, Any], team_state: Mapping[str, Any],
) -> dict[str, Any]:
    """실행 역할과 상태를 매니페스트·팀 상태에서만 만든다."""
    lead = team_state.get("lead")
    lead_row = lead if isinstance(lead, Mapping) else {
        "agent": manifest.get("leadRuntime") or manifest.get("leadProvider"),
        "model": manifest.get("leadModel") or "unknown",
        "status": "completed",
        "role": "Okstra lead",
    }
    lead_usage = team_state.get("leadUsage")
    workers = team_state.get("workers")
    worker_rows = workers if isinstance(workers, list) else []
    result: dict[str, Any] = {
        "executionStatus": [
            _execution_row(
                lead_row,
                lead=True,
                usage=lead_usage if isinstance(lead_usage, Mapping) else None,
            ),
            *[
                _execution_row(row)
                for row in worker_rows
                if isinstance(row, Mapping)
            ],
        ]
    }
    roles = execution_roles_from_manifest(dict(manifest))
    if roles:
        result.update(executionIdentityVersion=2, executionRoles=roles)
    return result


def _usage_row(summary: Mapping[str, Any], prefix: str, cost: object) -> dict[str, Any]:
    return {
        "totalTokens": summary.get(f"{prefix}TotalTokens"),
        "cacheReadTokens": summary.get(f"{prefix}CacheReadTokens"),
        "billableTokens": summary.get(f"{prefix}BillableEquivalentTokens"),
        "costUsd": cost,
    }


def _worker_details(team_state: Mapping[str, Any]) -> list[dict[str, Any]]:
    result = []
    for worker in team_state.get("workers") or []:
        if not isinstance(worker, Mapping):
            continue
        usage = worker.get("usage") if isinstance(worker.get("usage"), Mapping) else {}
        row = {"label": _text(worker.get("role") or worker.get("workerId"), "Worker")}
        _populate_usage(row, usage)
        for key in ("totalTokens", "cacheReadTokens", "billableTokens", "costUsd", "cliTotalTokens", "cliCostUsd"):
            row.setdefault(key, None)
        result.append(row)
    return result


def project_token_usage(team_state: Mapping[str, Any]) -> dict[str, Any]:
    """팀 상태의 사용량 합계를 리포트 표 구조로 반환한다."""
    summary = team_state.get("usageSummary")
    summary = summary if isinstance(summary, Mapping) else {}
    costs = summary.get("estimatedCostUsd")
    costs = costs if isinstance(costs, Mapping) else {}
    lead_cost = costs.get("lead")
    worker_cost = costs.get("claudeWorkers")
    return {
        "lead": _usage_row(summary, "lead", lead_cost),
        "worker": _usage_row(summary, "worker", worker_cost),
        "grand": _usage_row(
            summary,
            "grand",
            None if lead_cost is None or worker_cost is None else lead_cost + worker_cost,
        ),
        "workerDetails": _worker_details(team_state),
        "cli": {"costUsd": costs.get("cliWorkers")},
    }


def _trigger_rows(trigger: DesignSurfaceTrigger) -> list[dict[str, Any]]:
    return [
        {"step": item.step, "field": item.field, "match": item.match}
        for item in trigger.evidence
    ]


def project_design(
    planning: Mapping[str, Any], detector_snapshot: Mapping[str, Any],
) -> dict[str, Any]:
    """탐지 결과가 실제 계획 표면과 일치할 때만 설계 블록을 반환한다."""
    triggers = detect_design_surfaces(planning)
    expected = {(row.stage, row.kind): _trigger_rows(row) for row in triggers}
    coverage = detector_snapshot.get("stageCoverage")
    coverage = coverage if isinstance(coverage, list) else []
    actual: dict[tuple[int, str], list[dict[str, Any]]] = {}
    for stage in coverage:
        if not isinstance(stage, Mapping):
            raise ReportProjectionError("owner=design-surface-detector invalid stageCoverage row")
        for row in stage.get("rows") or []:
            if isinstance(row, Mapping):
                actual[(int(stage.get("stage") or 0), str(row.get("kind") or ""))] = list(row.get("triggerEvidence") or [])
    if expected != actual:
        raise ReportProjectionError("owner=design-surface-detector trigger coverage mismatch")
    preparation = detector_snapshot.get("designPreparation")
    if not isinstance(preparation, Mapping):
        raise ReportProjectionError("owner=design-surface-detector missing designPreparation")
    return {
        "designPreparation": deepcopy(dict(preparation)),
        "stageCoverage": deepcopy(coverage),
    }


def _round_history(state: Mapping[str, Any]) -> dict[str, Any]:
    if not (state.get("config") or {}).get("enabled", True):
        return {"disabled": True}
    rows = []
    for row in state.get("roundHistory") or []:
        if not isinstance(row, Mapping):
            continue
        rows.append({
            "round": row.get("round", 0),
            "inputQueueSize": row.get("inputQueueSize", 0),
            "resolvedCount": row.get("resolvedCount", 0),
            "carriedForwardCount": row.get("carriedForwardCount", 0),
            "dispatches": json.dumps(row.get("dispatches") or [], ensure_ascii=False),
            "skippedWorkers": json.dumps(row.get("skippedWorkers") or [], ensure_ascii=False),
        })
    return {"rounds": rows, "round2SkippedReason": state.get("round2SkippedReason", "not-skipped")}


def _source_items(finding: Mapping[str, Any]) -> list[str]:
    finding_id = _text(finding.get("findingId"), "unknown")
    workers = finding.get("consensusWorkers") or [finding.get("originWorker")]
    return [f"{_text(worker, 'worker').removesuffix('-worker')}:{finding_id}" for worker in workers]


def project_convergence(state: Mapping[str, Any]) -> dict[str, Any]:
    """수렴 상태에서 독자용 합의·이견과 공개 근거만 승격한다."""
    consensus = []
    differences = []
    promoted = []
    for finding in state.get("findings") or []:
        if not isinstance(finding, Mapping):
            continue
        evidence = _text(finding.get("originEvidence"), "")
        ticket_ids = finding.get("ticketIds") or ["unknown"]
        ticket_id = _text(ticket_ids[0] if ticket_ids else "unknown", "unknown")
        if finding.get("classification") in {"full-consensus", "partial-consensus"}:
            row = {
                "id": f"C-{len(consensus) + 1:03d}",
                "ticketId": ticket_id,
                "statement": _text(finding.get("summary"), "Converged finding"),
                "sourceItems": _source_items(finding),
                "evidence": evidence or "convergence state",
            }
            consensus.append(row)
            if evidence and ("/" in evidence or ":" in evidence):
                promoted.append({
                    "findingId": finding.get("findingId"),
                    "ticketId": ticket_id,
                    "evidence": evidence,
                    "sourceItems": row["sourceItems"],
                })
        else:
            positions = []
            for worker in [finding.get("originWorker"), *(finding.get("dissentingWorkers") or [])]:
                if worker:
                    positions.append({
                        "worker": str(worker),
                        "itemId": _text(finding.get("findingId"), ""),
                        "position": "origin" if worker == finding.get("originWorker") else "dissent",
                    })
            differences.append({
                "id": f"D-{len(differences) + 1:03d}",
                "ticketId": ticket_id,
                "disagreement": _text(finding.get("summary"), "Unresolved difference"),
                "workersPosition": positions or [{"worker": "unknown", "itemId": "", "position": "unresolved"}],
                "evidence": evidence or "convergence state",
            })
    result = {
        "crossVerification": {
            "roundHistory": _round_history(state),
            "consensus": consensus,
            "differences": differences,
        },
        "promotedEvidence": promoted,
    }
    return result
