#!/usr/bin/env python3
import csv
import re
import sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
MANIFEST = BASE / "apld_forecast_actual_manifest.csv"
ROWS = BASE / "apld_forecast_actual_rows.csv"

REQUIRED = [
    "company", "period", "metric_name", "raw_value", "normalized_value", "unit",
    "value_qualifier", "ownership_basis", "forecast_or_actual", "is_derived",
    "source_url", "source_location", "source_snippet", "snippet_support_status",
    "confidence", "caveat",
]
ALLOWED_UNITS = {"MW", "USD_billions", "USD_millions", "year"}
ALLOWED_CLASS = {"forecast", "actual"}
ALLOWED_SUPPORT = {"exact", "partial"}
ALLOWED_CONFIDENCE = {"high", "medium", "low"}
ALLOWED_DERIVED = {"True", "False"}
REQUIRED_BASIS_TERMS = {
    "critical_it_load",
    "utility_power",
    "legacy_data_center_hosting",
    "lease_base_term",
    "property_and_equipment",
}

def read_csv(path):
    if not path.exists():
        raise FileNotFoundError(path)
    with path.open(newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))

def nums(text):
    return re.findall(r"\d+(?:\.\d+)?", str(text).replace(",", ""))

def expected_norm(row):
    raw = str(row["raw_value"]).replace(",", "")
    if row["unit"] == "year":
        return float(row["normalized_value"])
    if "+" in raw:
        return sum(float(part) for part in raw.split("+"))
    value = float(raw)
    snippet = row["source_snippet"].lower()
    if row["unit"] == "MW" and ("gw" in snippet or "gigawatt" in snippet):
        return value * 1000
    return value

def snippet_supports(row):
    snippet = row["source_snippet"].replace(",", "")
    if row["unit"] == "year":
        return row["raw_value"] in snippet or "August 2027" in snippet
    raw_tokens = nums(row["raw_value"])
    norm_tokens = nums(row["normalized_value"])
    tokens = raw_tokens or norm_tokens
    return any(token in snippet for token in tokens)

def main():
    errors = []
    try:
        manifest = read_csv(MANIFEST)
        rows = read_csv(ROWS)
    except Exception as exc:
        print(f"validation failed: {exc}")
        return 1

    if len(manifest) < 8:
        errors.append(f"manifest row count too low: {len(manifest)}")
    manifest_urls = {r.get("source_url", "") for r in manifest}

    if not rows:
        errors.append("rows file is empty")
    elif set(REQUIRED) - set(rows[0]):
        errors.append(f"missing row columns: {sorted(set(REQUIRED) - set(rows[0]))}")
    if len(rows) < 30:
        errors.append(f"minimum row count failed: {len(rows)} < 30")

    classes = set()
    units = set()
    basis_blob = " ".join(r.get("ownership_basis", "") for r in rows)
    for idx, row in enumerate(rows, start=2):
        for col in REQUIRED:
            if not row.get(col, "").strip():
                errors.append(f"row {idx}: missing {col}")
        if row.get("company") != "Applied Digital":
            errors.append(f"row {idx}: bad company")
        if row.get("forecast_or_actual") not in ALLOWED_CLASS:
            errors.append(f"row {idx}: invalid forecast_or_actual")
        if row.get("unit") not in ALLOWED_UNITS:
            errors.append(f"row {idx}: invalid unit {row.get('unit')}")
        if row.get("is_derived") not in ALLOWED_DERIVED:
            errors.append(f"row {idx}: invalid is_derived")
        if row.get("source_url") not in manifest_urls:
            errors.append(f"row {idx}: source_url not present in manifest")
        if not row.get("source_url", "").startswith("https://"):
            errors.append(f"row {idx}: source_url must be https")
        if row.get("snippet_support_status") not in ALLOWED_SUPPORT:
            errors.append(f"row {idx}: invalid snippet_support_status")
        if row.get("confidence") not in ALLOWED_CONFIDENCE:
            errors.append(f"row {idx}: invalid confidence")
        if not snippet_supports(row):
            errors.append(f"row {idx}: snippet does not support value {row.get('raw_value')}")
        try:
            expected = expected_norm(row)
            actual = float(str(row["normalized_value"]).replace(",", ""))
            if abs(expected - actual) > 0.01:
                errors.append(f"row {idx}: normalized_value {actual} != expected {expected}")
        except Exception as exc:
            errors.append(f"row {idx}: normalization check failed: {exc}")
        classes.add(row.get("forecast_or_actual"))
        units.add(row.get("unit"))

    if classes != ALLOWED_CLASS:
        errors.append(f"forecast/actual coverage failed: {classes}")
    if "MW" not in units or "USD_billions" not in units:
        errors.append(f"unit coverage failed: {units}")
    for term in REQUIRED_BASIS_TERMS:
        if term not in basis_blob:
            errors.append(f"missing ownership basis term: {term}")

    if errors:
        print("APLD forecast-vs-actual validation failed")
        for error in errors:
            print(f"- {error}")
        return 1

    print("APLD forecast-vs-actual validation passed")
    print(f"- manifest rows: {len(manifest)}")
    print(f"- ledger rows: {len(rows)}")
    print(f"- classes: {sorted(classes)}")
    print(f"- units: {sorted(units)}")
    print("- provenance: every row source_url appears in manifest")
    print("- normalization: additive MW and GW-to-MW conversions checked")
    print("- ownership basis: critical IT load, utility power, legacy hosting, lease revenue, and PPE/development bases represented")
    return 0

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