"""Worker: one full outbox+inbox pass (§5.6).

``tick`` is the periodic heartbeat — deposit every fired push, receive inbound, and
expire stale pushes. It is driven by a daemon thread in the served MCP process
(P5 wiring) and by ``alexandria federation tick`` for cron-only installs. Sealing
happens HERE, at deposit time (not stage time), keyed by the push id so a
redelivery dedupes at the receiver.
"""
import base64

from alexandria_vault.federation import (identity, envelope, outbox, receive,
                                         relay_client as rc, state as fed_state)


def _deposit_pass(cfg, client, *, now=None):
    """Seal + deposit every due push, on the PEER's relay. Returns
    (deposited, rejected, failed) counts."""
    peers = receive.peers_by_fingerprint(cfg)
    priv, pub = identity.load_or_create()
    our_fp = identity.fingerprint(pub)
    deposited = rejected = failed = 0
    for push in outbox.due(now=now):
        peer = peers.get(push["peer_fp"])
        secret = fed_state.peer_secret_get(push["peer_fp"])
        if peer is None or not secret:
            # Peer removed / re-pairing mid-queue: this push can't be delivered.
            # Reject it (terminal) rather than re-yield it from due() every tick.
            outbox.mark_rejected(push["id"], "peer no longer configured")
            rejected += 1
            continue
        # The peer's relay URL is peer-supplied — refuse to make an outbound request
        # to a non-https / non-loopback target (SSRF guard, §10.9).
        peer_relay = peer.get("relay")
        if not rc.is_safe_relay_url(peer_relay):
            outbox.mark_rejected(push["id"], "peer relay URL is not https/loopback — refused")
            rejected += 1
            continue
        try:
            peer_pub = base64.b64decode(peer["pubkey"], validate=True)
            env = envelope.seal(priv, peer_pub, push["kind"], push["body"],
                                sender_fp=our_fp, recipient_fp=push["peer_fp"],
                                eid=push["id"], now=now)
        except envelope.EnvelopeError:
            outbox.mark_rejected(push["id"], "seal failed")
            rejected += 1
            continue
        # Deposit into the peer's mailbox on the PEER's relay (not necessarily ours).
        peer_client = client.for_relay(peer_relay)
        try:
            status, _ = peer_client.deposit(push["peer_fp"], secret["deposit_token"],
                                            envelope.serialize(env))
        except rc.RelayRefused as exc:
            # Connect-time SSRF refusal (relay resolved internal, or a redirect to
            # an unvalidated host): terminal — a security refusal never heals, so
            # reject rather than re-yield it every tick. Must precede RelayUnavailable
            # (RelayRefused is a subclass of it).
            outbox.mark_rejected(push["id"], f"deposit refused (SSRF guard): {exc}")
            rejected += 1
            continue
        except rc.RelayUnavailable as exc:
            outbox.mark_deposit_failed(push["id"], str(exc), now=now)
            failed += 1
            continue
        if status == 202:
            outbox.mark_deposited(push["id"])
            deposited += 1
        elif status == 403:                              # peer revoked our token
            outbox.mark_rejected(push["id"], "relay 403 — peer revoked our deposit token")
            rejected += 1
        elif status in (400, 413):                       # permanently undeliverable
            outbox.mark_rejected(push["id"], f"relay {status} — undeliverable")
            rejected += 1
        else:                                            # 429 / 5xx: transient, back off
            outbox.mark_deposit_failed(push["id"], f"relay {status}", now=now)
            failed += 1
    return deposited, rejected, failed


def tick(vault, cfg, client, *, now=None, today=None, commit=None):
    """One full pass: deposit fired pushes, receive inbound, expire stale. Returns a
    summary dict. ``commit`` (if given) is called per applied envelope inside receive
    (§5.6) — the served worker passes the git-writer's committer under its write lock."""
    deposited, rejected, failed = _deposit_pass(cfg, client, now=now)
    inbound = receive.poll_once(vault, cfg, client, now=now, today=today, commit=commit)
    # Closing deposit pass (F3): receipts (and any pushes) minted DURING receive are staged+
    # fired in the outbox but arrived after the opening deposit pass, so they'd otherwise sit
    # in `fired` until the next tick — which on a worker-less brain may never come, stranding
    # the sender's delivery confirmation. Deposit them now, in the same tick.
    d2, r2, f2 = _deposit_pass(cfg, client, now=now)
    deposited += d2
    rejected += r2
    failed += f2
    expired = outbox.expire_due(now=now)
    outbox.gc(now=now)                                   # reclaim settled rows (bounded growth)
    return {"deposited": deposited, "rejected": rejected, "deposit_failed": failed,
            "inbound": inbound, "expired": expired}


def deposit_once(vault, cfg, client, *, now=None):
    """Deposit fired pushes only (no receive) — the `push` CLI wants to SEND its push,
    not pull in and apply inbound content it can't commit. Returns the counts."""
    deposited, rejected, failed = _deposit_pass(cfg, client, now=now)
    return {"deposited": deposited, "rejected": rejected, "deposit_failed": failed}
