"""reconciliation/normalization."""
from __future__ import annotations

from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from .jsonl import append_jsonl, rewrite_jsonl, rotate_recent_if_needed
from .locks import central_lock
from .paths import resolve_under_root
from .project_meta import upsert_project_meta
from .run_index_row import read_run_index, slim_run_row
from .json_boundary import JsonBoundaryError, load_owned_object


DEFAULT_ABORT_AFTER_SECONDS = 12 * 3600  # 12시간 무진척이면 aborted 로 간주

# recent.jsonl 에서 디스크 manifest 로 재조정 대상이 되는 비종결 상태.
# completed/failed/aborted 는 이미 종결이라 건드리지 않는다.
NON_TERMINAL_RECENT_STATUSES = {"prepared", "running", "in-progress"}


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%SZ")


def _parse_iso(ts: str) -> Optional[datetime]:
    try:
        return datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ")
    except (ValueError, TypeError):
        return None


def normalize_central_status(manifest_status: str, validation_status: str) -> str:
    """Map okstra manifest/validation statuses to control-center statuses."""
    failed_values = {
        "failed",
        "contract-violation",
        "contract-violated",
        "validation-failed",
    }
    manifest_status = str(manifest_status or "")
    validation_status = str(validation_status or "")
    if manifest_status in failed_values or validation_status in failed_values:
        return "failed"
    if manifest_status in {"aborted", "prepared", "running", "in-progress"}:
        return manifest_status
    if manifest_status in {"error", "errored"}:
        return "failed"
    return "completed"


def normalize_reconciled_report_status(validation_status: str) -> str:
    """Final report existence means terminal success unless validation failed."""
    return normalize_central_status("completed", validation_status)


def _read_run_manifest_validation(project_root: Path, run_dir_rel: str,
                                  task_type: str, run_seq: int) -> str:
    """run-manifest 의 validation.status 를 읽는다. 못 읽으면 'not-run'.
    완료/실패 여부를 정확히 반영하기 위함 — 단순히 final-report 존재만으로
    'passed' 로 판정하면 contract-violation/failed 를 잘못 기록한다.
    """
    run_dir = resolve_under_root(project_root, run_dir_rel)
    if run_dir is None:
        return "not-run"
    suffix = f"-{task_type}-{run_seq:03d}"
    manifest = run_dir / "manifests" / f"run-manifest{suffix}.json"
    if not manifest.is_file():
        return "not-run"
    try:
        data = load_owned_object(manifest, artifact="run manifest")
    except JsonBoundaryError:
        return "not-run"
    validation = data.get("validation")
    if isinstance(validation, dict):
        return str(validation.get("status") or "not-run")
    if isinstance(validation, str):
        return validation
    return "not-run"


def _sync_project_index(home: Path, project_id: str, updated_rows: list) -> None:
    """updated_rows 의 (status/finishedAt/validation) 를 프로젝트 index.jsonl 의
    동일 runId 행에 반영한다. reconcile_active(promote) 와 reconcile_recent
    (in-place refresh) 가 공유하는 단일 인덱스 갱신 경로."""
    proj_index = home / "projects" / project_id / "index.jsonl"
    if not proj_index.is_file():
        return
    by_id = {r["runId"]: r for r in updated_rows}
    existing = read_run_index(proj_index)
    for er in existing:
        src = by_id.get(er.get("runId"))
        if src:
            er.update({k: src[k] for k in ("status", "finishedAt", "validation")})
    rewrite_jsonl(proj_index, [slim_run_row(row) for row in existing])


def reconcile_active(home: Path, *,
                     abort_after_seconds: int = DEFAULT_ABORT_AFTER_SECONDS,
                     project: Optional[str] = None) -> dict:
    """active.jsonl 의 각 행을 검사해 종결 추론. 종결된 행은 recent 로 이동.
    project 가 주어지면 해당 projectId 행만 추론 대상으로 삼고, 다른 프로젝트
    행은 그대로 보존한다(스코프 reconcile).
    중앙 락 안에서 read-modify-write 하므로 동시에 일어나는 record_start 와
    같은 active 행 append 가 누락되지 않는다.
    """
    with central_lock(home):
        return _reconcile_active_locked(home,
                                        abort_after_seconds=abort_after_seconds,
                                        project=project)


def _publish_promoted(
    home: Path, active: Path, survivors: list[dict], promoted: list[dict]
) -> None:
    rewrite_jsonl(active, [slim_run_row(row) for row in survivors])
    for row in promoted:
        append_jsonl(home / "recent.jsonl", slim_run_row(row))
        _sync_project_index(home, row["projectId"], [row])
        upsert_project_meta(
            home, row["projectId"], project_root=row["projectRoot"],
            when=row["finishedAt"], finished=True,
        )
    rotate_recent_if_needed(home)


