"""Explicit draft-3 asset evidence closure; signatures never grant access.

The caller supplies independently obtained origin pins/current snapshots and an
authenticated event. No trust is bootstrapped from the bundle or a reporter claim.
"""
import copy
from dataclasses import dataclass, field

from odexa_ref import contracts as c, crypto, policy
from . import delegation as d, free_contracts as f
from .authority_transport import AuthoritySnapshot

VERSION = d.VERSION
PROFILE = 'asset_evidence_v1'
TYPE = 'odexa-asset-manifest+jws'
MAX_MANIFESTS, MAX_DEPTH, MAX_DOCUMENTS = 32, 8, 64
MAX_BUNDLE = 8388608
BASE = {'protocol_version', 'origin', 'asset_id', 'version_id', 'published_at', 'authority', 'representations'}
EXTRA = {'profile', 'issuer', 'delegation_id', 'policy_digest', 'publication', 'content_variants', 'derived_from'}


class AssetError(ValueError):
    pass


def need(condition, message):
    if not condition:
        raise AssetError(message)


def raw(value):
    return crypto.json_bytes(value)


def digest(value):
    return crypto.digest(value)


def load(value, maximum=131072):
    return crypto.strict_json(value, maximum)


def _same_metadata(a, b):
    return all(a[k] == b[k] for k in ('media_type', 'media_parameters', 'languages'))


def validate_manifest(value):
    """Validate a new envelope, explicitly reuse only prior representation semantics.

    The temporary projection calls the existing manifest validator; it is never
    returned, signed, transmitted or accepted as a draft-2 wire artifact.
    """
    c.document(value)
    c.keys(value, BASE | EXTRA, 'asset_manifest')
    need(value['protocol_version'] == VERSION and value['profile'] == PROFILE, 'explicit asset profile required')
    prior = {k: copy.deepcopy(value[k]) for k in BASE}
    prior['protocol_version'] = c.VERSION
    c.validate_manifest(prior)
    c.reference(value['issuer'], 'issuer')
    if value['delegation_id'] is not None:
        c.reference(value['delegation_id'], 'delegation_id', value['origin'])
    c.hash_value(value['policy_digest'], 'policy_digest')
    op = d.validate_operation(value['publication'])
    need(op['kind'] == 'publish_manifest', 'publication operation required')
    for key in ('origin', 'issuer', 'delegation_id', 'asset_id', 'version_id'):
        need(op[key] == value[key], 'manifest/publication binding mismatch: ' + key)
    need(op['service_id'] == value['authority']['service_id'], 'authority service mismatch')
    reps = {r['representation_id']: r for r in value['representations']}
    c.array(value['content_variants'], 'content_variants', 0, 64)
    seen = set()
    for variant in value['content_variants']:
        c.keys(variant, {'representation_id', 'content_codings', 'content_length', 'content_digest'}, 'content_variant')
        need(variant['representation_id'] in reps, 'content variant has unknown representation')
        c.array(variant['content_codings'], 'content_codings', 1, 8)
        # Identity is the representation's decoded bytes; codings are ordered.
        import re
        need(all(isinstance(x, str) and re.fullmatch(r'[a-z0-9][a-z0-9-]{0,31}', x) and x != 'identity'
                 for x in variant['content_codings']), 'noncanonical content coding')
        c.integer(variant['content_length'], 'content_length')
        c.hash_value(variant['content_digest'], 'content_digest')
        identity = (variant['representation_id'], tuple(variant['content_codings']))
        need(identity not in seen, 'conflicting/duplicate content variant')
        seen.add(identity)
    c.array(value['derived_from'], 'derived_from', 0, 32)
    seen = set()
    for edge in value['derived_from']:
        c.keys(edge, {'representation_id', 'source', 'relationship'}, 'derivation')
        need(edge['representation_id'] in reps, 'derivation output representation absent')
        c.validate_asset_ref(edge['source'])
        need(edge['source'] is not None, 'parent reference required')
        need(edge['relationship'] in {'copy', 'extract', 'transform'}, 'unknown relationship')
        identity = (edge['representation_id'], raw(edge['source']))
        need(identity not in seen, 'duplicate/conflicting derivation edge')
        need(edge['source']['version_id'] != value['version_id'], 'self derivation forbidden')
        seen.add(identity)
    return value


def sign_manifest(payload_bytes, private_key, key_id):
    """Sign exact bytes after shape checks; caller must obtain current authority."""
    value = validate_manifest(load(payload_bytes))
    need(value['publication']['key_id'] == key_id, 'publication key mismatch')
    return crypto.sign_jws(payload_bytes, private_key, key_id, TYPE, allowed_types={TYPE})


