"""Format 9 validates exact feedback links, authors and consent transitions.

Histories are evidence of local actions, not signatures or external identity
verification. The SHA checksum detects changed bytes; it is not authenticity.
"""
import json,math,re,sqlite3
from datetime import datetime
from .feedback_models import Answers,Consent,CONSENT_VERSION
from .feedback import snapshot,projection,record,suspend,current_links
from .store import many


def validate(db,require,*,sanitised=False):
    previous=db.row_factory;db.row_factory=sqlite3.Row
    try:
        rows=lambda table:[dict(r) for r in db.execute('SELECT * FROM '+table+' ORDER BY rowid')]
        customers={r['id']:r for r in rows('customers')};jobs={r['id']:r for r in rows('jobs')}
        requests={r['id']:r for r in rows('feedback_requests')}
        require(len(requests)<=5000,'Feedback request limit exceeded.')
        hid=lambda value:isinstance(value,str) and re.fullmatch(r'[a-f0-9]{32}',value) is not None
        def stamp(value):
            try:return isinstance(value,str) and datetime.fromisoformat(value).tzinfo is not None
            except (TypeError,ValueError):return False
        from .customer_review import historical_owner
        def valid_request(r):
            require(hid(r['id']) and r['job_id'] in jobs and r['customer_id'] in customers and historical_owner(db,r['job_id'],r['customer_id']),'Feedback request has an inconsistent exact job/customer link.')
            require(all(type(r[k]) is int and r[k]>=1 for k in ('customer_version','job_version','version','access_version')) and r['customer_version']<=customers[r['customer_id']]['version'] and r['job_version']<=jobs[r['job_id']]['version'],'Invalid feedback record versions.')
            require(r['state'] in ('pending','responded','revoked','archived') and isinstance(r['expires'],(int,float)) and math.isfinite(r['expires']) and stamp(r['created_at']),'Invalid feedback state or expiry.')
            require(isinstance(r['delivery_email'],str) and len(r['delivery_email'])<=180 and re.fullmatch(r'[^\s@]+@[^\s@]+\.example',r['delivery_email']) is not None and isinstance(r['service'],str) and len(r['service'])<=120 and isinstance(r['reason'],str) and len(r['reason'])<=500 and hid(r['prepared_by']),'Invalid feedback request preparation.')
        # Demo owners have stable word IDs; allow those through the separate registry binding check.
        def request_check(r):
            prepared=r['prepared_by'];copy={**r,'prepared_by':'0'*32};valid_request(copy)
            require(prepared in ('oak','harbour') or hid(prepared),'Invalid feedback preparation author.')
        def state_check(v):
            require(isinstance(v,dict) and set(v)=={'request','response','consent','publication'},'Unsupported feedback history fields.')
            r=v['request'];require(set(r)==set(requests[r['id']]),'Unsupported feedback request fields.');request_check(r)
            response=v['response'];c=v['consent'];p=v['publication']
            require((response is None)==(c is None),'Response and consent must be recorded together.')
            require(not response or r['state']!='pending','Answered feedback cannot be pending.')
            require(response is not None or r['state']!='responded','Responded request has no original answers.')
            if response:
                require(set(response)=={'request_id','answers','actor','submitted_version','created_at'} and response['request_id']==r['id'] and stamp(response['created_at']) and type(response['submitted_version']) is int and 1<=response['submitted_version']<r['version'],'Invalid feedback submission evidence.')
                require(response['actor'] in ('portal:'+r['customer_id'],'feedback-token:'+r['id']),'Feedback response author is outside its exact scope.')
                Answers.model_validate(response['answers'])
                require(set(c)=={'request_id','state','display_name','statement','share_rating','version','wording_version','created_at'} and c['request_id']==r['id'] and c['state'] in ('private','approved','withdrawn','suspended') and c['wording_version']==CONSENT_VERSION and type(c['version']) is int and c['version']>=1 and c['share_rating'] in (0,1) and stamp(c['created_at']),'Invalid public consent evidence.')
                Consent.model_validate({'allow_public':c['state'] in ('approved','suspended'),'display_name':c['display_name'],'statement':c['statement'],'share_rating':bool(c['share_rating']),'wording_version':c['wording_version']})
            if p:
                require(response is not None and set(p)=={'request_id','consent_version','projection','state','published_at'} and p['request_id']==r['id'] and p['state'] in ('published','hidden') and type(p['consent_version']) is int and 1<=p['consent_version']<=c['version'] and stamp(p['published_at']),'Invalid testimonial publication evidence.')
                require(isinstance(p['projection'],dict) and set(p['projection'])=={'display_name','statement','rating'},'Public testimonial contains unsupported/private fields.')
                if p['state']=='published':require(r['state']=='responded' and c['state']=='approved' and p['consent_version']==c['version'] and p['projection']==projection(c,response),'Public testimonial lacks matching current permission.')
        actions={'prepared','reissued','submitted','consent.approved','consent.withdrawn','published','hidden','revoked','archived','record.changed','recovery.revoked'}
        histories={rid:[] for rid in requests}
        for h in rows('feedback_history'):
            require(h['request_id'] in requests and h['action'] in actions and stamp(h['created_at']) and isinstance(h['detail'],str) and len(h['detail'])<=1000,'Invalid feedback history.')
            v=json.loads(h['snapshot']);require(v.get('request',{}).get('id')==h['request_id'],'Feedback history points at another request.');state_check(v)
            require(v['request']['version']==h['version'],'Feedback history version mismatch.')
            histories[h['request_id']].append((h,v))
        immutable=('id','customer_id','job_id','customer_version','job_version','delivery_email','service','reason','prepared_by','created_at')
        for rid,r in requests.items():
            request_check(r);entries=histories[rid]
            require(jobs[r['job_id']]['customer_id']==r['customer_id'] or r['state'] in ('revoked','archived'),'Transferred feedback remains usable under its former identity.')
            require([h['version'] for h,v in entries]==list(range(1,r['version']+1)) and bool(entries),'Feedback history is incomplete.')
            current=snapshot(db,rid);state_check(current);require(entries[-1][1]==current,'Feedback does not match its last recorded history.')
            first_h,first=entries[0]
            require(first_h['action']=='prepared' and first_h['actor']==r['prepared_by'] and first['request']['state']=='pending' and first['request']['access_version']==1 and all(first[k] is None for k in ('response','consent','publication')),'Feedback has no original preparation event.')
            prev=first;published=[]
            for h,v in entries:
                req=v['request'];c=v['consent'];p=v['publication'];a=h['action']
                require(all(req[k]==r[k] for k in immutable),'Feedback history changes an immutable customer/job binding.')
                if a=='published':published.append(p)
                if p:require(any({**old,'state':p['state']}==p for old in published),'Publication has no matching exact consent publication event.')
                if h['version']==1:continue
                require(a!='prepared','Repeated feedback preparation event.')
                old=prev['request'];oldc=prev['consent'];oldp=prev['publication']
                require(req['version']==old['version']+1,'Feedback version skipped.')
                access_change=a in ('reissued','revoked','archived','record.changed','recovery.revoked')
                require(req['access_version']==old['access_version']+int(access_change),'Feedback credential generation is inconsistent.')
                if a!='reissued':require(req['expires']==old['expires'],'Feedback expiry changed without a reissue.')
                if a=='submitted':
                    require(old['state']=='pending' and req['state']=='responded' and prev['response'] is None and v['response'] is not None and v['response']['submitted_version']==old['version'] and h['actor']==v['response']['actor'] and c['version']==1 and c['state'] in ('private','approved'),'Invalid feedback submission transition.')
                else:require(v['response']==prev['response'],'Original customer feedback was edited or deleted.')
                if a in ('consent.approved','consent.withdrawn'):
                    require(oldc is not None and c['version']==oldc['version']+1 and c['state']==('approved' if a=='consent.approved' else 'withdrawn') and h['actor'] in ('portal:'+r['customer_id'],'feedback-token:'+rid),'Consent was changed by the wrong actor or version.')
                    require(req['state']==old['state'] and (a!='consent.approved' or req['state']=='responded'),'Consent reopened a closed request.')
                elif a in ('revoked','archived','record.changed','recovery.revoked'):
                    expected=({**oldc,'state':'suspended','version':oldc['version']+1,'created_at':c['created_at']} if oldc and oldc['state']=='approved' else oldc)
                    require(c==expected,'Suspension changed customer-approved wording.')
                elif a!='submitted':require(c==oldc,'Consent changed without a customer action.')
                if a=='reissued':require(old['state']!='archived' and req['state']==('responded' if v['response'] else 'pending'),'Invalid feedback reissue.')
                elif a=='archived':require(req['state']=='archived','Invalid archive state.')
                elif a in ('revoked','record.changed'):require(req['state']=='revoked','Invalid revocation state.')
                elif a=='recovery.revoked':require(req['state']==('archived' if old['state']=='archived' else 'revoked') and h['actor']=='recovery','Invalid recovery state.')
                elif a!='submitted':require(req['state']==old['state'],'Feedback state changed without a matching action.')
                if a=='published':require(p and p['state']=='published' and req['state']=='responded','Publication action has no approved projection.')
                elif a in ('hidden','consent.approved','consent.withdrawn','revoked','archived','record.changed','recovery.revoked'):require(p==({**oldp,'state':'hidden'} if oldp else None),'Withdrawal did not preserve and hide the prior projection.')
                else:require(p==oldp,'Public projection changed without publication.')
                prev=v
            if current['publication'] and current['publication']['state']=='published':require(current_links(db,r),'Published feedback is no longer bound to the approved customer/job versions.')
        tokens=rows('feedback_tokens')
        if sanitised:require(not tokens,'Recovery contains feedback credentials.')
        for t in tokens:
            require(hid(t['id']) and t['request_id'] in requests and isinstance(t['token_hash'],str) and re.fullmatch(r'[a-f0-9]{64}',t['token_hash']) and type(t['access_version']) is int and 1<=t['access_version']<=requests[t['request_id']]['access_version'] and isinstance(t['expires'],(int,float)) and math.isfinite(t['expires']) and stamp(t['created_at']),'Invalid feedback credential.')
        return {'requests':len(requests),'responses':len(rows('feedback_responses')),'history':sum(map(len,histories.values())),'published':len([v for v in rows('feedback_publications') if v['state']=='published'])}
    finally:db.row_factory=previous


def sanitise(db):db.execute('DELETE FROM feedback_tokens')


def restored(store):
    with store.registry() as db:bids=[r[0] for r in db.execute('SELECT id FROM businesses')]
    for bid in bids:
        with store.tenant(bid) as db:
            db.execute('BEGIN IMMEDIATE');sanitise(db)
            for r in many(db,'SELECT * FROM feedback_requests'):
                db.execute("UPDATE feedback_requests SET state=CASE WHEN state='archived' THEN 'archived' ELSE 'revoked' END,version=version+1,access_version=access_version+1 WHERE id=?",(r['id'],));suspend(db,r['id']);record(db,r['id'],'recovery.revoked','recovery','Credentials removed and publication paused. Explicit renewed customer permission and owner publication are required.')