def _reconcile_active_locked(home: Path, *,
                             abort_after_seconds: int,
                             project: Optional[str] = None) -> dict:
    active = home / "active.jsonl"
    summary = {"completed": 0, "failed": 0, "aborted": 0, "running": 0}
    rows = read_run_index(active)
    if not rows:
        return summary
    survivors = []
    promoted = []
    now = datetime.now(timezone.utc).replace(tzinfo=None)
    for row in rows:
        # project 필터: 스코프 외 행은 추론하지 않고 active 에 그대로 남긴다.
        # 상태를 추론하지 않았으므로 running 으로 집계하지 않는다 — summary 는
        # 이번 패스가 실제로 추론/이동한 행만 반영한다.
        if project and row.get("projectId") != project:
            survivors.append(row)
            continue
        project_root = Path(row.get("projectRoot", ""))
        from .final_report_paths import index_report_record_rel

        final_rel = index_report_record_rel(row)
        final_abs = project_root / final_rel if final_rel else None
        if final_abs and final_abs.is_file():
            validation_status = _read_run_manifest_validation(
                project_root, row.get("runDirRel", ""),
                row.get("taskType", ""), int(row.get("runSeq", 0) or 0))
            # validate-okstra-run.py 가 아직 실행되지 않은 시점에는 final-report
            # 만 존재하고 validation 은 'not-run' 으로 보고된다. 이때 promote 하면
            # normalize_reconciled_report_status 가 'completed' 로 매핑하여 행이
            # recent 로 옮겨가고, 이후 validator 가 failed/contract-violated 를
            # 기록해도 reconcile 이 다시 보지 않아 영구히 잘못된 상태로 남는다.
            # validation 이 terminal 신호('not-run' 이외) 일 때만 promote 하고,
            # 그 외에는 active 에 유지해 후속 reconcile / abort timeout 에 맡긴다.
            if validation_status and validation_status != "not-run":
                row["status"] = normalize_reconciled_report_status(validation_status)
                row["finishedAt"] = _now_iso()
                row["validation"] = validation_status
                promoted.append(row)
                if row["status"] == "completed":
                    summary["completed"] += 1
                else:
                    summary["failed"] += 1
                continue
        started = _parse_iso(row.get("startedAt", ""))
        if started and (now - started).total_seconds() > abort_after_seconds:
            row["status"] = "aborted"
            row["finishedAt"] = _now_iso()
            promoted.append(row)
            summary["aborted"] += 1
            continue
        survivors.append(row)
        summary["running"] += 1
    if not promoted:
        return summary
    _publish_promoted(home, active, survivors, promoted)
    return summary


def reconcile_recent(home: Path, *, project: Optional[str] = None) -> dict:
    """recent.jsonl 의 비종결(prepared 등) 행을 디스크 run-manifest 로 in-place 갱신.

    `--render-only` 로 prepared 로 박힌 행은 record_start 가 terminal 로 취급해
    recent 에 직접 쓰므로 reconcile_active(active.jsonl 전용) 의 promote 경로를
    타지 않고, backfill 도 이미 인덱싱된 runId 라 skip 한다. 실행이 끝나
    manifest 가 terminal(passed/failed) 로 바뀌어도 recent 의 status 가 prepared
    로 영영 고착되는 갭을 메운다. 중앙 락 안에서 read-modify-write 한다."""
    with central_lock(home):
        return _reconcile_recent_locked(home, project=project)


def _reconcile_recent_locked(home: Path, *, project: Optional[str] = None) -> dict:
    recent = home / "recent.jsonl"
    summary = {"completed": 0, "failed": 0, "unchanged": 0}
    rows = read_run_index(recent)
    updated_by_project: dict = {}
    for row in rows:
        if (project and row.get("projectId") != project) or \
                row.get("status") not in NON_TERMINAL_RECENT_STATUSES:
            summary["unchanged"] += 1
            continue
        validation_status = _read_run_manifest_validation(
            Path(row.get("projectRoot", "")), row.get("runDirRel", ""),
            row.get("taskType", ""), int(row.get("runSeq", 0) or 0))
        if not validation_status or validation_status == "not-run":
            summary["unchanged"] += 1
            continue
        new_status = normalize_reconciled_report_status(validation_status)
        if new_status in NON_TERMINAL_RECENT_STATUSES:
            summary["unchanged"] += 1
            continue
        row["status"] = new_status
        row["finishedAt"] = row.get("finishedAt") or _now_iso()
        row["validation"] = validation_status
        updated_by_project.setdefault(row["projectId"], []).append(row)
        summary["completed" if new_status == "completed" else "failed"] += 1
    if not updated_by_project:
        return summary
    rewrite_jsonl(recent, [slim_run_row(row) for row in rows])
    for pid, urows in updated_by_project.items():
        _sync_project_index(home, pid, urows)
    return summary
