from datetime import datetime, timezone
REQUIRED_EVIDENCE_FIELDS = ['metadata_role','trusted_key_id','version','expiry','authorized_targets','target_hash','observed_state','acceptance_decision','negative_evidence','residual_risk']

def _dt(s):
    return datetime.fromisoformat(s.replace('Z','+00:00'))

def _role_keys(role, policy):
    delegated=policy.get('delegations',{}).get(role)
    if delegated is not None:
        return delegated.get('trusted_keys',[])
    return policy.get('trusted_root',{}).get('roles',{}).get(role,[])

def _nonblank(value):
    return isinstance(value,str) and bool(value.strip())

def _not_expired(meta, policy):
    try:
        return _dt(meta.get('expires','')) > _dt(policy.get('as_of',''))
    except Exception:
        return False

def _matches_authorized(target_path, patterns):
    for pattern in patterns or []:
        if not isinstance(pattern,str) or not pattern:
            continue
        prefix=pattern[:-1] if pattern.endswith('*') else pattern
        if target_path.startswith(prefix):
            return True
    return False

def verify_metadata_authority(meta, role, policy):
    if not isinstance(meta,dict) or meta.get('role') != role:
        return {'decision':'DENY_ROLE_IDENTITY','failure_boundary':'ROLE_AUTHORITY'}
    if meta.get('signature_state') != policy.get('required_signature_state'):
        return {'decision':'DENY_SIGNATURE_STATE','failure_boundary':'ROLE_AUTHORITY'}
    signer=meta.get('signer','')
    if signer not in _role_keys(role,policy):
        return {'decision':'DENY_ROLE_SIGNER','failure_boundary':'ROLE_AUTHORITY'}
    return {'decision':'AUTHORITY_OK','failure_boundary':'NONE','trusted_key_id':signer}

def verify_delegation(target_path, role, policy, meta=None):
    if role == 'targets':
        allowed=(meta or {}).get('authorized_targets',[])
        if not _matches_authorized(target_path,allowed):
            return {'decision':'DENY_TARGETS_SCOPE','failure_boundary':'TARGET_AUTHORITY','authorized_targets':allowed}
        return {'decision':'DELEGATION_NOT_REQUIRED','failure_boundary':'NONE','authorized_targets':allowed}
    d=policy.get('delegations',{}).get(role)
    if not d: return {'decision':'DENY_UNKNOWN_DELEGATION','failure_boundary':'DELEGATION_SCOPE'}
    if not any(target_path.startswith(p) for p in d.get('path_prefixes',[])):
        return {'decision':'DENY_DELEGATION_SCOPE','failure_boundary':'DELEGATION_SCOPE','authorized_targets':[p+'*' for p in d.get('path_prefixes',[])]}
    return {'decision':'DELEGATION_OK','failure_boundary':'NONE','authorized_targets':[p+'*' for p in d.get('path_prefixes',[])]}

def verify_snapshot(repo, policy, required_role='targets'):
    s=repo.get('metadata',{}).get('snapshot',{})
    a=verify_metadata_authority(s,'snapshot',policy)
    if a['decision']!='AUTHORITY_OK': return a
    if not _not_expired(s,policy):
        return {'decision':'DENY_SNAPSHOT_EXPIRED','failure_boundary':'SNAPSHOT_FRESHNESS','expiry':s.get('expires')}
    minimum=policy.get('client_state',{}).get('minimum_snapshot_version',0)
    if not isinstance(s.get('version'),int) or s['version'] < minimum:
        return {'decision':'DENY_ROLLBACK_SNAPSHOT','failure_boundary':'ROLLBACK_SNAPSHOT','observed_version':s.get('version'),'minimum_version':minimum}
    required={'targets'}
    if required_role and required_role!='targets': required.add(required_role)
    if not required.issubset(set(s.get('meta_versions',{}))):
        return {'decision':'DENY_SNAPSHOT_ROLE_MISSING','failure_boundary':'SNAPSHOT_CONSISTENCY','required_roles':sorted(required)}
    for role,ver in s.get('meta_versions',{}).items():
        actual=repo.get('metadata',{}).get(role,{}).get('version')
        if actual != ver:
            return {'decision':'DENY_SNAPSHOT_INCONSISTENT','failure_boundary':'SNAPSHOT_CONSISTENCY','role':role}
    return {'decision':'SNAPSHOT_OK','failure_boundary':'NONE','version':s['version']}