def validate_bundle(value):
    c.keys(value, {'protocol_version', 'profile', 'roots', 'manifests', 'documents'}, 'asset_bundle')
    need(value['protocol_version'] == VERSION and value['profile'] == PROFILE, 'unsupported asset bundle')
    c.array(value['roots'], 'roots', 1, 64)
    for ref in value['roots']:
        c.validate_asset_ref(ref)
        need(ref is not None, 'root reference required')
    need(len({raw(r) for r in value['roots']}) == len(value['roots']), 'duplicate root')
    c.array(value['manifests'], 'manifests', 1, MAX_MANIFESTS)
    for signed in value['manifests']:
        need(isinstance(signed, str) and len(signed) <= 262144, 'bounded compact JWS required')
        _, _, _, payload, _ = crypto.split_jws(signed, allowed_types={TYPE})
        validate_manifest(load(payload))
    c.array(value['documents'], 'documents', 1, MAX_DOCUMENTS)
    for document in value['documents']:
        c.keys(document, {'digest', 'body_b64url'}, 'document')
        c.hash_value(document['digest'], 'digest')
        body = crypto.unb64u(document['body_b64url'])
        load(body)
        need(digest(body) == document['digest'], 'document exact-byte mismatch')
    return value


@dataclass(frozen=True)
class AssetTrust:
    current_snapshots: dict
    pinned_documents: dict
    # Retained independently of the incoming bundle; caller persists these pins.
    manifest_pins: dict = field(default_factory=dict)


@dataclass(frozen=True)
class VerifiedClosure:
    bundle_bytes: bytes
    roots: tuple
    manifests: dict
    verified_at: str
    resource_permission_granted: bool = False


def _snapshot(provided, origin, now):
    snap = AuthoritySnapshot(provided.authority_bytes, provided.policy_bytes, copy.deepcopy(provided.observation))
    pol = policy.validate_policy(load(snap.policy_bytes))
    meta = d.validate_authority(load(snap.authority_bytes), pol)
    obs = snap.observation
    c.keys(obs, {'source', 'origin', 'authority_url', 'policy_url', 'authority_digest', 'policy_digest',
                'checked_at', 'available', 'tls_verified', 'redirected', 'revalidated'}, 'observation')
    need(meta['origin'] == origin and obs['origin'] == origin, 'current snapshot origin mismatch')
    need(obs['source'] in {'https', 'colocated'}, 'unsupported snapshot source')
    for key in ('available', 'tls_verified', 'redirected', 'revalidated'):
        need(type(obs[key]) is bool, 'observation booleans required')
    need(obs['available'] and obs['revalidated'] and not obs['redirected']
         and (obs['source'] == 'colocated' or obs['tls_verified']), 'current trusted acquisition required')
    need(obs['authority_url'] == origin + '/odexa-service.json' and obs['policy_url'] == origin + '/odexa.json', 'current snapshot URLs differ')
    need(obs['authority_digest'] == digest(snap.authority_bytes) and obs['policy_digest'] == digest(snap.policy_bytes), 'current snapshot bytes differ')
    need(0 <= (c.timestamp(now, 'now') - c.timestamp(obs['checked_at'], 'checked_at')).total_seconds() <= 5, 'current snapshot stale')
    return snap, meta, pol


