"""Validate saved photo relationships without executing archive-supplied code."""
import json
import sqlite3
from . import photo_models as m


def validate(db,require):
    previous=db.row_factory;db.row_factory=sqlite3.Row
    try:
        rows=lambda table:[dict(r) for r in db.execute('SELECT * FROM '+table)]
        jobs={r['id']:r for r in rows('jobs')};assets={r['id'] for r in rows('assets')}
        photos={r['id']:r for r in rows('job_photos')};blobs={r['id']:r for r in rows('photo_blobs')}
        projects={r['id']:r for r in rows('gallery_projects')}
        require(len(photos)<=1000 and len(projects)<=100,'Local media record limit exceeded.')
        import re
        identity=lambda v:isinstance(v,str) and re.fullmatch('[a-f0-9]{32}',v) is not None
        require(all(identity(i) for i in [*photos,*blobs,*projects]),'Invalid media record identity.')
        require({p['blob_id'] for p in photos.values()}==set(blobs) and len(photos)==len(blobs),'Private image inventory is inconsistent.')
        for b in blobs.values():
            require(re.fullmatch('[a-f0-9]{64}',b['sha256']) is not None and type(b['bytes']) is int and 0<b['bytes']<=16*1024*1024 and all(type(b[k]) is int and 0<b[k]<=1800 for k in ('width','height')),'Invalid private image metadata.')
        from .customer_review import historical_owner
        def photo(p):
            require(p['job_id'] in jobs and historical_owner(db,p['job_id'],p['customer_id']) and p['blob_id'] in blobs,'Photo job/customer binding is invalid.')
            m.PhotoEdit.model_validate({k:p[k] for k in m.PhotoEdit.model_fields})
            require(p['archived'] in (0,1) and p['reviewed'] in (0,1),'Invalid photo state.')
            if p['customer_share'] is not None:m.PublicPhotoText.model_validate_json(p['customer_share'])
            require(not p['archived'] or (not p['reviewed'] and p['customer_share'] is None),'Archived photos must remain private and unreviewed.')
        for p in photos.values():photo(p)
        ph={i:[] for i in photos}
        for h in rows('photo_history'):
            require(h['photo_id'] in photos and h['action'] in ('photo.uploaded','photo.metadata_updated','photo.customer_shared','photo.customer_unshared','photo.customer_transferred','photo.permission_suspended','photo.enquiry_recovery_suspended','photo.review','photo.archive','photo.restore'),'Invalid photo history.')
            snap=json.loads(h['snapshot']);require(set(snap)==set(photos[h['photo_id']]),'Invalid photo history fields.');photo(snap)
            require(all(snap[k]==photos[h['photo_id']][k] for k in ('id','job_id','blob_id','uploaded_by','membership_id','created_at')),'Photo history changes an immutable binding.')
            ph[h['photo_id']].append((h['id'],snap))
        for pid,history in ph.items():
            ordered=[s for _,s in sorted(history)];require(bool(ordered) and [s['version'] for s in ordered]==list(range(1,photos[pid]['version']+1)) and ordered[-1]==photos[pid],'Photo history is incomplete.')
        image_links={}
        def project(p):
            require(p['source_job_id'] is None or p['source_job_id'] in jobs,'Project source job is missing.')
            require(type(p['version']) is int and p['version']>=1 and p['archived'] in (0,1) and (not p['archived'] or p['published'] is None),'Invalid project state.')
            for key in ('draft','published'):
                if p[key] is None:continue
                content=m.ProjectContent.model_validate_json(p[key]).model_dump();pairs={}
                if key=='draft':require(content['slug']==p['draft_slug'],'Project draft address is inconsistent.')
                for i in content['images']:
                    if i['source_kind']=='job':require(i['source_id'] in photos and photos[i['source_id']]['job_id']==p['source_job_id'],'Project photo belongs to another job.')
                    else:require(p['source_job_id'] is None and i['source_id'] in assets,'Project website image is outside its scope.')
                    link=(p['id'],i['id']);source=(i['source_kind'],i['source_id']);require(link not in image_links or image_links[link]==source,'Gallery image identity changed source.');image_links[link]=source
                    if i['pair_id']:pairs.setdefault(i['pair_id'],set()).add(i['phase'])
                if key=='published':require(bool(content['images']) and all(i['alt'] for i in content['images']) and all(v=={'before','after'} for v in pairs.values()),'Published gallery has undescribed or incomplete images.')
        for p in projects.values():project(p)
        gh={i:[] for i in projects}
        for h in rows('gallery_history'):
            require(h['project_id'] in projects and h['action'] in ('project.created','project.draft_saved','project.published','project.withdraw','project.enquiry_recovery_withdraw','project.archive','project.restore','project.restored_to_draft'),'Invalid gallery history.')
            require(h['permission_confirmed']==int(h['action']=='project.published'),'Publication permission acknowledgement is missing or inconsistent.')
            snap=json.loads(h['snapshot']);current=projects[h['project_id']];require(set(snap)==set(current),'Invalid project history fields.');project(snap)
            require(all(snap[k]==current[k] for k in ('id','source_job_id','created_at')) and h['version']==snap['version'],'Project history changes its source or version.')
            gh[h['project_id']].append((h['id'],snap,h['action']))
        slugs={}
        for pid,history in gh.items():
            ordered=sorted(history);p=projects[pid]
            require(bool(ordered) and [s['version'] for _,s,_ in ordered]==list(range(1,p['version']+1)) and ordered[-1][1]==p,'Project history is incomplete.')
            for slug in {p['draft_slug'],*( [json.loads(p['published'])['slug']] if p['published'] else [])}:
                require(slug not in slugs or slugs[slug]==pid,'Project addresses overlap.');slugs[slug]=pid
            if p['published']:
                require(any(action=='project.published' and s['published']==p['published'] for _,s,action in ordered),'Public snapshot lacks a reviewed publication event.')
                require(all(i['source_kind']!='job' or not photos[i['source_id']]['archived'] for i in json.loads(p['published'])['images']),'An archived photo is still public.')
        return {i:{k:b[k] for k in ('sha256','bytes','width','height')} for i,b in blobs.items()}
    finally:db.row_factory=previous
