import math

POSITIVE_STATES={"SELECTED","TARGET_SPECIFIC"}
METHOD_PATHS={"PHOTOGRAMMETRY_MESH","NERF_VIEW_SYNTHESIS","3DGS_VIEW_SYNTHESIS","HYBRID_MESH_PLUS_VIEW_SYNTHESIS"}
VIEW_ONLY={"NERF_VIEW_SYNTHESIS","3DGS_VIEW_SYNTHESIS"}
ALLOWED_PATHS=METHOD_PATHS|{"RECAPTURE","HOLD","NONE"}
ALLOWED_STATES={"SELECTED","TARGET_SPECIFIC","REWORK_REQUIRED","HOLD","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN","METHOD_UNSUITABLE"}
ALLOWED_SCOPES={"STANDARD_DECLARED_TARGET","CONSTRAINED_NAMED_TARGET_ONLY","PENDING_TARGET_AUTHORITY"}
RESOLVED_PROVENANCE={"VERIFIED_RIGHTS_CLEARED","SELF_CREATED","PUBLIC_DOMAIN"}
UNRESOLVED_TOKENS={"","UNKNOWN","UNRESOLVED","TO_BE_ESTABLISHED","TBD","PENDING"}
FIELD_DOMAINS={
 "subject_type":{"SMALL_TEXTURED_ROOM","GLOSSY_WEAK_TEXTURE_OBJECT"},
 "overlap_quality":{"HIGH","MEDIUM","LOW"},
 "lighting_consistency":{"PASS","FAIL","WARN_SPECULAR_CHANGE"},
 "camera_pose_source":{"COLMAP_SFM","TO_BE_ESTABLISHED","UNKNOWN"},
 "sfm_status":{"PASS","FAIL","NOT_RUN"},
 "mvs_status":{"PASS","FAIL","NOT_RUN"},
 "dense_output_type":{"DENSE_POINTS_AND_EDITABLE_MESH","NONE","UNKNOWN"},
 "nerf_representation":{"CONTINUOUS_VOLUMETRIC_RADIANCE_FIELD","NOT_TRAINED","NONE","UNKNOWN"},
 "novel_view_quality":{"PASS","FAIL","NOT_EVALUATED","UNKNOWN"},
 "gs_render_target":{"INTERACTIVE_NOVEL_VIEW","NOT_TRAINED","NONE","UNKNOWN"},
 "geometry_extractability":{"EDITABLE_MESH_AVAILABLE_FROM_MVS","NONE","UNKNOWN"},
 "editability_requirement":{"REQUIRED","OPTIONAL","NOT_REQUIRED"},
 "metric_scale_requirement":{"REQUIRED","OPTIONAL","NOT_REQUIRED"},
 "metric_scale_evidence":{"PASS","FAIL","UNKNOWN","NOT_REQUIRED"},
 "hardware_constraint":{"CUDA_WORKSTATION","CPU_ONLY_CONSTRAINED","NO_COMPATIBLE_ACCELERATOR"},
 "downstream_target":{"INTERACTIVE_WALKTHROUGH_AND_EDITABLE_GEOMETRY","METRIC_SCALE_EDITABLE_MESH","METRIC_SCALE_EDITABLE_OBJECT_ASSET"},
}
ALLOWED_FAILURE_MODES={
 "INSUFFICIENT_OVERLAP_FOR_STABLE_POSE_RECOVERY",
 "METRIC_SCALE_NOT_VERIFIED",
 "REFERENCE_ACCELERATOR_PATH_UNAVAILABLE",
 "SPECULARITY_RISK",
 "WEAK_TEXTURE_RISK",
 "METRIC_SCALE_UNPROVEN",
 "CAPTURE_LIGHTING_FAILURE",
}
METHOD_BINDINGS={
 "PHOTOGRAMMETRY_SFM_MVS":{
   "evidence_ref":"source://D3D-003/COLMAP",
   "claim_ref":"lesson://D3D-003#claim/D3D-003-K1",
   "authority_class":"PRIMARY_OFFICIAL",
   "output_representation":"SPARSE_PLUS_DENSE_GEOMETRY",
   "downstream_fit":"EDITABLE_GEOMETRY",
 },
 "NERF":{
   "evidence_ref":"source://D3D-003/NERF",
   "claim_ref":"lesson://D3D-003#claim/D3D-003-K2",
   "authority_class":"ACADEMIC_ORIGINAL",
   "output_representation":"CONTINUOUS_VOLUMETRIC_RADIANCE_FIELD",
   "downstream_fit":"NOVEL_VIEW_SYNTHESIS",
 },
 "3D_GAUSSIAN_SPLATTING":{
   "evidence_ref":"source://D3D-003/3DGS",
   "claim_ref":"lesson://D3D-003#claim/D3D-003-K3",
   "authority_class":"ACADEMIC_AUTHORS_REFERENCE",
   "output_representation":"OPTIMIZED_3D_GAUSSIANS",
   "downstream_fit":"REALTIME_NOVEL_VIEW",
 },
 "MAKNOW_DECISION_POLICY":{
   "evidence_ref":"lesson://D3D-003#claim/D3D-003-K4",
   "claim_ref":"lesson://D3D-003#claim/D3D-003-K4",
   "authority_class":"MAKNOW_DESIGN",
   "output_representation":"DECISION_POLICY",
   "downstream_fit":"BOUNDED_METHOD_SELECTION",
 },
}
PATH_STATE={
 "PHOTOGRAMMETRY_MESH":{"SELECTED","TARGET_SPECIFIC","REWORK_REQUIRED","METHOD_UNSUITABLE","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "NERF_VIEW_SYNTHESIS":{"SELECTED","TARGET_SPECIFIC","REWORK_REQUIRED","METHOD_UNSUITABLE","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "3DGS_VIEW_SYNTHESIS":{"SELECTED","TARGET_SPECIFIC","REWORK_REQUIRED","METHOD_UNSUITABLE","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "HYBRID_MESH_PLUS_VIEW_SYNTHESIS":{"SELECTED","TARGET_SPECIFIC","REWORK_REQUIRED","METHOD_UNSUITABLE","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "RECAPTURE":{"REWORK_REQUIRED","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "HOLD":{"HOLD","EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN"},
 "NONE":{"EVIDENCE_INCOMPLETE","PROVENANCE_UNKNOWN","METHOD_UNSUITABLE"}}

def _finite(v): return isinstance(v,(int,float)) and not isinstance(v,bool) and math.isfinite(v)
def _rowmap(pack): return {r.get('method'):r for r in pack.get('method_evidence',[]) if isinstance(r,dict) and isinstance(r.get('method'),str)}
def _traceable(row):
    return isinstance(row,dict) and all(isinstance(row.get(k),str) and row[k].strip() for k in ['evidence_ref','claim_ref','authority_class','output_representation','downstream_fit'])

def method_binding_issues(pack):
    issues=[]
    rows=pack.get('method_evidence')
    if not isinstance(rows,list): return ['method_evidence must be a list']
    seen=set()
    for row in rows:
        if not isinstance(row,dict):
            issues.append('method_evidence rows must be objects'); continue
        method=row.get('method')
        if method not in METHOD_BINDINGS:
            issues.append('method_evidence method is outside the canonical registry'); continue
        if method in seen:
            issues.append(f'duplicate method_evidence row: {method}')
        seen.add(method)
        expected=METHOD_BINDINGS[method]
        for key,val in expected.items():
            if row.get(key)!=val: issues.append(f'method_evidence binding mismatch for {method}: {key}')
    return list(dict.fromkeys(issues))

def method_issues(pack,path):
    issues=[]; rows=_rowmap(pack)
    def need(method):
        if method not in rows or not _traceable(rows.get(method)): issues.append(f'traceable method evidence required: {method}')
    if path=='PHOTOGRAMMETRY_MESH':
        need('PHOTOGRAMMETRY_SFM_MVS')
        if pack.get('mvs_status')!='PASS' or pack.get('dense_output_type') in {'NONE','UNKNOWN',''} or pack.get('geometry_extractability') in {'NONE','UNKNOWN',''}:
            issues.append('photogrammetry mesh requires PASS MVS plus explicit dense/geometry evidence')
    elif path=='NERF_VIEW_SYNTHESIS':
        need('NERF')
        if pack.get('nerf_representation') in {'NOT_TRAINED','UNKNOWN','NONE',''} or not _finite(pack.get('nerf_training_budget_minutes')) or pack.get('nerf_training_budget_minutes')<=0:
            issues.append('NeRF positive path requires trained representation evidence')
        if pack.get('novel_view_quality')!='PASS': issues.append('NeRF positive path requires PASS novel-view quality evidence')
        if pack.get('hardware_constraint') in {'CPU_ONLY_CONSTRAINED','NO_COMPATIBLE_ACCELERATOR'}: issues.append('NeRF reference path is incompatible with declared constrained hardware')
    elif path=='3DGS_VIEW_SYNTHESIS':
        need('3D_GAUSSIAN_SPLATTING')
        if not isinstance(pack.get('gaussian_count'),int) or isinstance(pack.get('gaussian_count'),bool) or pack.get('gaussian_count')<=0 or pack.get('gs_render_target') in {'NOT_TRAINED','UNKNOWN','NONE',''} or not _finite(pack.get('gs_training_budget_minutes')) or pack.get('gs_training_budget_minutes')<=0:
            issues.append('3DGS positive path requires trained Gaussian/render evidence')
        if pack.get('novel_view_quality')!='PASS': issues.append('3DGS positive path requires PASS novel-view quality evidence')
        if pack.get('hardware_constraint') in {'CPU_ONLY_CONSTRAINED','NO_COMPATIBLE_ACCELERATOR'}: issues.append('3DGS reference path is incompatible with declared constrained hardware')
    elif path=='HYBRID_MESH_PLUS_VIEW_SYNTHESIS':
        need('PHOTOGRAMMETRY_SFM_MVS'); need('MAKNOW_DECISION_POLICY')
        if not (_traceable(rows.get('NERF')) or _traceable(rows.get('3D_GAUSSIAN_SPLATTING'))): issues.append('hybrid positive path requires traceable evidence for at least one view-synthesis representation')
        if pack.get('mvs_status')!='PASS' or pack.get('dense_output_type') in {'NONE','UNKNOWN',''} or pack.get('geometry_extractability') in {'NONE','UNKNOWN',''}: issues.append('hybrid path requires explicit editable geometry evidence')
        if pack.get('novel_view_quality')!='PASS': issues.append('hybrid path requires PASS novel-view quality evidence')
        if pack.get('hardware_constraint') in {'CPU_ONLY_CONSTRAINED','NO_COMPATIBLE_ACCELERATOR'}: issues.append('hybrid reference view-synthesis path is incompatible with declared constrained hardware')
    if path in METHOD_PATHS: need('MAKNOW_DECISION_POLICY')
    return list(dict.fromkeys(issues))

def expected_state(pack,path,scope):
    prov=pack.get('license_provenance_ref'); ps=pack.get('provenance_status')
    if ps not in RESOLVED_PROVENANCE or not isinstance(prov,str) or prov.strip().upper() in UNRESOLVED_TOKENS: return 'PROVENANCE_UNKNOWN'
    missing=pack.get('missing_decisive_evidence')
    if not isinstance(missing,list) or missing: return 'EVIDENCE_INCOMPLETE'
    if scope=='PENDING_TARGET_AUTHORITY': return 'HOLD'
    pose_stable=pack.get('camera_pose_source') not in {'UNKNOWN','TO_BE_ESTABLISHED',''} and pack.get('sfm_status')!='FAIL'
    if path=='RECAPTURE' or pack.get('overlap_quality')=='LOW' or pack.get('lighting_consistency')=='FAIL' or not pose_stable: return 'REWORK_REQUIRED'
    if path=='NONE': return 'METHOD_UNSUITABLE'
    if path in METHOD_PATHS:
        if pack.get('editability_requirement')=='REQUIRED' and path in VIEW_ONLY: return 'METHOD_UNSUITABLE'
        if pack.get('metric_scale_requirement')=='REQUIRED' and pack.get('metric_scale_evidence')!='PASS': return 'METHOD_UNSUITABLE'
        if method_issues(pack,path): return 'METHOD_UNSUITABLE'
        return 'TARGET_SPECIFIC' if scope=='CONSTRAINED_NAMED_TARGET_ONLY' else 'SELECTED'
    return 'EVIDENCE_INCOMPLETE'

def validate_pack(pack):
    issues=[]; d=pack.get('decision') or {}; path=d.get('selected_path',''); state=d.get('final_state',''); scope=d.get('target_constraint_scope','')
    required_strings=['capture_id','subject_type','overlap_quality','lighting_consistency','camera_pose_source','sfm_status','mvs_status','dense_output_type','nerf_representation','novel_view_quality','gs_render_target','geometry_extractability','editability_requirement','metric_scale_requirement','metric_scale_evidence','hardware_constraint','downstream_target','provenance_status']
    for k in required_strings:
        if not isinstance(pack.get(k),str) or not pack.get(k).strip(): issues.append(f'missing or invalid decisive field: {k}')
    for k,allowed in FIELD_DOMAINS.items():
        v=pack.get(k)
        if isinstance(v,str) and v.strip() and v not in allowed: issues.append(f'{k} is outside the canonical vocabulary')
    qfm=pack.get('quality_failure_mode')
    if not isinstance(qfm,list) or any(not isinstance(x,str) or x not in ALLOWED_FAILURE_MODES for x in qfm): issues.append('quality_failure_mode must be a list of canonical failure-mode tokens')
    for k in ['image_count','camera_count']:
        v=pack.get(k)
        if not isinstance(v,int) or isinstance(v,bool) or v<=0: issues.append(f'{k} must be a positive integer')
    sp=pack.get('sparse_point_count')
    if sp is not None and (not isinstance(sp,int) or isinstance(sp,bool) or sp<0): issues.append('sparse_point_count must be a non-negative integer or null')
    for k in ['nerf_training_budget_minutes','gs_training_budget_minutes']:
        v=pack.get(k)
        if v is not None and (not _finite(v) or v<0): issues.append(f'{k} must be a finite non-negative number or null')
    g=pack.get('gaussian_count')
    if g is not None and (not isinstance(g,int) or isinstance(g,bool) or g<0): issues.append('gaussian_count must be a non-negative integer or null')
    if path not in ALLOWED_PATHS: issues.append('selected_path is outside the allowed vocabulary')
    if state not in ALLOWED_STATES: issues.append('final_state is outside the allowed vocabulary')
    if scope not in ALLOWED_SCOPES: issues.append('target_constraint_scope is outside the allowed vocabulary')
    if path in PATH_STATE and state and state not in PATH_STATE[path]: issues.append('selected_path/final_state pair violates canonical compatibility')
    if scope=='PENDING_TARGET_AUTHORITY' and path!='HOLD': issues.append('PENDING_TARGET_AUTHORITY requires HOLD selected_path')
    if scope=='PENDING_TARGET_AUTHORITY' and state in POSITIVE_STATES: issues.append('PENDING_TARGET_AUTHORITY cannot support a positive final_state')
    if state=='SELECTED' and scope!='STANDARD_DECLARED_TARGET': issues.append('SELECTED requires STANDARD_DECLARED_TARGET')
    if state=='TARGET_SPECIFIC' and scope!='CONSTRAINED_NAMED_TARGET_ONLY': issues.append('TARGET_SPECIFIC requires CONSTRAINED_NAMED_TARGET_ONLY')
    if not isinstance(d.get('rationale'),str) or not d.get('rationale','').strip(): issues.append('decision rationale is required')
    if 'triggers' in d:
        tr=d.get('triggers'); required_trigger_keys={'review','migration','escalation','stop','pivot'}
        if not isinstance(tr,dict) or set(tr.keys())!=required_trigger_keys: issues.append('decision.triggers must contain exactly review/migration/escalation/stop/pivot')
        elif any(not isinstance(tr.get(k),str) or not tr[k].strip() for k in required_trigger_keys): issues.append('all bounded decision triggers must be explicit non-empty strings')
    missing=pack.get('missing_decisive_evidence')
    if not isinstance(missing,list): issues.append('missing_decisive_evidence must be a list')
    prov=pack.get('license_provenance_ref'); ps=pack.get('provenance_status')
    if not isinstance(prov,str) or not prov.strip(): issues.append('provenance reference is required')
    if ps not in RESOLVED_PROVENANCE and ps!='UNRESOLVED': issues.append('provenance_status is outside the allowed vocabulary')
    # Method evidence structure is validated even before a positive outcome.
    me=pack.get('method_evidence')
    if not isinstance(me,list): issues.append('method_evidence must be a list')
    issues.extend(method_binding_issues(pack))
    if state in POSITIVE_STATES:
        if ps not in RESOLVED_PROVENANCE or not isinstance(prov,str) or prov.strip().upper() in UNRESOLVED_TOKENS: issues.append('positive state cannot coexist with unresolved provenance')
        if isinstance(missing,list) and missing: issues.append('positive state cannot coexist with missing decisive evidence')
        pose_stable=pack.get('camera_pose_source') not in {'UNKNOWN','TO_BE_ESTABLISHED',''} and pack.get('sfm_status')!='FAIL'
        if path in METHOD_PATHS and not pose_stable: issues.append('positive reconstruction path requires stable pose/capture evidence')
        if pack.get('overlap_quality')=='LOW' or pack.get('lighting_consistency')=='FAIL': issues.append('positive state cannot coexist with failed capture-quality evidence')
        if pack.get('editability_requirement')=='REQUIRED' and path in VIEW_ONLY: issues.append('view-synthesis-only path cannot satisfy a required editable mesh outcome')
        if pack.get('metric_scale_requirement')=='REQUIRED' and pack.get('metric_scale_evidence')!='PASS': issues.append('positive state requires PASS metric scale evidence when metric scale is required')
        issues.extend(method_issues(pack,path))
    if path in ALLOWED_PATHS and state in ALLOWED_STATES and scope in ALLOWED_SCOPES:
        exp=expected_state(pack,path,scope)
        if state!=exp: issues.append(f'final_state does not match canonical precedence; expected {exp}')
    return list(dict.fromkeys(issues))
