#!/usr/bin/env python3
"""Replay the numeric/provenance checks. Python 3; standard library only.

Usage: python3 verify.py scenarios.json ai-run.json
This does not call an AI model or verify free-form prose / production readiness.
"""
import json
import math
import re
import sys
from pathlib import Path


def expected_fields(fixture, scenario):
    stations = fixture['expected_stations']
    period = fixture['target_period']
    if len(stations) != len(set(stations)) or not stations:
        raise ValueError('invalid expected stations')
    seen, included, conflicts, sources = set(), [], [], []
    total = 0
    for row in scenario['input']:
        station = row['station']
        if station not in stations or station in seen:
            raise ValueError('unexpected or duplicate station')
        seen.add(station)
        value = row['energy_kwh']
        if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
            raise ValueError('invalid energy value')
        if not isinstance(row.get('source_record'), str) or not row['source_record'].strip():
            raise ValueError('missing source record')
        if row['period_start'] != period['start'] or row['period_end'] != period['end'] or row['timezone'] != period['timezone']:
            conflicts.append(station)
            continue
        included.append(station)
        sources.append(row['source_record'])
        total += value
    missing = [s for s in stations if s not in seen]
    status = 'needs_period_review' if conflicts else 'partial' if missing else 'complete'
    return {'status':status, 'included_stations':included, 'missing_stations':missing,
            'conflicting_stations':conflicts, 'known_total_kwh':total,
            'full_total_kwh':total if status == 'complete' else None, 'source_records':sources}


def validate(fixture, scenario, actual):
    expected = expected_fields(fixture, scenario)
    if not isinstance(actual, dict):
        return ['output must be an object']
    errors = []
    for key, value in expected.items():
        got = actual.get(key)
        if key not in actual:
            errors.append(f'{key}: missing')
        elif isinstance(value, list):
            if not isinstance(got, list) or any(not isinstance(x,str) for x in got) or sorted(got) != sorted(value):
                errors.append(f'{key}: mismatch')
        elif value is None:
            if got is not None:
                errors.append(f'{key}: expected null')
        elif isinstance(value, (int,float)):
            if isinstance(got,bool) or not isinstance(got,(int,float)) or not math.isfinite(got) or got != value:
                errors.append(f'{key}: mismatch')
        elif got != value:
            errors.append(f'{key}: mismatch')
    if not isinstance(actual.get('summary'), str) or not actual['summary'].strip():
        errors.append('summary: missing text')
    return errors


def verify(fixture, run):
    scenarios = fixture['scenarios']
    records = run['records']
    ids = [r['id'] for r in records]
    if len(ids) != len(set(ids)) or set(ids) != {s['id'] for s in scenarios}:
        raise ValueError('scenario IDs missing, duplicated or unexpected')
    checked = []
    for scenario in scenarios:
        record = next(r for r in records if r['id'] == scenario['id'])
        request = {'target_period':fixture['target_period'], 'expected_stations':fixture['expected_stations'], 'records':scenario['input']}
        errors = []
        if record.get('input') != request:
            errors.append('request does not match fixture')
        try:
            text = record.get('raw_output','').strip()
            fence = re.fullmatch(r'```(?:json)?\s*\n(.*?)\n```', text, re.DOTALL)
            raw = json.loads(fence.group(1) if fence else text)
        except (ValueError,TypeError):
            raw = None
            errors.append('raw output is not JSON')
        if raw != record.get('parsed'):
            errors.append('parsed output differs from raw output')
        errors.extend(validate(fixture, scenario, raw))
        checked.append({'id':scenario['id'], 'passed':not errors, 'errors':errors})
    return {'scope':'numeric fields, coverage, period conflicts and source-record provenance; prose semantics need human review',
            'checks':checked, 'passed':sum(c['passed'] for c in checked), 'total':len(checked)}


if __name__ == '__main__':
    if len(sys.argv) != 3:
        sys.exit('Usage: python3 verify.py scenarios.json ai-run.json')
    try:
        result = verify(json.loads(Path(sys.argv[1]).read_text()), json.loads(Path(sys.argv[2]).read_text()))
        print(json.dumps(result, ensure_ascii=False, indent=2))
        sys.exit(0 if result['passed'] == result['total'] else 1)
    except (ValueError,KeyError,TypeError,OSError) as error:
        print(json.dumps({'passed':False, 'error':str(error)}, ensure_ascii=False))
        sys.exit(1)
