"""Reference solution for Sustainability data-quality project: build an honest electricity inventory.
Original synthetic teaching data only. Python 3.10+, standard library.
Run python solve.py [path-to-project-inputs]. No network or credentials.
"""
from pathlib import Path
from datetime import datetime, date
import csv, json, math, statistics, sys

def read(root, name):
    with (Path(root)/name).open(newline='',encoding='utf-8') as f:
        reader=csv.DictReader(f)
        if not reader.fieldnames or len(reader.fieldnames)!=len(set(reader.fieldnames)):
            raise ValueError(f'{name}: missing or duplicate headers')
        rows=list(reader)
        if any(None in row or any(v is None for v in row.values()) for row in rows):
            raise ValueError(f'{name}: inconsistent row length')
        return rows

def rules(root): return json.loads((Path(root)/'rules.json').read_text())

def number(value):
    x=float(value)
    if not math.isfinite(x): raise ValueError('Non-finite input')
    return x

def nonnegative(value):
    x=number(value)
    if x<0:raise ValueError('Negative quantity or amount')
    return x

def flag(value):
    if value not in ('true','false'):raise ValueError('Boolean must be true or false')
    return value=='true'

def timestamp(value):
    d=datetime.fromisoformat(value.replace('Z','+00:00'))
    if d.tzinfo is None:raise ValueError('Timestamp requires an explicit time zone')
    return d

def unique(rows, keys, ignore=()):
    by={};duplicates=0
    for row in rows:
        key=tuple(row[k] for k in keys)
        if any(not k for k in key):raise ValueError('Missing record identity')
        if key in by:
            prior={k:v for k,v in by[key].items() if k not in ignore}
            new={k:v for k,v in row.items() if k not in ignore}
            if prior!=new:raise ValueError(f'Conflicting records at {key}')
            duplicates+=1
        else:by[key]=row
    return list(by.values()),duplicates

def rounded(value):
    if isinstance(value,float): return round(value,8)
    if isinstance(value,dict):return {k:rounded(v) for k,v in value.items()}
    if isinstance(value,list):return [rounded(v) for v in value]
    return value

def month_index(s):
    d=datetime.strptime(s,'%Y-%m')
    if d.strftime('%Y-%m')!=s:raise ValueError('Use YYYY-MM')
    return d.year*12+d.month

def electricity_quality(root):
    r=rules(root);facilities,_=unique(read(root,'facilities.csv'),['facility_id']);facts={x['facility_id']:x for x in facilities};factor_rows,_=unique(read(root,'fictional_factors.csv'),['region']);factors={x['region']:nonnegative(x['kg_co2e_per_kwh']) for x in factor_rows}
    raw=read(root,'activity.csv');rows,retries=unique(raw,['record_id']);cells={};out=0
    for x in rows:
        if x['facility_id'] not in facts:raise ValueError('Unmapped facility')
        if not flag(facts[x['facility_id']]['in_scope']) or x['month'] not in r['months']:out+=1;continue
        key=(x['facility_id'],x['month'])
        if key in cells:raise ValueError('Two different records cover one expected cell')
        cells[key]=x
    subtotal=kwh=estimate=0.;missing=[];known=0
    for f in facilities:
        if not flag(f['in_scope']):continue
        for m in r['months']:
            key=(f['facility_id'],m);x=cells.get(key)
            if x is None or x['quantity']=='':missing.append('/'.join(key));continue
            if x['quality'] not in ('metered','estimated'):raise ValueError('Unrecognized quality')
            if x['unit'] not in r['conversion']:raise ValueError('Unknown unit')
            value=nonnegative(x['quantity'])*r['conversion'][x['unit']];kwh+=value;known+=1
            if x['quality']=='estimated':estimate+=value
            if f['region'] not in factors:raise ValueError('Missing factor')
            subtotal+=value*factors[f['region']]/1000
    # The complete quarter is calculable only when the whole expected grid is known.
    # The supplied single-cell scenarios do not estimate any additional missing cell.
    scenario=([subtotal+value*factors[facts['C']['region']]/1000 for value in r['missing_scenarios_kwh']]
              if missing==['C/2026-02'] else None)
    return {'raw_rows':len(raw),'retry_rows_removed':retries,'out_of_scope_or_period':out,'known_cells':known,'expected_cells':sum(flag(f['in_scope']) for f in facilities)*len(r['months']),'missing_cells':missing,'known_kwh_including_estimate':kwh,'estimated_kwh':estimate,'known_input_tonnes':subtotal,'full_quarter_tonnes':None if missing else subtotal,'missing_month_scenario_tonnes':scenario}

def main():
    directory=Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).resolve().parent
    try:
        result=electricity_quality(directory)
        print(json.dumps(rounded(result),indent=2,allow_nan=False))
    except (OSError,ValueError,KeyError,TypeError,ZeroDivisionError) as exc:
        print(f"Input could not be analyzed: {exc}",file=sys.stderr);return 2
    return 0
if __name__=='__main__':raise SystemExit(main())
