"""Nova Per-Run Manifest Writer (REQ-187).

Emits nova.run.started, nova.run.completed, nova.run.failed events with
(run_id, contractId, env, stages x durations, exit, confidence, HITL block
count). Writes metrics/runs/<run_id>.json. scripts/run_platform.sh invokes
the writer at run start + run end.

D-120: Nova-native (JSONL events + JSON manifest file, no Kafka).
D-128: metrics/ at repo root.
"""

import datetime
import json
import os
import sys
import time
import uuid

_METRICS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "metrics")
_RUNS_DIR = os.path.join(_METRICS_DIR, "runs")

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from core.metrics.event_envelope import emit, make_event, append_event


def _iso8601_now():
    return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _backfill_outcome(decision_id, outcome):
    """Transition fact_decision.outcome pending -> outcome (REQ-317).

    Best-effort: logs a warning and skips if decision_id is missing or the
    backfill raises. Never raises — the run is already completing/failing
    and the manifest write is the source of truth for the run outcome.
    """
    if not decision_id:
        # A run that failed before ai.decision.made was emitted has no
        # decision to backfill (e.g. a schema-validation failure). Skip
        # silently rather than pollute stderr on every clean run.
        return None
    try:
        from core.metrics import outcome_backfill
        return outcome_backfill.backfill(decision_id, outcome)
    except Exception as exc:  # pragma: no cover - defensive
        print(f"[run_manifest] outcome backfill skipped for {decision_id}: {exc}",
              file=sys.stderr)
        return None


def _run_id():
    return f"run-{int(time.time())}-{uuid.uuid4().hex[:8]}"


def start_run(contract_id, environment, stages=None):
    """Emit nova.run.started + return the run_id."""
    run_id = _run_id()
    data = {
        "contract_id": contract_id,
        "environment": environment,
        "started_at": _iso8601_now(),
        "stages": stages or [],
    }
    emit("nova.run.started", run_id, environment, data, contract_id=contract_id)
    return run_id


def complete_run(run_id, contract_id, environment, stages, exit_code, confidence=None, hitl=None, policy=None, cost_estimate_usd=None, decision_id=None, escalation_reason=None):
    """Emit nova.run.completed + write the per-run manifest JSON.

    Args:
        run_id: the run identifier from start_run()
        contract_id: the contract UUID
        environment: dev|qa|prod|dr
        stages: list of {name, duration_ms, exit_code, error?}
        exit_code: the overall run exit code
        confidence: optional {score, band, perInput}
        hitl: optional {gate, result, block}
        policy: optional {passed, failed, skipped}
        cost_estimate_usd: optional float
        decision_id: optional string (links to the Decision Ledger)
        escalation_reason: optional string (REQ-318) — "confidence" when
            the ai.decision.made band was block; absent/None otherwise.
            Persisted into the manifest so the collector can write it
            into fact_run (Post-Pilot Human Escalation Frequency denom).
    """
    started_at = stages[0].get("started_at", _iso8601_now()) if stages else _iso8601_now()
    completed_at = _iso8601_now()
    outcome = "succeeded" if exit_code == 0 else "failed"

    manifest = {
        "run_id": run_id,
        "contract_id": contract_id,
        "environment": environment,
        "started_at": started_at,
        "completed_at": completed_at,
        "exit_code": exit_code,
        "stages": stages,
        "outcome": outcome,
    }
    if confidence:
        manifest["confidence"] = confidence
    if hitl:
        manifest["hitl"] = hitl
    if policy:
        manifest["policy"] = policy
    if cost_estimate_usd is not None:
        manifest["cost_estimate_usd"] = cost_estimate_usd
    if decision_id:
        manifest["decision_id"] = decision_id
    if escalation_reason:
        manifest["escalation_reason"] = escalation_reason

    os.makedirs(_RUNS_DIR, exist_ok=True)
    manifest_path = os.path.join(_RUNS_DIR, f"{run_id}.json")
    with open(manifest_path, "w", encoding="utf-8") as fh:
        json.dump(manifest, fh, indent=2, sort_keys=True)

    event_type = "nova.run.completed" if exit_code == 0 else "nova.run.failed"
    emit(event_type, run_id, environment, manifest, contract_id=contract_id)

    # REQ-317: backfill fact_decision.outcome pending -> succeeded/failed
    # after the run completes. The decision_id links the run to the
    # Decision Ledger entry written by ai.decision.made. Best-effort: a
    # run that failed before ai.decision.made was emitted has no
    # decision_id and the backfill is a no-op (the run outcome is still
    # captured in the manifest above).
    backfill_result = _backfill_outcome(decision_id, outcome)

    return manifest


def persist_run_artifacts(run_id, work_dir):
    """Copy ephemeral $WORK/*.json to metrics/runs/<run_id>/ as durable artifacts.

    Args:
        run_id: the run identifier
        work_dir: the $WORK directory (e.g. /tmp/nova_platform_run)
    """
    if not work_dir or not os.path.isdir(work_dir):
        return []
    dest = os.path.join(_RUNS_DIR, run_id)
    os.makedirs(dest, exist_ok=True)
    copied = []
    for fname in ("pcr.json", "signal.json", "event.json", "outbox_item.json", "stack.json", "checkov.json"):
        src = os.path.join(work_dir, fname)
        if os.path.isfile(src):
            import shutil
            shutil.copy2(src, os.path.join(dest, fname))
            copied.append(fname)
    return copied


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("usage: run_manifest.py <start|complete|persist> <contract_id> <environment> [run_id] [work_dir]", file=sys.stderr)
        sys.exit(2)
    action = sys.argv[1]
    cid = sys.argv[2]
    env = sys.argv[3]
    if action == "start":
        rid = start_run(cid, env)
        print(rid)
    elif action == "complete":
        rid = sys.argv[4] if len(sys.argv) >= 5 else _run_id()
        m = complete_run(rid, cid, env, [], 0)
        print(json.dumps(m, indent=2))
    elif action == "persist":
        rid = sys.argv[4] if len(sys.argv) >= 5 else ""
        wd = sys.argv[5] if len(sys.argv) >= 6 else ""
        copied = persist_run_artifacts(rid, wd)
        print(json.dumps({"copied": copied}))