from __future__ import annotations
import copy, re

HEX64=re.compile(r"^[0-9a-f]{64}$")
HEX40=re.compile(r"^[0-9a-f]{40}$")

def _deny(decision,boundary,**extra):
    out={'decision':decision,'failed_boundary':boundary}; out.update(extra); return out

def apply_probe(data, probe):
    d=copy.deepcopy(data); parts=probe['path'].split('.'); cur=d
    for part in parts[:-1]: cur=cur[int(part)] if part.isdigit() else cur[part]
    last=parts[-1]
    if last.isdigit(): cur[int(last)]=probe['value']
    else: cur[last]=probe['value']
    return d

def _component_names_and_validate(sbom):
    comps=sbom.get('components')
    if not isinstance(comps,list) or not comps: return None,'components must be a non-empty list'
    names=[]
    for c in comps:
        if not isinstance(c,dict): return None,'component must be object'
        name=c.get('name'); version=c.get('version'); purl=c.get('purl'); dig=(c.get('digest') or {}).get('sha256') if isinstance(c.get('digest'),dict) else None
        if not all(isinstance(x,str) and x.strip() for x in [name,version,purl,dig]): return None,'component metadata missing'
        if not HEX64.fullmatch(dig): return None,'component sha256 invalid'
        names.append(name)
    if len(set(names))!=len(names): return None,'duplicate component names are ambiguous in this fixture'
    return names,None

def evaluate(data, policy):
  try:
    if not isinstance(data,dict) or not isinstance(policy,dict): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    a=data.get('artifact'); s=data.get('sbom'); p=data.get('provenance'); att=data.get('attestation')
    if not all(isinstance(x,dict) for x in [a,s,p,att]): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    ad=(a.get('digest') or {}).get('sha256') if isinstance(a.get('digest'),dict) else None
    expected=[policy.get('expected_artifact_id'),policy.get('expected_artifact_name'),policy.get('expected_artifact_version'),policy.get('expected_artifact_digest')]
    actual=[a.get('id'),a.get('name'),a.get('version'),ad]
    if not all(isinstance(x,str) and x for x in expected+actual): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    if not HEX64.fullmatch(ad): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    if actual!=expected: return _deny('DENY_ARTIFACT_IDENTITY','ARTIFACT_IDENTITY')

    target=s.get('target')
    if not isinstance(target,dict) or not isinstance(target.get('digest'),dict): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    if target.get('name')!=a['name'] or target.get('version')!=a['version'] or target['digest'].get('sha256')!=ad:
        return _deny('DENY_SBOM_ARTIFACT_MISMATCH','SBOM_ARTIFACT_BINDING')

    names,err=_component_names_and_validate(s)
    if err: return _deny('DENY_MALFORMED_EVIDENCE','SBOM_COMPONENT_METADATA',reason=err)
    missing=[x for x in policy.get('required_components',[]) if x not in set(names)]
    if missing: return _deny('DENY_SBOM_COVERAGE','SBOM_COVERAGE',missing_components=missing)
    primary=next((c for c in s['components'] if c.get('name')==a['name']),None)
    if not primary or primary.get('version')!=a['version'] or primary.get('digest',{}).get('sha256')!=ad:
        return _deny('DENY_SBOM_COMPONENT_BINDING','SBOM_COMPONENT_BINDING')

    deps=s.get('dependencies')
    if not isinstance(deps,dict) or set(deps.keys())!=set(names): return _deny('DENY_SBOM_DEPENDENCY_GRAPH','SBOM_DEPENDENCY_GRAPH')
    for parent,children in deps.items():
        if not isinstance(children,list) or any((not isinstance(c,str) or c not in set(names)) for c in children):
            return _deny('DENY_SBOM_DEPENDENCY_GRAPH','SBOM_DEPENDENCY_GRAPH')
    for parent,child in policy.get('required_dependency_edges',[]):
        if child not in deps.get(parent,[]): return _deny('DENY_SBOM_DEPENDENCY_GRAPH','SBOM_DEPENDENCY_GRAPH')
    for terminal in policy.get('required_dependency_terminals',[]):
        if deps.get(terminal)!=[]: return _deny('DENY_SBOM_DEPENDENCY_GRAPH','SBOM_DEPENDENCY_GRAPH')

    if p.get('_type')!=policy.get('expected_statement_type') or p.get('predicateType')!=policy.get('expected_predicate_type'):
        return _deny('DENY_PROVENANCE_TYPE','PROVENANCE_TYPE')
    subjects=p.get('subject')
    if not isinstance(subjects,list) or len(subjects)!=1 or not isinstance(subjects[0],dict): return _deny('DENY_PROVENANCE_SUBJECT_MISMATCH','PROVENANCE_SUBJECT_BINDING')
    sd=(subjects[0].get('digest') or {}).get('sha256') if isinstance(subjects[0].get('digest'),dict) else None
    if subjects[0].get('name')!=a['id'] or sd!=ad: return _deny('DENY_PROVENANCE_SUBJECT_MISMATCH','PROVENANCE_SUBJECT_BINDING')

    pred=p.get('predicate');
    if not isinstance(pred,dict): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    rd=pred.get('runDetails'); bd=pred.get('buildDefinition')
    if not isinstance(rd,dict) or not isinstance(bd,dict): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    builder=(rd.get('builder') or {}).get('id') if isinstance(rd.get('builder'),dict) else None
    if not isinstance(builder,str) or builder not in policy.get('approved_builders',[]): return _deny('DENY_UNAPPROVED_BUILDER','BUILDER_POLICY',builder_id=builder)
    if att.get('synthetic_verified') is not True or not isinstance(att.get('identity'),str) or att.get('identity') not in policy.get('approved_attestors',[]):
        return _deny('DENY_ATTESTATION_IDENTITY','ATTESTATION_POLICY')

    deps2=bd.get('resolvedDependencies')
    if not isinstance(deps2,list): return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
    expected_uri=policy.get('expected_source_uri'); expected_rev=policy.get('expected_source_revision')
    match=False; observed=[]
    for dep in deps2:
        if not isinstance(dep,dict): continue
        uri=dep.get('uri'); dig=dep.get('digest') if isinstance(dep.get('digest'),dict) else {}
        rev=dig.get('gitCommit');
        if isinstance(uri,str) and isinstance(rev,str): observed.append({'uri':uri,'gitCommit':rev})
        if uri==expected_uri and rev==expected_rev: match=True
    if not match: return _deny('DENY_SOURCE_REVISION','SOURCE_REVISION_BINDING',observed_sources=observed)
    return {'decision':'ACCEPT_EVIDENCE_SET','failed_boundary':None,'artifact_digest':ad,'component_count':len(s['components']),'builder_id':builder,'source_revision':expected_rev,'attestation_identity':att['identity']}
  except (KeyError,TypeError,IndexError,AttributeError,ValueError):
    return _deny('DENY_MALFORMED_EVIDENCE','MALFORMED_EVIDENCE')
