"""Format-14 evidence for an additive correction, including reactivation-only reviews."""
import json
import re
from datetime import datetime
from uuid import UUID, uuid5
from .customer_review import CONTACT,CONTACT_FIELDS,packed
from .customer_review_models import MergeCorrectionSelection
from .store import digest


def validate(db,require,histories,transfers,jobs,chains):
    rows=lambda table:[dict(r) for r in db.execute('SELECT * FROM '+table+' ORDER BY rowid')]
    corrections=rows('customer_merge_corrections');seen=set();actions=set()
    require(len(corrections)<=5000,'Too many merge corrections.')
    def safe(v):return {**v['contact'],'state':v['record']['state'],'canonical_id':v['record']['canonical_id'],'notes':v['record']['notes'],'record_version':v['record']['version']}
    def job_evidence(j):
        from .workflow import JOB_FIELDS
        require(j is not None and j['id'] in jobs and set(j)==set(jobs[j['id']]),'Correction lost an inventoried job.')
        require(any(json.loads(h['snapshot'])['job']=={k:j[k] for k in JOB_FIELDS} for h in rows('job_workflow_history') if h['job_id']==j['id']),'Correction job version has no retained work history.')
        require(all(j[k]==jobs[j['id']][k] for k in j if k not in JOB_FIELDS),'Correction changed immutable job provenance.')
    for r in corrections:
        require(re.fullmatch('[a-f0-9]{32}',r['id']) is not None and r['id'] not in transfers and r['merge_id'] in transfers and r['merge_id'] not in seen,'Invalid or duplicated merge correction identity.')
        require(all(isinstance(r[k],str) and datetime.fromisoformat(r[k]).tzinfo is not None for k in ('created_at','completed_at')),'Invalid correction timestamp.')
        seen.add(r['merge_id']);original=transfers[r['merge_id']];old=json.loads(original['plan']);p=json.loads(r['plan']);out=json.loads(r['result'])
        require(original['kind']=='merge' and (r['alias_id'],r['retained_id'])==(original['source_id'],original['target_id']),'Correction references a different merge.')
        require(set(p)=={'selection','context','retained_fields','work_review','scope','feedback_review','actor_access','dependency_hash','errors','can_confirm','effect','preview_hash'} and p['preview_hash']==r['preview_hash']==digest(packed({k:v for k,v in p.items() if k!='preview_hash'})),'Correction preview shape or hash changed.')
        selection=MergeCorrectionSelection.model_validate(p['selection']);require(selection.model_dump()==p['selection'],'Noncanonical merge correction.')
        require((selection.merge_id,selection.alias_id,selection.retained_id)==(r['merge_id'],r['alias_id'],r['retained_id']) and p['can_confirm'] is True and p['errors']==[],'Correction was not an exact confirmable review.')
        require(isinstance(r['reason'],str) and 5<=len(r['reason'].strip())<=1000 and original['created_at']<=r['created_at']<=r['completed_at'],'Invalid correction reason or chronology.')
        a=p['actor_access'];require(set(a)=={'business_id','subject','membership_id','membership_version','role'} and a['subject']==r['actor'] and a['role']=='owner' and type(a['membership_version']) is int and a['membership_version']>=1 and re.fullmatch('[a-f0-9]{32}',a['membership_id']) is not None,'Invalid correction owner review.')
        require(re.fullmatch('[a-f0-9]{64}',p['dependency_hash']) is not None,'Missing correction dependency fingerprint.')
        ctx=p['context'];require(set(ctx)=={'merge','original_alias','original_retained','merge_retained_fields','alias','retained','original_jobs','unrelated_jobs_staying','fields','already_corrected','errors','effect'} and ctx['already_corrected'] is None and ctx['errors']==[],'Correction context changed.')
        require(ctx['merge']=={k:original[k] for k in ('id','created_at','actor','reason','preview_hash')} and ctx['original_alias']==old['source'] and ctx['original_retained']==old['target'] and ctx['merge_retained_fields']==old['retained_fields'],'Original merge evidence was rewritten.')
        require(ctx['alias']['state']=='merged' and ctx['alias']['canonical_id']==r['retained_id'] and ctx['alias']['version']==old['source']['version']+1 and ctx['alias']['record_version']==old['source']['record_version']+1,'Correction no longer refers to the original retired alias.')
        require(ctx['retained']['state']=='active' and ctx['retained']['canonical_id'] is None,'Correction retained customer was not active.')
        values={}
        for k,choice in selection.fields.model_dump().items():
            f=ctx['fields'][k];require(f=={'before_merge':old['target'][k],'merge_retained':old['retained_fields'][k],'current':ctx['retained'][k],'can_restore':ctx['retained'][k]==old['retained_fields'][k]},'Contact restoration evidence changed.')
            require(choice=='keep_current' or f['can_restore'],'Correction overwrote a later contact value.')
            values[k]=f['current'] if choice=='keep_current' else f['before_merge']
        require(set(ctx['fields'])==set(CONTACT_FIELDS) and p['retained_fields']==values,'Correction saved different contact choices.')
        afters={}
        for side,action,version in [('alias','merge_correction.reactivate',selection.alias_version),('retained','merge_correction.retain',selection.retained_version)]:
            cid=getattr(selection,side+'_id');hs=histories[cid];matches=[(i,h,v) for i,(h,v) in enumerate(hs) if h['action']==action and v['contact']['version']==version+1]
            require(len(matches)==1,'Correction has no unique customer state event.');i,h,v=matches[0]
            require(i>0 and safe(hs[i-1][1])==ctx[side] and ctx[side]['version']==version and h['actor']==r['actor'] and h['reason']==r['reason'] and r['created_at']<=h['created_at']<=r['completed_at'],'Correction customer event differs from review.')
            before=hs[i-1][1];expected={'record':{**before['record'],'version':before['record']['version']+1,'state':'active','canonical_id':None},'contact':{**before['contact'],'version':version+1}}
            if side=='retained':expected['contact'].update(values)
            else:require(all(ctx[side][k]==old['source'][k] for k in CONTACT_FIELDS),'Original alias contact values changed.')
            require(v==expected,'Correction overwrote unrelated customer data.');afters[side]=safe(v);actions.add(h['id'])
        inv=p['work_review'];require(set(inv)=={'jobs','associations','documents','sharing_to_pause','errors','ownership_chains'} and inv['errors']==[],'Correction work inventory changed.')
        ids=sorted(selection.job_ids);original_ids=[j['id'] for j in old['jobs']]
        require([x['original_job'] for x in ctx['original_jobs']]==old['jobs'] and set(ids)<=set(original_ids),'Correction included work outside the original merge.')
        expected_scope='reactivation_only' if not ids else 'all_original_jobs' if set(ids)==set(original_ids) else 'selected_original_jobs'
        require(p['scope']==expected_scope and [j['id'] for j in inv['jobs']]==ids,'Correction scope does not match its selected jobs.')
        selected={j['id']:j for j in inv['jobs']}
        for x in ctx['original_jobs']:
            require(set(x)=={'original_job','current_job','ownership_chain','eligible','blockers'},'Unexpected original job context.')
            j=x['current_job'];job_evidence(j);prior=[m for m in chains[j['id']] if m['after_version']<=j['version']]
            require(x['ownership_chain']==prior and any(m['transfer_id']==r['merge_id'] for m in prior) and prior[-1]['to_customer_id']==j['customer_id'],'Correction lost the later ownership context.')
            require(x['eligible']==(not x['blockers']) and (j['customer_id']==r['retained_id'] or not x['eligible']),'Correction incorrectly offered a job now owned elsewhere.')
            if j['id'] in selected:require(x['eligible'] is True and x['blockers']==[] and j==selected[j['id']] and j['customer_id']==r['retained_id'] and inv['ownership_chains'][j['id']]==prior,'Correction selected an ineligible or different job version.')
        staying=ctx['unrelated_jobs_staying'];require(len({j['id'] for j in staying})==len(staying),'Duplicate staying job.')
        for j in staying:job_evidence(j);require(j['id'] not in original_ids and j['customer_id']==r['retained_id'],'Correction misrepresented unrelated retained work.')
        child_id=uuid5(UUID(r['id']),'customer-separation').hex if ids else None
        require(r['child_transfer_id']==child_id,'Correction ownership operation changed.')
        if ids:
            require(child_id in transfers,'Correction lost its new ownership moves.');child=transfers[child_id];cp=json.loads(child['plan'])
            require(child['kind']=='jobs' and child['source_id']==r['retained_id'] and child['target_id']==r['alias_id'] and child['actor']==r['actor'] and child['reason']==r['reason'] and r['created_at']<=child['created_at']<=r['completed_at'],'Correction child move is outside its reviewed scope.')
            require(cp['selection']=={'kind':'jobs','source_id':r['retained_id'],'target_id':r['alias_id'],'source_version':selection.retained_version+1,'target_version':selection.alias_version+1,'job_ids':selection.job_ids,'fields':{k:'target' for k in CONTACT_FIELDS}} and cp['source']==afters['retained'] and cp['target']==afters['alias'] and all(cp[k]==inv[k] for k in inv),'Correction child plan differs from its reviewed inventory.')
        else:require(inv['jobs']==[] and inv['documents']==[] and inv['associations']=={} and inv['ownership_chains']=={},'Reactivation-only review moved work.')
        require(out=={'id':r['id'],'merge_id':r['merge_id'],'alias_id':r['alias_id'],'retained_id':r['retained_id'],'scope':expected_scope,'job_ids':ids,'child_transfer_id':child_id,'alias_version':selection.alias_version+1+bool(ids),'retained_version':selection.retained_version+1+bool(ids),'portal_access':'Both revoked; fresh exact-customer invitation required.'},'Correction result does not match the saved operation.')
        pause=inv['sharing_to_pause'];require(set(pause)=={'portal_customer_ids','photo_ids','gallery_ids','feedback_ids','photo_versions','gallery_versions'} and pause['portal_customer_ids']==[r['retained_id'],r['alias_id']],'Correction sharing scope changed.')
        window=lambda h:r['created_at']<=h['created_at']<=r['completed_at'] and h.get('actor',h.get('actor_id'))==r['actor']
        for cid in (r['alias_id'],r['retained_id']):require(any(window(h) and h['customer_id']==cid and h['action']=='revoked' for h in rows('portal_events')),'Correction did not revoke both portal accounts.')
        for pid in pause['photo_ids']:
            history=[h for h in rows('photo_history') if h['photo_id']==pid];before=[json.loads(h['snapshot']) for h in history if json.loads(h['snapshot'])['version']==pause['photo_versions'][pid]]
            require(len(before)==1,'Correction photo review evidence missing.')
            if before[0]['reviewed'] or before[0]['customer_share'] is not None:require(any(window(h) and h['action']=='photo.permission_suspended' and json.loads(h['snapshot'])=={**before[0],'version':before[0]['version']+1,'updated_at':json.loads(h['snapshot'])['updated_at'],'customer_share':None,'reviewed':0} for h in history),'Correction did not suspend photo sharing.')
        for gid in pause['gallery_ids']:require(any(window(h) and h['project_id']==gid and h['action']=='project.withdraw' and h['version']==pause['gallery_versions'][gid]+1 and json.loads(h['snapshot'])['published'] is None for h in rows('gallery_history')),'Correction did not withdraw a public gallery.')
        require(sorted(x['id'] for x in p['feedback_review'])==sorted(pause['feedback_ids']),'Correction feedback inventory changed.')
        for before in p['feedback_review']:
            require(set(before)=={'id','customer_id','state','version','access_version'} and before['customer_id'] in (r['alias_id'],r['retained_id']),'Unexpected feedback review scope.')
            hs=[h for h in rows('feedback_history') if h['request_id']==before['id']]
            saved=[json.loads(h['snapshot'])['request'] for h in hs if h['version']==before['version']]
            require(len(saved)==1 and {k:saved[0][k] for k in before}==before,'Correction feedback review is missing.')
            if before['state'] in ('pending','responded'):require(any(window(h) and h['action']=='record.changed' and h['version']==before['version']+1 and json.loads(h['snapshot'])['request']['state']=='revoked' for h in hs),'Correction did not revoke old feedback permission.')

    require(actions=={h['id'] for hs in histories.values() for h,v in hs if h['action'].startswith('merge_correction.')},'A customer reactivation has no unique reviewed correction.')
    return len(corrections)