def verify_closure(bundle_bytes, *, trust, now, histories=None):
    """Verify closed historical evidence with independent current origin knowledge.

    `now` and trust are trusted in-process inputs. This is not an HTTP collector.
    Historical permission is evaluated at the publisher's asserted signing time;
    a retained origin pin is not an independent timestamp witness. Current policy
    expiry or retired signers qualify history instead of erasing it. Known revoked
    signing material rejects evidence regardless of claimed signing time.
    """
    need(callable(now), 'trusted clock callable required')
    instant = now(); c.timestamp(instant, 'now')
    bundle = validate_bundle(load(bundle_bytes, MAX_BUNDLE))
    pins = copy.deepcopy(trust.pinned_documents)
    manifest_pins = copy.deepcopy(trust.manifest_pins)
    for sha, body in pins.items():
        need(isinstance(body, bytes) and digest(body) == sha, 'invalid independent document pin')
    need(len(pins) <= 512, 'external pin set exceeds bounded verifier')
    documents = {}
    for item in bundle['documents']:
        need(item['digest'] not in documents, 'duplicate document')
        documents[item['digest']] = crypto.unb64u(item['body_b64url'])
    manifests, versions, required_docs, origins = {}, {}, set(), set()
    for signed in bundle['manifests']:
        _, _, header, payload, _ = crypto.split_jws(signed, allowed_types={TYPE})
        value = validate_manifest(load(payload)); sha = digest(payload)
        need(sha not in manifests, 'duplicate manifest payload, including re-signature')
        need(value['version_id'] not in versions, 'immutable version redefined')
        need(manifest_pins.get(value['version_id'], sha) == sha, 'previously pinned version redefined')
        versions[value['version_id']] = sha; origins.add(value['origin'])
        need(value['published_at'] <= instant, 'future manifest')
        manifests[sha] = dict(value=value, payload_bytes=payload, compact_jws=signed, header=header)
        required_docs.update((value['authority']['digest'], value['policy_digest']))
    need(set(documents) == required_docs, 'missing or extra context document')
    snapshots, current_meta, revoked = {}, {}, set()
    for origin in origins:
        need(origin in trust.current_snapshots, 'origin lacks independently current snapshot')
        snap, meta, _ = _snapshot(trust.current_snapshots[origin], origin, instant)
        snapshots[origin], current_meta[origin] = snap, meta
        if histories is not None:
            need(origin in histories, 'origin lacks monotonic history')
            histories[origin].require_current(snap)
        for body in (snap.authority_bytes, snap.policy_bytes):
            pins[digest(body)] = body
    for sha, body in documents.items():
        need(pins.get(sha) == body, 'context lacks independent exact origin pin')
    # Include withdrawn compromised keys remembered in independently pinned history.
    pinned_policies = []
    for body in pins.values():
        try:
            pol = policy.validate_policy(load(body))
            if pol['origin'] in origins: pinned_policies.append(pol)
        except (ValueError, TypeError, KeyError):
            pass
    historical_metadata = []
    for body in pins.values():
        for pol in pinned_policies:
            try:
                meta = d.validate_authority(load(body), pol)
            except (ValueError, TypeError, KeyError):
                continue
            historical_metadata.append(meta)
            revoked.update(raw(k['public_jwk']) for s in meta['services'] for k in s['signing_keys'] if k['state'] == 'revoked')
            break
    for entry in manifests.values():
        value, header = entry['value'], entry['header']
        ar, pr = documents[value['authority']['digest']], documents[value['policy_digest']]
        pol = policy.validate_policy(load(pr)); meta = d.validate_authority(load(ar), pol)
        need(meta['origin'] == value['origin'] and meta['revision'] == value['authority']['revision'], 'authority revision/origin differs')
        op = value['publication']
        need(header['kid'] == op['key_id'], 'JWS publication key substitution')
        historical_obs = dict(source='https', origin=value['origin'], authority_url=value['origin']+'/odexa-service.json',
            policy_url=value['origin']+'/odexa.json', authority_digest=digest(ar), policy_digest=digest(pr),
            checked_at=value['published_at'], available=True, tls_verified=True, redirected=False, revalidated=True)
        decision = d.evaluate_authority(ar, pr, op, now=value['published_at'], observation=historical_obs)
        need(decision['decision'] == 'allow', 'manifest lacked historical publication authority: ' + decision['reason'] if decision['decision'] != 'allow' else '')
        service = next(s for s in meta['services'] if s['id'] == op['service_id'])
        key = next(k for k in service['signing_keys'] if k['kid'] == header['kid'])
        need(raw(key['public_jwk']) not in revoked, 'known revoked material cannot authenticate history')
        # A key identifier cannot silently be rebound in independent later metadata.
        for other in historical_metadata:
            if other['origin'] != value['origin']: continue
            for s in other['services']:
                if s['id'] == service['id']:
                    need(s['issuer'] == service['issuer'], 'origin reused service ID for different issuer')
                for k in s['signing_keys']:
                    if k['kid'] == header['kid']:
                        need(k['public_jwk'] == key['public_jwk'] and k['uses'] == key['uses']
                             and k['not_before'] == key['not_before'] and k['not_after'] == key['not_after'], 'origin rebound immutable signing-key identity')
        crypto.verify_jws(entry['compact_jws'], key['public_jwk'], TYPE, header['kid'], allowed_types={TYPE})
        snap = snapshots[value['origin']]
        current = d.evaluate_authority(snap.authority_bytes, snap.policy_bytes, op, now=instant, observation=snap.observation)
        entry['publication_authorized_now'] = current['decision'] == 'allow'
        entry['current_publication_reason'] = current.get('reason', 'allowed')
        entry['signature_assurance'] = 'historical_origin_authorized'
    visited = set()
    def resolve(ref):
        sha = ref['manifest_digest']
        need(sha in manifests, 'missing referenced manifest')
        value = manifests[sha]['value']
        need(all(ref[k] == value[k] for k in ('asset_id', 'version_id')), 'qualified manifest reference substitution')
        rep = next((r for r in value['representations'] if r['representation_id'] == ref['representation_id']), None)
        need(rep is not None, 'referenced representation absent')
        return value, rep
    def walk(ref, path=()):
        value, _ = resolve(ref); sha = ref['manifest_digest']
        need(sha not in path, 'derivation cycle')
        need(len(path) < MAX_DEPTH, 'derivation depth exceeded')
        visited.add(sha)
        for edge in value['derived_from']:
            parent, parent_rep = resolve(edge['source'])
            need(parent['published_at'] <= value['published_at'], 'parent published after child')
            if edge['relationship'] == 'copy':
                child_rep = next(r for r in value['representations'] if r['representation_id'] == edge['representation_id'])
                need(_same_metadata(child_rep, parent_rep) and child_rep['decoded_length'] == parent_rep['decoded_length']
                     and child_rep['decoded_digest'] == parent_rep['decoded_digest'], 'copy changed representation')
            walk(edge['source'], path + (sha,))
    for root in bundle['roots']: walk(root)
    need(visited == set(manifests), 'unreachable extra manifest')
    final = now(); c.timestamp(final, 'now')
    need(final >= instant, 'trusted clock moved backwards')
    for origin, snap in snapshots.items():
        _snapshot(snap, origin, final)
        if histories is not None: histories[origin].require_current(snap)
    return VerifiedClosure(bundle_bytes, tuple(copy.deepcopy(bundle['roots'])), manifests, final)


