#!/usr/bin/env python3
"""
validate_json_generator_outputs_v1.py — py→json 생성기 출력 검증 하네스.
호출 성공 여부가 아니라 실제 출력 내용이 계약을 충족하는지 검증한다.
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
TEMP = ROOT / "Temp"

CONTRACTS = [
    {
        "id": "computed_harness_v1",
        "file": "Temp/computed_harness_v1.json",
        "generator": "src/quant_engine/compute_formula_outputs.py",
        "required_keys": ["order_blueprint_json", "cash_recovery_plan_json", "per_ticker", "meta"],
        "non_null_keys": ["per_ticker", "meta"],
        "list_non_empty_keys": ["per_ticker"],
        "nested": [
            {"path": ["meta", "formulas_run"], "type": "list"},
            {"path": ["meta", "source_file"], "type": "str_nonempty"},
        ],
    },
    {
        "id": "final_decision_packet_active",
        "file": "Temp/final_decision_packet_active.json",
        "generator": "src/dotnet/QuantEngine.Tools -- packet-v4",
        "required_keys": ["formula_id", "meta", "canonical_metrics", "pass_100", "execution_readiness", "prediction"],
        "non_null_keys": ["formula_id", "pass_100", "execution_readiness"],
        "list_non_empty_keys": [],
        "nested": [
            {"path": ["pass_100", "gate"], "type": "str_nonempty"},
            {"path": ["execution_readiness", "gate"], "type": "str_nonempty"},
            {"path": ["canonical_metrics", "total_asset_krw"], "type": "numeric_or_none_warn"},
        ],
    },
    {
        "id": "operational_report",
        "file": "Temp/operational_report.json",
        "generator": "src/dotnet/QuantEngine.Tools -- report",
        "required_keys": ["schema_version", "generated_at", "sections", "section_errors"],
        "non_null_keys": ["sections"],
        "list_non_empty_keys": ["sections"],
        "nested": [
            {"path": ["section_count"], "type": "int_ge", "min": 30},
        ],
    },
]


def _get_nested(data: Any, path: list) -> Any:
    cur = data
    for k in path:
        if isinstance(cur, dict):
            cur = cur.get(k)
        else:
            return None
    return cur


def _check_contract(contract: dict, errors: list, warns: list) -> bool:
    fpath = ROOT / contract["file"]
    cid   = contract["id"]

    if not fpath.exists():
        errors.append(f"[{cid}] 파일 없음: {contract['file']}")
        return False

    try:
        data = json.loads(fpath.read_text(encoding="utf-8"))
    except Exception as exc:
        errors.append(f"[{cid}] JSON 파싱 실패: {exc}")
        return False

    if not isinstance(data, dict):
        errors.append(f"[{cid}] 루트가 dict가 아님: {type(data).__name__}")
        return False

    ok = True

    for k in contract.get("required_keys", []):
        if k not in data:
            errors.append(f"[{cid}] 필수 키 누락: {k}")
            ok = False

    for k in contract.get("non_null_keys", []):
        if data.get(k) is None:
            errors.append(f"[{cid}] 필수 키가 null: {k}")
            ok = False

    for k in contract.get("list_non_empty_keys", []):
        val = data.get(k)
        if not isinstance(val, list) or len(val) == 0:
            errors.append(f"[{cid}] 리스트가 비어있음: {k} (got {type(val).__name__} len={len(val) if isinstance(val, list) else '?'})")
            ok = False

    for nc in contract.get("nested", []):
        path   = nc["path"]
        chk    = nc["type"]
        val    = _get_nested(data, path)
        dotted = ".".join(str(p) for p in path)

        if chk == "list":
            if not isinstance(val, list):
                errors.append(f"[{cid}] {dotted}: list 필요, got {type(val).__name__} ({val!r:.50})")
                ok = False
        elif chk == "str_nonempty":
            if not isinstance(val, str) or not val.strip():
                errors.append(f"[{cid}] {dotted}: 비어있지 않은 str 필요, got {val!r}")
                ok = False
        elif chk == "int_ge":
            mn = nc.get("min", 0)
            if not isinstance(val, int) or val < mn:
                errors.append(f"[{cid}] {dotted}: int>={mn} 필요, got {val!r}")
                ok = False
        elif chk == "numeric_or_none_warn":
            # 프로버넌스 래핑 dict {"value": N, ...} 허용
            actual = val.get("value") if isinstance(val, dict) else val
            if actual is None:
                warns.append(f"[{cid}] {dotted}: null (아직 집계 안 됐을 수 있음)")
            elif not isinstance(actual, (int, float)):
                errors.append(f"[{cid}] {dotted}: numeric 필요, got {type(actual).__name__} {actual!r}")
                ok = False

    return ok


def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--strict-warns", action="store_true",
                    help="경고도 FAIL 처리")
    args = ap.parse_args()

    all_errors: list[str] = []
    all_warns:  list[str] = []
    contract_results = []

    for contract in CONTRACTS:
        errs: list[str] = []
        wrns: list[str] = []
        passed = _check_contract(contract, errs, wrns)
        contract_results.append({
            "id":        contract["id"],
            "file":      contract["file"],
            "generator": contract["generator"],
            "gate":      "PASS" if (passed and not (args.strict_warns and wrns)) else "FAIL",
            "errors":    errs,
            "warns":     wrns,
        })
        all_errors.extend(errs)
        all_warns.extend(wrns)

    overall_ok = len(all_errors) == 0 and not (args.strict_warns and all_warns)
    gate = "PASS" if overall_ok else "FAIL"

    result = {
        "formula_id":     "JSON_GENERATOR_OUTPUT_HARNESS_V1",
        "contracts_total": len(CONTRACTS),
        "error_count":    len(all_errors),
        "warn_count":     len(all_warns),
        "gate":           gate,
        "contracts":      contract_results,
        "errors":         all_errors,
        "warns":          all_warns,
    }

    out = TEMP / "json_generator_outputs_v1.json"
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding="utf-8")

    for cr in contract_results:
        status = cr["gate"]
        print(f"  [{status}] {cr['id']} ({cr['file']})")
        for e in cr["errors"]:
            print(f"    ERROR: {e}")
        for w in cr["warns"]:
            print(f"    WARN:  {w}")

    print(f"JSON_GENERATOR_OUTPUT_HARNESS: gate={gate} errors={len(all_errors)} warns={len(all_warns)}")
    print(f"OUTPUT: {out}")

    return 0 if overall_ok else 1


if __name__ == "__main__":
    import sys
    sys.exit(main())
