#!/usr/bin/env python3
"""Validate ORCL production artifacts: schema, row counts, https provenance, snippets."""

from __future__ import annotations

import argparse
import csv
import json
import subprocess
import sys
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
MIN_QUARTERS = 8
MIN_VINTAGE_ROWS = 16

VINTAGE_FIELDS = [
    "company",
    "ticker",
    "as_of_period",
    "target_period",
    "metric_name",
    "raw_value",
    "normalized_gw",
    "unit",
    "forecast_or_actual",
    "is_derived",
    "source_url",
    "source_location",
    "source_snippet",
    "snippet_support_status",
    "confidence",
    "caveat",
]

TIMESERIES_FIELDS = [
    "company",
    "ticker",
    "fiscal_quarter",
    "actual_gw",
    "latest_forecast_gw",
    "original_forecast_gw",
    "under_construction_gw",
    "capex_actual_usd_b",
    "capex_guidance_latest_usd_b",
    "capex_guidance_original_usd_b",
    "capex_actual_metric",
    "capex_actual_is_derived",
    "caveat",
]

REQUIRED_FILES = [
    "source_manifest.csv",
    "forecast_vintage.csv",
    "capacity_timeseries.csv",
    "company_gw_chart.html",
    "extractor.py",
    "validator.py",
]

EXPECTED_QUARTERS = [
    "FY2024-FQ4",
    "FY2025-FQ1",
    "FY2025-FQ2",
    "FY2025-FQ3",
    "FY2025-FQ4",
    "FY2026-FQ1",
    "FY2026-FQ2",
    "FY2026-FQ3",
]


def load_csv(path: Path) -> list[dict[str, str]]:
    with path.open(encoding="utf-8") as handle:
        return list(csv.DictReader(handle))


def fail(errors: list[str]) -> int:
    report = {"status": "FAIL", "errors": errors}
    print(json.dumps(report, indent=2))
    return 1


def pass_report(checks: list[str]) -> int:
    report = {"status": "PASS", "checks": checks}
    print(json.dumps(report, indent=2))
    return 0


def validate_https_urls(rows: list[dict[str, str]], label: str) -> list[str]:
    errors: list[str] = []
    for i, row in enumerate(rows, start=2):
        url = row.get("source_url", "")
        if url and not url.startswith("https://"):
            errors.append(f"{label} row {i}: source_url must start with https:// ({url})")
    return errors


def validate_snippet_status(rows: list[dict[str, str]], label: str, sample_every: int = 3) -> list[str]:
    errors: list[str] = []
    for i, row in enumerate(rows):
        if i % sample_every != 0:
            continue
        status = row.get("snippet_support_status", "")
        if status != "verified_primary":
            errors.append(f"{label} sampled row {i+2}: snippet_support_status={status!r}")
        snippet = row.get("source_snippet", "").strip()
        if not snippet:
            errors.append(f"{label} sampled row {i+2}: empty source_snippet")
    return errors


def validate_vintage(path: Path, manifest_urls: set[str]) -> tuple[list[str], list[str]]:
    errors: list[str] = []
    checks: list[str] = []
    if not path.exists():
        return [f"Missing {path.name}"], checks
    rows = load_csv(path)
    if len(rows) < MIN_VINTAGE_ROWS:
        errors.append(f"forecast_vintage.csv needs >={MIN_VINTAGE_ROWS} rows, got {len(rows)}")
    else:
        checks.append(f"forecast_vintage rows={len(rows)} (>={MIN_VINTAGE_ROWS})")

    if rows:
        missing_cols = set(VINTAGE_FIELDS) - set(rows[0].keys())
        if missing_cols:
            errors.append(f"forecast_vintage missing columns: {sorted(missing_cols)}")

    as_of = {r["as_of_period"] for r in rows}
    if len(as_of) < MIN_QUARTERS:
        errors.append(f"forecast_vintage needs >={MIN_QUARTERS} as_of_period, got {len(as_of)}")
    else:
        checks.append(f"forecast_vintage as_of_periods={len(as_of)}")

    guidance = [r for r in rows if r["metric_name"] == "capex_guidance" and r["target_period"] == "FY2026"]
    if len(guidance) < 4:
        errors.append(f"Need >=4 FY2026 capex_guidance vintages, got {len(guidance)}")
    else:
        checks.append(f"FY2026 capex_guidance vintages={len(guidance)}")

    for i, row in enumerate(rows, start=2):
        if row["ticker"] != "ORCL":
            errors.append(f"vintage row {i}: ticker must be ORCL")
        if row["normalized_gw"] not in ("", None):
            errors.append(f"vintage row {i}: normalized_gw must be blank for ORCL")
        if row["source_url"] not in manifest_urls:
            errors.append(f"vintage row {i}: source_url not in manifest")

    errors.extend(validate_https_urls(rows, "forecast_vintage"))
    errors.extend(validate_snippet_status(rows, "forecast_vintage"))
    return errors, checks


