"""Strict format-10 operational state and history validation."""
import json,re,sqlite3
from datetime import datetime
from .store import digest
from .workflow_models import Metadata,Settings,calendar_day
from .workflow import JOB_FIELDS

TABLES=('opportunity_history','opportunity_reviews','opportunity_settings_history','opportunity_settings','job_workflow_history','job_workflow')

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+' ORDER BY rowid')]
        jobs={j['id']:j for j in rows('jobs')};customers={c['id']:c for c in rows('customers')};quotes={q['id']:q for q in rows('quotes')}
        def stamp(value):
            require(isinstance(value,str) and datetime.fromisoformat(value).tzinfo is not None,'Invalid workflow timestamp.')
        def integer(value,minimum=1):return type(value) is int and value>=minimum
        def meta(value):
            validated=Metadata.model_validate(value).model_dump();require(validated==value,'Noncanonical workflow metadata.');return validated
        ws={w['job_id']:w for w in rows('job_workflow')};require(set(ws)==set(jobs),'Every job must have exactly one operational baseline.')
        for jid,w in ws.items():
            m=meta(json.loads(w['content']));require(integer(w['version']) and integer(w['feedback_version']) and w['feedback_version']<=jobs[jid]['version'],'Invalid workflow/basis version.')
            history=[h for h in rows('job_workflow_history') if h['job_id']==jid]
            require([h['version'] for h in history]==list(range(1,w['version']+1)),'Incomplete workflow history.')
            previous_snapshot=None
            for h in history:
                stamp(h['created_at']);v=json.loads(h['snapshot']);require(set(v)=={'job','metadata','feedback_version','version'} and set(v['job'])==set(JOB_FIELDS),'Unexpected workflow history fields.')
                historical_meta=meta(v['metadata']);j=v['job'];require(j['id']==jid and j['customer_id'] in customers and integer(j['version']) and j['version']==h['job_version'] and j['version']<=jobs[jid]['version'] and v['version']==h['version'],'Workflow history has inconsistent exact links or versions.')
                require(not historical_meta['archived'] or j['status'] in ('completed','cancelled'),'An archived job has an open stage.')
                require(not historical_meta['completed_date'] or j['status']=='completed','Completion date belongs to an open/cancelled job.')
                require(j['status'] in ('new','site_visit','accepted','scheduled','in_progress','completed','on_hold','cancelled') and all(isinstance(j[k],str) for k in ('notes','service','message','scheduled_date')) and len(j['notes'])<=6000,'Invalid job snapshot.')
                calendar_day(j['scheduled_date']);require(integer(v['feedback_version']) and v['feedback_version']<=j['version'],'Invalid feedback basis history.')
                if previous_snapshot:
                    old=previous_snapshot;material=any(j[k]!=old['job'][k] for k in ('status','scheduled_date','service','message','customer_id')) or any(v['metadata'][k]!=old['metadata'][k] for k in ('completed_date','archived'))
                    # A guarded customer relink can advance the job separately.
                    require(j['version']>old['job']['version'],'Workflow job versions did not advance.')
                    require(v['feedback_version']==(j['version'] if material else old['feedback_version']),'Harmless/material feedback basis transition is inconsistent.')
                else:require(h['actor']=='initialisation' and v['feedback_version']==j['version'] and v['metadata']==Metadata().model_dump(),'Invalid observed migration/creation baseline.')
                previous_snapshot=v
            require(previous_snapshot['metadata']==m and previous_snapshot['feedback_version']==w['feedback_version'],'Current metadata differs from its retained history.')
            require(all(previous_snapshot['job'][k]==jobs[jid][k] for k in JOB_FIELDS if k!='version'),'Job differs from its saved workflow history.')
        cfg=rows('opportunity_settings');require(len(cfg)==1 and cfg[0]['id']==1 and integer(cfg[0]['version']),'Invalid opportunity settings.')
        content=json.loads(cfg[0]['content']);require(Settings.model_validate(content).model_dump()==content,'Invalid canonical opportunity settings.')
        sh=rows('opportunity_settings_history');require([h['version'] for h in sh]==list(range(2,cfg[0]['version']+1)),'Incomplete opportunity settings history.')
        for h in sh:
            v=json.loads(h['content']);require(Settings.model_validate(v).model_dump()==v,'Noncanonical settings history.');stamp(h['created_at'])
        require((sh[-1]['content'] if sh else json.dumps(Settings().model_dump()))==cfg[0]['content'],'Settings differ from retained history.')
        reviews={r['id']:r for r in rows('opportunity_reviews')};histories=rows('opportunity_history');require(len(reviews)<=10000 and len(histories)<=100000,'Operational history exceeds supported bounds.')
        context_keys={'id','kind','generation','job_id','customer_id','quote_id','customer_name','service','reference','source_version','reason','age_days','total_pence','due_date','related_job_id','postcode_group','eligible','job_version','customer_version','settings_version','related_version'}
        from .customer_review import historical_owner
        def check_review(r):
            require(r['kind'] in ('next_action','quote','repeat','review','postcode') and r['job_id'] in jobs and r['customer_id'] in customers and r['state'] in ('open','prepared','snoozed','dismissed','completed') and integer(r['version']),'Invalid opportunity identity/state.')
            require(historical_owner(db,r['job_id'],r['customer_id']),'Opportunity job belongs to a different exact customer.')
            require(isinstance(r['generation'],str) and 1<=len(r['generation'])<=180,'Invalid opportunity generation.')
            source=r['quote_id'] or r['job_id'];require(r['id']==digest(r['kind']+':'+source+':'+r['customer_id']+':'+r['generation'])[:32],'Opportunity identity differs from exact source generation.')
            if r['quote_id']:require(r['kind']=='quote' and r['quote_id'] in quotes and quotes[r['quote_id']]['job_id']==r['job_id'] and historical_owner(db,quotes[r['quote_id']]['job_id'],r['customer_id']),'Opportunity quote belongs to another exact job/customer.')
            else:require(r['kind']!='quote','Quote opportunity has no quote.')
            stamp(r['updated_at']);calendar_day(r['snooze_until']);require(bool(r['snooze_until'])==(r['state']=='snoozed'),'Invalid snooze state/date.')
            require(isinstance(r['draft'],str) and len(r['draft'])<=3000 and isinstance(r['note'],str) and len(r['note'])<=1000,'Invalid review text.')
            c=json.loads(r['context']);require(set(c)==context_keys and all(c[k]==r[k] for k in ('id','kind','generation','job_id','customer_id','quote_id')) and c['eligible'] is True,'Invalid reviewed source facts.')
            require(integer(c['job_version']) and c['job_version']<=jobs[r['job_id']]['version'] and integer(c['customer_version']) and c['customer_version']<=customers[r['customer_id']]['version'] and integer(c['settings_version']) and c['settings_version']<=cfg[0]['version'],'Reviewed versions exceed saved source versions.')
            require(integer(c['source_version']) and c['source_version']<=(quotes[r['quote_id']]['version'] if r['quote_id'] else jobs[r['job_id']]['version']),'Invalid reviewed source version.')
            require(c['related_job_id'] is None or c['kind']=='postcode' and c['related_job_id'] in jobs and integer(c['related_version']) and c['related_version']<=jobs[c['related_job_id']]['version'],'Invalid postcode-group basis.')
            require((c['related_job_id'] is not None)==(c['kind']=='postcode') and (c['related_version'] is None)==(c['related_job_id'] is None),'Postcode source fields do not agree.')
            require(all(isinstance(c[k],str) and len(c[k])<=2000 for k in ('customer_name','service','reason','postcode_group')),'Invalid reviewed display facts.')
            require(c['age_days'] is None or type(c['age_days']) is int and c['age_days']>=0,'Invalid eligibility age.')
            require(c['total_pence'] is None or type(c['total_pence']) is int and c['total_pence']>=0,'Invalid quote estimate.')
            calendar_day(c['due_date'])
        for oid,r in reviews.items():
            check_review(r);history=[h for h in histories if h['opportunity_id']==oid];require([h['version'] for h in history]==list(range(1,r['version']+1)),'Incomplete opportunity review history.')
            prior=None
            for h in history:
                stamp(h['created_at']);v=json.loads(h['snapshot']);require(set(v)==set(r) and v['id']==oid and v['version']==h['version'],'Review history points at another item/version.');check_review(v)
                states={'prepare':'prepared','snooze':'snoozed','dismiss':'dismissed','complete':'completed','reopen':'open'}
                require(h['action'] in states and v['state']==states[h['action']],'Review action/state mismatch.')
                require(all(v[k]==r[k] for k in ('id','kind','job_id','customer_id','quote_id','generation')),'Review source identity changed.')
                if prior:
                    require(prior['state'] not in ('dismissed','completed') or h['action']=='reopen','Closed review changed without reopening.')
                    if h['action']!='prepare':require(v['draft']==prior['draft'],'Draft changed without preparation.')
                elif h['action']!='prepare':require(not v['draft'],'A first review has an unprepared draft.')
                if h['action']=='reopen':require(prior and prior['state'] in ('dismissed','completed','snoozed'),'Review reopened without a closed or snoozed predecessor.')
                if h['action']=='prepare':require(len(v['draft'].strip())>=5,'Prepared draft is empty.')
                if h['action'] in ('snooze','dismiss','complete'):require(len(v['note'].strip())>=5,'Review reason is missing.')
                prior=v
            require(prior==r,'Review differs from its last history event.')
        require(all(h['opportunity_id'] in reviews for h in histories),'Orphan opportunity history.')
        return {'job_baselines':len(ws),'job_history':len(rows('job_workflow_history')),'reviews':len(reviews),'review_history':len(histories),'settings_version':cfg[0]['version']}
    finally:db.row_factory=previous
