"""Strict recurrence relationship and review-history validation for format seven."""
import json,re,sqlite3
from . import recurrence_models as m
from .scheduling import calendar_date,decode
from .recurrence import TABLES

def validate(db,require):
    previous=db.row_factory;db.row_factory=sqlite3.Row
    try:
        rows=lambda t:[dict(r) for r in db.execute('SELECT * FROM '+t)]
        data={t:{r['id']:r for r in rows(t)} for t in TABLES if t!='recurrence_history'}
        contracts=data['recurring_contracts'];rules=data['recurrence_rules'];blocks=data['recurrence_blocks'];occ=data['recurrence_occurrences'];runs=data['recurrence_runs']
        jobs={r['id']:r for r in rows('jobs')};people={r['id'] for r in rows('team_resources')};bookings={r['id']:r for r in rows('calendar_bookings')}
        identity=lambda v:isinstance(v,str) and re.fullmatch('[a-f0-9]{32}',v) is not None
        require(len(contracts)<=100 and len(occ)<=5000 and len(runs)<=10000,'Recurrence limits exceeded.')
        require(all(identity(k) for t in data.values() for k in t),'Invalid recurrence identity.')
        from .customer_review import historical_owner
        def contract(v):
            m.Metadata.model_validate({k:v[k] for k in m.Metadata.model_fields})
            require(v['state'] in ('active','paused','retired') and type(v['version']) is int and v['version']>=1,'Invalid recurring definition state.')
            require(v['kind'] in ('job','unavailable'),'Invalid recurring booking kind.')
            if v['kind']=='job':require(v['job_id'] in jobs and (jobs[v['job_id']]['customer_id']==v['customer_id'] or contracts[v['id']]['state']=='retired' and historical_owner(db,v['job_id'],v['customer_id'])),'Recurring job/customer binding is invalid.')
            else:require(v['job_id'] is None and v['customer_id'] is None,'Unavailable recurrence has customer work attached.')
        def rule(v):
            require(v['contract_id'] in contracts and type(v['revision']) is int and 1<=v['revision']<=contracts[v['contract_id']]['version'],'Invalid rule revision.')
            r=m.Rule.model_validate_json(v['content']);calendar_date(v['effective_from'])
            require(v['effective_from']<=r.until_date and set(r.resource_ids)<=people,'Invalid rule horizon or resources.')
            require(contracts[v['contract_id']]['kind']!='job' or not r.whole_team,'Customer recurrence uses unnamed whole team.')
        def block(v):
            require(v['contract_id'] in contracts and v['active'] in (0,1) and type(v['version']) is int and v['version']>=1,'Invalid exclusion state.')
            m.Block(start_date=v['start_date'],end_date=v['end_date'],reason=v['reason'])
        def occurrence(v):
            require(v['contract_id'] in contracts and type(v['version']) is int and v['version']>=1 and v['state'] in ('active','excluded','cancelled') and v['overridden'] in (0,1),'Invalid occurrence state.')
            calendar_date(v['nominal_date']);cid=v['contract_id']
            require(all(rid in rules and rules[rid]['contract_id']==cid for rid in (v['rule_id'],v['original_rule_id'])),'Occurrence rule belongs to another contract.')
            require(json.loads(v['rule_snapshot'])==json.loads(rules[v['rule_id']]['content']),'Occurrence rule snapshot is inconsistent.')
            ids=json.loads(v['exclusion_ids']);require(isinstance(ids,list) and len(ids)==len(set(ids)) and all(i in blocks and blocks[i]['contract_id']==cid for i in ids),'Occurrence exclusion belongs to another contract.')
            require(v['booking_id'] is None or v['booking_id'] in bookings and bookings[v['booking_id']]['kind']==contracts[cid]['kind'] and bookings[v['booking_id']]['job_id']==contracts[cid]['job_id'],'Occurrence booking changed its exact job/kind.')
            require(v['state']!='active' or v['booking_id'] is not None,'Active occurrence has no booking.')
        validators={'contract':contract,'rule':rule,'block':block,'occurrence':occurrence}
        tables={'contract':contracts,'rule':rules,'block':blocks,'occurrence':occ}
        for kind,table in tables.items():
            for value in table.values():validators[kind](value)
        for v in occ.values():
            if v['booking_id']:require(bookings[v['booking_id']]['archived']==int(v['state']!='active'),'Occurrence/calendar active states disagree.')
        histories={k:{rid:[] for rid in t} for k,t in tables.items()}
        allowed={'created','generate','revise','state','block','unblock','override','cancel','exclude','restore','removed'}
        for h in sorted(rows('recurrence_history'),key=lambda h:h['id']):
            require(h['kind'] in tables and h['record_id'] in tables[h['kind']] and h['action'] in allowed,'Unrecognised recurrence history.')
            current=tables[h['kind']][h['record_id']];after=json.loads(h['after_value']);before=json.loads(h['before_value']) if h['before_value'] else None;detail=json.loads(h['detail'])
            require(set(after)==set(current) and (before is None or set(before)==set(current)),'Recurrence history fields changed.')
            validators[h['kind']](after)
            require(h['contract_id']==(after['id'] if h['kind']=='contract' else after['contract_id']),'Recurrence history contract differs.')
            immutable={'contract':('id','kind','job_id','customer_id','created_at'),'rule':tuple(current),'block':('id','contract_id','start_date','end_date','reason','created_at'),'occurrence':('id','contract_id','nominal_date','original_rule_id','created_at')}[h['kind']]
            require(all(after[k]==current[k] for k in immutable),'Recurrence history changes immutable identity.')
            if h['kind']=='occurrence' and after['booking_id']:require(after['booking_id']==current['booking_id'],'Occurrence changed its stable booking identity.')
            sequence=histories[h['kind']][h['record_id']];require(before==(sequence[-1] if sequence else None),'Recurrence history chain is incomplete.')
            if h['kind']=='rule':require(not sequence and h['action']=='created','Rules must be immutable.')
            else:require(after['version']==len(sequence)+1,'Recurrence versions are not contiguous.')
            if h['kind'] in ('contract','occurrence') and h['action']!='created':require(detail.get('run_id') in runs and runs[detail['run_id']]['contract_id']==h['contract_id'],'Change lacks a reviewed run.')
            sequence.append(after)
        for kind,table in tables.items():
            for rid,current in table.items():require(bool(histories[kind][rid]) and histories[kind][rid][-1]==current,'Latest recurrence history does not match its record.')
        for c in contracts.values():require(any(r['contract_id']==c['id'] and r['revision']==1 for r in rules.values()),'Contract has no initial rule.')
        for run in runs.values():
            require(run['contract_id'] in contracts and re.fullmatch('[a-f0-9]{64}',run['preview_hash']) is not None and run['acknowledge_retained'] in (0,1),'Invalid confirmed recurrence run.')
            plan=json.loads(run['plan']);op=json.loads(run['operation']);result=json.loads(run['result'])
            m.Preview(version=plan['contract_version'],operation=op)
            require(plan['contract_id']==run['contract_id'] and plan['operation']==op and plan['preview_hash']==run['preview_hash'] and plan['can_confirm'] is True and not plan['errors'] and len(plan['rows'])<=1000 and all(r['status']!='error' for r in plan['rows']),'Saved recurrence preview is inconsistent.')
            require(not plan['retained_for_separate_review'] or run['acknowledge_retained']==1,'Retained visits were not acknowledged.')
            require(result['run_id']==run['id'] and result['contract_id']==run['contract_id'] and result['preview_hash']==run['preview_hash'] and result['version']==plan['contract_version']+1,'Saved recurrence result is inconsistent.')
            changed=[r for r in plan['rows'] if r['change']];ids=result['changed_occurrence_ids'];require(len(ids)==len(changed)==plan['change_count'] and len(set(ids))==len(ids),'Run changed occurrence inventory is inconsistent.')
            for rid,row in zip(ids,changed):require(rid in occ and occ[rid]['contract_id']==run['contract_id'] and occ[rid]['nominal_date']==row['nominal_date'],'Run changed another occurrence.')
            require(any(h['version']==result['version'] for h in histories['contract'][run['contract_id']]),'Run lacks its contract revision.')
        # The current generated booking must still equal its last audited schedule.
        for o in occ.values():
            if not o['booking_id']:continue
            audit=db.execute('SELECT after_value FROM calendar_audit WHERE record_id=? ORDER BY id DESC LIMIT 1',(o['booking_id'],)).fetchone()
            require(audit is not None and json.loads(audit[0])==decode(dict(bookings[o['booking_id']])),'Generated calendar history is inconsistent.')
        return {'contracts':len(contracts),'rules':len(rules),'occurrences':len(occ),'exclusions':len(blocks),'confirmed_reviews':len(runs)}
    finally:db.row_factory=previous