def validate_timeseries(path: Path) -> tuple[list[str], list[str]]:
    errors: list[str] = []
    checks: list[str] = []
    if not path.exists():
        return [f"Missing {path.name}"], checks
    rows = load_csv(path)
    if len(rows) < MIN_QUARTERS:
        errors.append(f"capacity_timeseries.csv needs >={MIN_QUARTERS} rows, got {len(rows)}")
    else:
        checks.append(f"capacity_timeseries rows={len(rows)}")

    if rows:
        missing_cols = set(TIMESERIES_FIELDS) - set(rows[0].keys())
        if missing_cols:
            errors.append(f"capacity_timeseries missing columns: {sorted(missing_cols)}")

    quarters = [r["fiscal_quarter"] for r in rows]
    if quarters != EXPECTED_QUARTERS:
        errors.append(f"Unexpected fiscal_quarter sequence: {quarters}")

    for i, row in enumerate(rows, start=2):
        for gw_col in ("actual_gw", "latest_forecast_gw", "original_forecast_gw", "under_construction_gw"):
            if row.get(gw_col, "") != "":
                errors.append(f"timeseries row {i}: {gw_col} must be blank for ORCL GW proxy")
        if not row.get("capex_actual_usd_b"):
            errors.append(f"timeseries row {i}: missing capex_actual_usd_b")

    # Original guidance constant after first vintage
    originals = {r["capex_guidance_original_usd_b"] for r in rows if r["capex_guidance_original_usd_b"]}
    if originals != {"25"}:
        errors.append(f"original guidance should be 25 for all post-vintage quarters, got {originals}")

    # Latest guidance revision chain spot checks
    latest_by_q = {r["fiscal_quarter"]: r["capex_guidance_latest_usd_b"] for r in rows}
    expected_latest = {
        "FY2025-FQ4": "25",
        "FY2026-FQ1": "35",
        "FY2026-FQ2": "50",
        "FY2026-FQ3": "50",
    }
    for q, exp in expected_latest.items():
        if latest_by_q.get(q) != exp:
            errors.append(f"{q} latest guidance expected {exp}, got {latest_by_q.get(q)}")

    checks.append("timeseries guidance rollup spot-checks OK")
    return errors, checks


def validate_chart(path: Path) -> tuple[list[str], list[str]]:
    errors: list[str] = []
    checks: list[str] = []
    if not path.exists():
        return [f"Missing {path.name}"], checks
    text = path.read_text(encoding="utf-8")
    if "<svg" not in text:
        errors.append("company_gw_chart.html must contain SVG")
    if "GW-proxy caveat" not in text and "GW-proxy" not in text:
        errors.append("chart missing GW-proxy caveat banner")
    for series in ("Capex actual", "Latest FY2026", "Original FY2026"):
        if series not in text:
            errors.append(f"chart missing legend for: {series}")
    checks.append("company_gw_chart.html structure OK")
    return errors, checks


def validate_extractor(prod_dir: Path) -> tuple[list[str], list[str]]:
    errors: list[str] = []
    checks: list[str] = []
    extractor = prod_dir / "extractor.py"
    if not extractor.exists():
        return ["Missing extractor.py"], checks
    out = prod_dir / "_validator_forecast_vintage.csv"
    try:
        result = subprocess.run(
            [
                sys.executable,
                str(extractor),
                "--manifest",
                str(prod_dir / "source_manifest.csv"),
                "--output",
                str(out),
            ],
            capture_output=True,
            text=True,
            timeout=180,
            check=False,
        )
        if result.returncode != 0:
            errors.append(f"extractor.py failed: {result.stderr or result.stdout}")
            return errors, checks
        rows = load_csv(out)
        if len(rows) < MIN_VINTAGE_ROWS:
            errors.append(f"extractor regenerated {len(rows)} rows, need >={MIN_VINTAGE_ROWS}")
        else:
            checks.append(f"extractor regenerates {len(rows)} vintage rows")
        out.unlink(missing_ok=True)
    except subprocess.TimeoutExpired:
        errors.append("extractor.py timed out")
    return errors, checks


def main() -> int:
    parser = argparse.ArgumentParser(description="Validate ORCL production artifacts")
    parser.add_argument(
        "--dir",
        type=Path,
        default=BASE_DIR,
        help="Production ORCL directory (default: script directory)",
    )
    args = parser.parse_args()
    prod_dir = args.dir.resolve()

    all_errors: list[str] = []
    all_checks: list[str] = []

    for name in REQUIRED_FILES:
        if not (prod_dir / name).exists():
            all_errors.append(f"Missing required file: {name}")
        else:
            all_checks.append(f"present: {name}")

    manifest_path = prod_dir / "source_manifest.csv"
    manifest_urls: set[str] = set()
    if manifest_path.exists():
        manifest_rows = load_csv(manifest_path)
        manifest_urls = {r["source_url"] for r in manifest_rows}
        if len(manifest_rows) < 4:
            all_errors.append(f"source_manifest needs >=4 rows, got {len(manifest_rows)}")
        for row in manifest_rows:
            if row["ticker"] != "ORCL":
                all_errors.append("manifest contains non-ORCL ticker")
            if not row["source_url"].startswith("https://"):
                all_errors.append(f"manifest bad url: {row['source_url']}")

    errs, checks = validate_vintage(prod_dir / "forecast_vintage.csv", manifest_urls)
    all_errors.extend(errs)
    all_checks.extend(checks)

    errs, checks = validate_timeseries(prod_dir / "capacity_timeseries.csv")
    all_errors.extend(errs)
    all_checks.extend(checks)

    errs, checks = validate_chart(prod_dir / "company_gw_chart.html")
    all_errors.extend(errs)
    all_checks.extend(checks)

    errs, checks = validate_extractor(prod_dir)
    all_errors.extend(errs)
    all_checks.extend(checks)

    if all_errors:
        return fail(all_errors)
    return pass_report(all_checks)


if __name__ == "__main__":
    raise SystemExit(main())