def verify_timestamp(repo, policy):
    t=repo.get('metadata',{}).get('timestamp',{})
    a=verify_metadata_authority(t,'timestamp',policy)
    if a['decision']!='AUTHORITY_OK': return a
    minimum=policy.get('client_state',{}).get('minimum_timestamp_version',0)
    if not isinstance(t.get('version'),int) or t['version'] < minimum:
        return {'decision':'DENY_ROLLBACK_TIMESTAMP','failure_boundary':'ROLLBACK_TIMESTAMP'}
    try:
        if _dt(t.get('expires','')) <= _dt(policy.get('as_of','')):
            return {'decision':'DENY_TIMESTAMP_EXPIRED','failure_boundary':'TIMESTAMP_FRESHNESS','expiry':t.get('expires')}
    except Exception:
        return {'decision':'DENY_TIMESTAMP_EXPIRY_INVALID','failure_boundary':'TIMESTAMP_FRESHNESS'}
    snapshot_version=repo.get('metadata',{}).get('snapshot',{}).get('version')
    if t.get('snapshot_version') != snapshot_version:
        return {'decision':'DENY_TIMESTAMP_SNAPSHOT_MISMATCH','failure_boundary':'TIMESTAMP_FRESHNESS'}
    return {'decision':'TIMESTAMP_FRESH','failure_boundary':'NONE','version':t['version'],'expiry':t['expires']}

def verify_target(target_path, role, repo, policy):
    meta=repo.get('metadata',{}).get(role,{})
    a=verify_metadata_authority(meta,role,policy)
    if a['decision']!='AUTHORITY_OK': return a
    if not _not_expired(meta,policy): return {'decision':'DENY_TARGETS_EXPIRED','failure_boundary':'TARGETS_FRESHNESS','expiry':meta.get('expires')}
    d=verify_delegation(target_path,role,policy,meta)
    if d['decision'].startswith('DENY_'): return d
    s=verify_snapshot(repo,policy,role)
    if s['decision']!='SNAPSHOT_OK': return s
    t=verify_timestamp(repo,policy)
    if t['decision']!='TIMESTAMP_FRESH': return t
    declared=meta.get('targets',{}).get(target_path)
    observed=repo.get('targets',{}).get(target_path)
    if not declared or not observed:
        return {'decision':'DENY_TARGET_NOT_DECLARED','failure_boundary':'TARGET_AUTHORITY'}
    if declared.get('hash') != observed.get('hash') or declared.get('length') != observed.get('length'):
        return {'decision':'DENY_TARGET_DIGEST','failure_boundary':'TARGET_INTEGRITY'}
    return {'decision':'ACCEPT_TARGET','failure_boundary':'NONE','trusted_key_id':a['trusted_key_id'],'snapshot_version':s['version'],'timestamp_version':t['version'],'timestamp_expiry':t['expiry'],'target_hash':observed['hash'],'authorized_targets':d.get('authorized_targets',meta.get('authorized_targets',[]))}

def build_evidence_row(target_path, role, repo, policy, negative_evidence='none in this baseline row'):
    decision=verify_target(target_path,role,repo,policy); meta=repo.get('metadata',{}).get(role,{})
    return {'metadata_role':role,'trusted_key_id':meta.get('signer',''),'version':meta.get('version',''),'expiry':meta.get('expires',''),'authorized_targets':meta.get('authorized_targets',decision.get('authorized_targets',[])),'target_hash':repo.get('targets',{}).get(target_path,{}).get('hash',''),'observed_state':decision.get('decision',''),'acceptance_decision':decision.get('decision',''),'negative_evidence':negative_evidence,'residual_risk':'Synthetic local metadata only; does not prove real signing operations, repository compromise resistance, package safety, or production deployment security.'}

def evidence_row_complete(row):
    if not isinstance(row,dict) or set(row)!=set(REQUIRED_EVIDENCE_FIELDS): return False
    for k in REQUIRED_EVIDENCE_FIELDS:
        if row.get(k) in (None,'',[],{}): return False
        if isinstance(row.get(k),str) and not row.get(k).strip(): return False
    return True

def classify_transfer():
    return {'status':'REBUILD_ROLE_NAMESPACE_AND_FRESHNESS','reuse_first_namespace':False,'reason':'plugin_and_core_channels_have_distinct_authority_and_state'}
