"""list/format/show."""
from __future__ import annotations

import re as _re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import List, Optional

from .paths import resolve_under_root
from .json_boundary import JsonBoundaryError, load_owned_object
from .reconcile import _parse_iso
from .run_index_row import read_run_index


def list_projects(home: Path, *, only_active: bool = False,
                  sort_by: str = "recent") -> List[dict]:
    """projects/<id>/meta.json 을 모두 읽어 정렬된 목록을 반환."""
    base = home / "projects"
    if not base.is_dir():
        return []
    items: List[dict] = []
    for child in base.iterdir():
        meta_file = child / "meta.json"
        if not meta_file.is_file():
            continue
        try:
            meta = load_owned_object(meta_file, artifact="project metadata")
        except JsonBoundaryError:
            # 동시 쓰기/이전 크래시로 깨진 meta.json 하나가 전체 listing 을
            # 죽이지 않도록 해당 프로젝트만 건너뛴다(다른 JSON 리더와 동일 정책).
            continue
        # projectRoot 가 누락/빈 문자열이면 Path("").is_dir() 이 cwd 를 가리켜
        # True 가 되므로(missing 으로 잡히지 않음), 빈 값은 명시적으로 missing
        # 처리한다.
        proj_root = meta.get("projectRoot") or ""
        meta["missing"] = (not proj_root) or (not Path(proj_root).is_dir())
        if only_active and int(meta.get("activeCount", 0)) <= 0:
            continue
        items.append(meta)
    if sort_by == "name":
        items.sort(key=lambda m: m.get("projectId", ""))
    elif sort_by == "count":
        items.sort(key=lambda m: int(m.get("runCount", 0)), reverse=True)
    else:
        items.sort(key=lambda m: m.get("lastRunAt", ""), reverse=True)
    return items


def format_projects_table(items: List[dict]) -> str:
    """projects 테이블 텍스트 포맷팅."""
    rows = [("PROJECT-ID", "RUNS", "ACTIVE", "LAST-RUN", "ROOT")]
    for it in items:
        root = it.get("projectRoot", "") + (" (MISSING)" if it.get("missing") else "")
        rows.append((it.get("projectId", ""), str(it.get("runCount", 0)),
                     str(it.get("activeCount", 0)), it.get("lastRunAt", ""),
                     root))
    widths = [max(len(r[i]) for r in rows) for i in range(5)]
    lines = []
    for r in rows:
        lines.append("  ".join(c.ljust(widths[i]) for i, c in enumerate(r)))
    return "\n".join(lines) + "\n"


def _parse_since(value: str) -> Optional[datetime]:
    """--since 파싱. 빈 문자열은 None(필터 없음). 형식 위반은 ValueError 로
    fail-fast 한다. 이전엔 잘못된 값(`7days`)이 silently None 으로 강등되어
    필터가 무력화됐고, rerun --filter --since <typo> 가 max-spawn 까지 모든
    historical run 을 spawn 하던 회귀가 있었다.
    """
    if not value:
        return None
    m = _re.match(r"^(\d+)([dhm])$", value)
    if m:
        n, unit = int(m.group(1)), m.group(2)
        delta = {"d": timedelta(days=n), "h": timedelta(hours=n),
                 "m": timedelta(minutes=n)}[unit]
        return datetime.now(timezone.utc).replace(tzinfo=None) - delta
    try:
        return datetime.strptime(value, "%Y-%m-%d")
    except ValueError as exc:
        raise ValueError(
            f"invalid --since {value!r}: expected <N>[dhm] or YYYY-MM-DD"
        ) from exc


def _parse_started_at(ts: str) -> Optional[datetime]:
    """--since 비교용 startedAt 파싱. 기준값은 naive UTC 이므로 결과도 naive UTC.

    _parse_iso 는 `...Z` 형식만 받지만 backfill 이 manifest createdAt 을 그대로
    startedAt 으로 옮기면 offset 형식(`+00:00`)이 섞일 수 있다. 그 경우 None 으로
    빠지면 --since 필터가 fail-open 으로 모든 row 를 통과시키므로, fromisoformat
    fallback 으로 offset 형식도 받아 naive UTC 로 정규화한다.
    """
    parsed = _parse_iso(ts)
    if parsed is not None:
        return parsed
    try:
        dt = datetime.fromisoformat(ts)
    except (ValueError, TypeError):
        return None
    if dt.tzinfo is not None:
        dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
    return dt


def list_runs(home: Path, *, project: str = "all", task_group: str = "all",
              status: str = "all", since: str = "", limit: int = 0,
              include_archive: bool = False) -> List[dict]:
    """active + recent (옵션 archive) 를 합쳐 필터/정렬 후 반환."""
    rows = read_run_index(home / "active.jsonl") + read_run_index(home / "recent.jsonl")
    if include_archive:
        archive_dir = home / "archive"
        if archive_dir.is_dir():
            for f in sorted(archive_dir.glob("*/*.jsonl")):
                rows.extend(read_run_index(f))
    threshold = _parse_since(since)
    out: List[dict] = []
    for r in rows:
        if project != "all" and r.get("projectId") != project:
            continue
        if task_group != "all" and r.get("taskGroup") != task_group:
            continue
        if status != "all" and r.get("status") != status:
            continue
        if threshold:
            started = _parse_started_at(r.get("startedAt", ""))
            if started and started < threshold:
                continue
        out.append(r)
    out.sort(key=lambda r: r.get("startedAt", ""), reverse=True)
    if limit > 0:
        out = out[:limit]
    return out


def format_runs_table(rows: List[dict]) -> str:
    header = ("RUN-ID", "STATUS", "STARTED", "TASK-TYPE")
    body = [(r.get("runId", ""), r.get("status", ""),
             r.get("startedAt", ""), r.get("taskType", "")) for r in rows]
    table = [header] + body
    widths = [max(len(t[i]) for t in table) for i in range(4)]
    lines = []
    for t in table:
        lines.append("  ".join(c.ljust(widths[i]) for i, c in enumerate(t)))
    return "\n".join(lines) + "\n"


def find_row_by_run_id(home: Path, run_id: str) -> Optional[dict]:
    """active+recent+archive 를 모두 뒤져 runId 가 일치하는 최신 row 반환."""
    rows = list_runs(home, include_archive=True)
    for r in rows:
        if r.get("runId") == run_id:
            return r
    return None


def format_show(row: dict) -> str:
    inv = row.get("invocationFile", "")
    return (
        f"runId           : {row.get('runId')}\n"
        f"projectId       : {row.get('projectId')}\n"
        f"projectRoot     : {row.get('projectRoot')}\n"
        f"taskKey         : {row.get('taskGroup')}/{row.get('taskId')}\n"
        f"taskType        : {row.get('taskType')}\n"
        f"status          : {row.get('status')}\n"
        f"startedAt       : {row.get('startedAt')}\n"
        f"finishedAt      : {row.get('finishedAt')}\n"
        f"workers         : {','.join(row.get('workers', []))}\n"
        f"leadModel       : {row.get('leadModel')}\n"
        f"validation      : {row.get('validation')}\n"
        f"runDirRel       : {row.get('runDirRel')}\n"
        f"finalReportRecordRel  : {row.get('finalReportRecordRel') or row.get('finalReportRel')}\n"
        f"invocationFile  : {inv}\n"
    )


def absolute_final_report_path(row: dict) -> Optional[Path]:
    from .final_report_paths import index_report_record_rel

    target = resolve_under_root(row.get("projectRoot", ""), index_report_record_rel(row))
    return target.resolve() if target is not None else None
