from math import isfinite

def _num(x): return isinstance(x,(int,float)) and not isinstance(x,bool) and isfinite(x)
def _r(x): return round(float(x)+0.0,2)
def _out(state, source, allocated, unallocated, direct, shared, delta, reason):
    return {'decision_state':state,'source_total':source,'allocated_total':allocated,'unallocated_total':unallocated,'direct_allocated':direct,'shared_allocated':shared,'reconciliation_delta':delta,'reason_code':reason}

def evaluate(c):
    if c.get('cost_period') in (None,'') or c.get('currency') in (None,'') or not isinstance(c.get('lines'),list) or not c['lines']:
        return _out('HOLD',None,None,None,None,None,None,'DECISIVE_EVIDENCE_MISSING')
    source=0.0
    for line in c['lines']:
        if line.get('source_line_ref') in (None,'') or line.get('category') in (None,'') or line.get('classification') in (None,'') or line.get('evidence_ref') in (None,'') or not _num(line.get('effective_cost')) or line['effective_cost']<0:
            return _out('HOLD',None,None,None,None,None,None,'DECISIVE_EVIDENCE_MISSING')
        source += line['effective_cost']
    source=_r(source)
    if c.get('reconciliation_locked') and _num(c.get('snapshot_source_total')) and abs(source-c['snapshot_source_total'])>1e-9:
        allocated=_r(c['snapshot_allocated_total']) if _num(c.get('snapshot_allocated_total')) else None
        return _out('HOLD',source,allocated,None,None,None,_r(source-c['snapshot_source_total']),'LATE_ADJUSTMENT_REQUIRES_RECONCILIATION')
    for line in c['lines']:
        cls=line['classification']
        if cls=='DIRECT' and (line.get('owner') in (None,'') or line.get('target') in (None,'')):
            return _out('HOLD',source,None,None,None,None,None,'DIRECT_OWNERSHIP_MISSING')
        if cls=='SHARED':
            d=line.get('driver')
            if not isinstance(d,dict) or not d.get('method') or not isinstance(d.get('quantities'),dict) or not d['quantities']:
                return _out('HOLD',source,None,None,None,None,None,'SHARED_DRIVER_EVIDENCE_MISSING')
            if d.get('method')=='EQUAL_SPLIT' and d.get('approved') is not True:
                return _out('DISPUTED',source,None,None,None,None,None,'UNAPPROVED_EQUAL_SPLIT')
            if d.get('basis_version')!=d.get('pool_version'):
                return _out('DISPUTED',source,None,None,None,None,None,'DRIVER_SCOPE_OR_VERSION_STALE')
            if any(not _num(v) or v<0 for v in d['quantities'].values()) or sum(d['quantities'].values())<=0:
                return _out('HOLD',source,None,None,None,None,None,'INVALID_DRIVER_QUANTITY')
            if d.get('approved') is not True:
                return _out('DISPUTED',source,None,None,None,None,None,'DRIVER_NOT_APPROVED')
        elif cls not in ('DIRECT','UNRESOLVED'):
            return _out('DISPUTED',source,None,None,None,None,None,'UNKNOWN_COST_CLASSIFICATION')
    direct=0.0; shared=0.0; unallocated=0.0
    for line in c['lines']:
        if line['classification']=='DIRECT': direct += line['effective_cost']
        elif line['classification']=='UNRESOLVED': unallocated += line['effective_cost']
        else:
            d=line['driver']; total=sum(d['quantities'].values())
            # Deterministic allocation sums exactly to the pool; per-target details are learner work.
            shared += line['effective_cost'] if total>0 else 0.0
    allocated=direct+shared
    delta=source-allocated-unallocated
    vals=(_r(allocated),_r(unallocated),_r(direct),_r(shared),_r(delta))
    if abs(delta)>0.005:
        return _out('HOLD',source,*vals,'RECONCILIATION_DELTA_NONZERO')
    if unallocated>0.005:
        return _out('PARTIAL',source,*vals,'UNALLOCATED_RESIDUE_VISIBLE')
    return _out('RECONCILED',source,*vals,'PERIOD_FULLY_RECONCILED')