def correlate_delivery(event, closure, *, content_bytes=None, decoded_bytes=None):
    """Correlate an already authenticated gateway event; byte truth is separate.

    Without supplied bytes, matching event counters/digests are only authenticated
    observations. A range never establishes whole-version delivery. No decompressor
    is run: caller-supplied decoded bytes must come from its trusted decoding path.
    """
    f.validate_report(event)
    need(isinstance(closure, VerifiedClosure), 'verified closure required')
    result = dict(binding='referenced_only', observation_basis='authenticated_event_claim',
                  full_version_delivered=False, partial_version_delivered=False,
                  content_bytes_checked=False, decoded_bytes_checked=False,
                  resource_permission_granted=False)
    ref = event['asset_ref']
    if ref is None:
        result['binding'] = 'unbound'; return result
    need(ref['manifest_digest'] in closure.manifests, 'referenced manifest missing from verified closure')
    value = closure.manifests[ref['manifest_digest']]['value']
    need(event['origin'] == value['origin'] and all(ref[k] == value[k] for k in ('asset_id', 'version_id')), 'event asset substitution')
    rep = next((r for r in value['representations'] if r['representation_id'] == ref['representation_id']), None)
    need(rep is not None, 'event representation absent')
    need(event['occurred_at'] >= value['published_at'], 'delivery precedes manifest publication')
    if event['source'] != 'origin_observed' or event['http'] is None:
        return result
    http = event['http']
    if http['representation_metadata'] is not None:
        need(_same_metadata(http['representation_metadata'], rep), 'observed representation metadata conflicts')
    for body, prefix in ((content_bytes, 'content'), (decoded_bytes, 'decoded')):
        if body is not None:
            need(isinstance(body, bytes), 'observed bytes must be bytes')
            need(http[prefix+'_bytes'] == len(body) and http[prefix+'_digest'] == digest(body), 'supplied bytes differ from observed digest/count')
            result[prefix+'_bytes_checked'] = True
    if not http['content_codings'] and content_bytes is not None:
        need(decoded_bytes is None or content_bytes == decoded_bytes, 'identity decoded bytes differ')
        decoded_bytes = content_bytes
        result['decoded_bytes_checked'] = True
    if content_bytes is not None or decoded_bytes is not None:
        result['observation_basis'] = 'supplied_bytes_compared'
    if event['event_type'] != 'delivery.completed' or http['hop_role'] != 'end_client':
        return result
    if http['representation_metadata'] is None:
        return result
    if http['kind'] == 'partial':
        need(http['range']['complete_length'] == rep['decoded_length'], 'range total differs from representation')
        # A slice hash cannot be checked against a full-representation hash alone.
        result['binding'] = 'range_reference_consistent'
        result['partial_version_delivered'] = False
        return result
    if http['kind'] != 'full': return result
    if http['decoded_digest'] is None: return result
    need(http['decoded_digest'] == rep['decoded_digest'] and http['decoded_bytes'] == rep['decoded_length'], 'observed decoded representation conflicts')
    if http['content_codings']:
        variant = next((v for v in value['content_variants'] if v['representation_id'] == ref['representation_id']
                        and v['content_codings'] == http['content_codings']), None)
        if variant is not None:
            need(http['content_digest'] == variant['content_digest'] and http['content_bytes'] == variant['content_length'], 'observed encoded variant conflicts')
    result['binding'] = 'full_representation_consistent'
    # A signed observation is not proof that the endpoint received or used bytes.
    result['full_version_delivered'] = result['decoded_bytes_checked']
    return result
