#!/usr/bin/env python3
"""Single-writer GitHub webhook topology reconciler for the Gitea source authority."""

import hashlib
import json
import os
import signal
import sys
import threading
import time
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone


API_BASE = "https://api.github.com"
STOP = threading.Event()


class ApiError(RuntimeError):
    def __init__(self, method, path, status):
        super().__init__(f"github-api-{method.lower()}-{status}")
        self.status = status
        self.path = path


def url_hash(value):
    return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()[:16]


def emit(event, **fields):
    print(json.dumps({"event": event, **fields, "valuesPrinted": False}, ensure_ascii=False, separators=(",", ":")), flush=True)


def load_topology(path):
    with open(path, encoding="utf-8") as handle:
        topology = json.load(handle)
    if topology.get("version") != 1:
        raise ValueError("hook topology version must be 1")
    prefix = topology.get("managedUrlPrefix")
    if not isinstance(prefix, str) or not prefix.startswith("https://"):
        raise ValueError("managedUrlPrefix must be https")
    repositories = topology.get("repositories")
    if not isinstance(repositories, list) or not repositories:
        raise ValueError("hook topology repositories must not be empty")
    for item in repositories:
        if not isinstance(item, dict) or not isinstance(item.get("repository"), str):
            raise ValueError("hook topology repository is invalid")
        urls = item.get("desiredUrls")
        if not isinstance(urls, list) or not urls or any(not isinstance(url, str) or not url.startswith("https://") for url in urls):
            raise ValueError("hook topology desiredUrls must contain https URLs")
        if len(urls) != len(set(urls)):
            raise ValueError("hook topology desiredUrls must be unique per repository")
        hooks = item.get("desiredHooks")
        if not isinstance(hooks, list) or not hooks:
            raise ValueError("hook topology desiredHooks must not be empty")
    reconcile = topology.get("reconcile") if isinstance(topology.get("reconcile"), dict) else {}
    recovery = reconcile.get("failedDeliveryRecovery") if isinstance(reconcile.get("failedDeliveryRecovery"), dict) else {}
    if recovery.get("enabled") is True:
        if recovery.get("targetId") != topology.get("ownerTargetId"):
            raise ValueError("failed delivery recovery must be scoped to the owner target")
        if not isinstance(recovery.get("statePath"), str) or not recovery["statePath"].startswith("/"):
            raise ValueError("failed delivery recovery statePath must be absolute")
    return topology


def request(token, timeout_seconds, method, api_path, payload=None, expected=(200, 201, 204), tolerate=()):
    data = None if payload is None else json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(API_BASE + api_path, data=data, method=method)
    req.add_header("Authorization", "Bearer " + token)
    req.add_header("Accept", "application/vnd.github+json")
    req.add_header("X-GitHub-Api-Version", "2022-11-28")
    if payload is not None:
        req.add_header("Content-Type", "application/json")
    try:
        with urllib.request.urlopen(req, timeout=timeout_seconds) as response:
            body = response.read().decode("utf-8", errors="replace")
            if response.status not in expected:
                raise ApiError(method, api_path, response.status)
            return response.status, json.loads(body) if body else None
    except urllib.error.HTTPError as error:
        error.read()
        if error.code in tolerate:
            return error.code, None
        raise ApiError(method, api_path, error.code) from error


def list_hooks(token, timeout_seconds, max_pages, repository):
    hooks = []
    encoded = urllib.parse.quote(repository, safe="/")
    for page in range(1, max_pages + 1):
        _, rows = request(token, timeout_seconds, "GET", f"/repos/{encoded}/hooks?per_page=100&page={page}")
        if not isinstance(rows, list):
            raise RuntimeError("github-hooks-response-invalid")
        hooks.extend(item for item in rows if isinstance(item, dict))
        if len(rows) < 100:
            return hooks
    raise RuntimeError("github-hooks-pagination-bound-exhausted")


def now_iso(epoch_ms=None):
    value = time.time() if epoch_ms is None else epoch_ms / 1000
    return datetime.fromtimestamp(value, tz=timezone.utc).isoformat().replace("+00:00", "Z")


def parse_epoch_ms(value):
    if not isinstance(value, str) or not value:
        return 0
    try:
        return int(datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp() * 1000)
    except ValueError:
        return 0


def atomic_write_ledger(path, ledger):
    directory = os.path.dirname(path)
    os.makedirs(directory, mode=0o700, exist_ok=True)
    temporary = os.path.join(directory, f".{os.path.basename(path)}.{os.getpid()}.{time.time_ns()}.tmp")
    payload = json.dumps(ledger, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + "\n"
    try:
        with open(temporary, "x", encoding="utf-8") as handle:
            os.chmod(temporary, 0o600)
            handle.write(payload)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
        directory_fd = os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
        try:
            os.fsync(directory_fd)
        finally:
            os.close(directory_fd)
    finally:
        try:
            os.unlink(temporary)
        except FileNotFoundError:
            pass


def load_ledger(path):
    try:
        with open(path, encoding="utf-8") as handle:
            ledger = json.load(handle)
    except FileNotFoundError:
        ledger = {"version": 1, "kind": "GiteaGithubHookRecoveryLedger", "createdAt": now_iso(), "records": {}}
        atomic_write_ledger(path, ledger)
        return ledger
    if ledger.get("version") != 1 or ledger.get("kind") != "GiteaGithubHookRecoveryLedger" or not isinstance(ledger.get("records"), dict):
        raise ValueError("hook recovery ledger is invalid")
    allowed_states = {"pending", "attempted", "delivery-succeeded-awaiting-source", "source-committed", "terminal-exhausted"}
    for record in ledger["records"].values():
        if not isinstance(record, dict) or record.get("state") not in allowed_states:
            raise ValueError("hook recovery ledger record state is invalid")
        if not isinstance(record.get("repository"), str) or not record["repository"] or not isinstance(record.get("deliveryId"), str) or not record["deliveryId"]:
            raise ValueError("hook recovery ledger record identity is invalid")
        if not isinstance(record.get("attempts"), int) or record["attempts"] < 0 or not isinstance(record.get("cooldownMs"), int) or record["cooldownMs"] < 0:
            raise ValueError("hook recovery ledger record counters are invalid")
    return ledger


def cleanup_ledger(ledger, retention_seconds, now_ms):
    cutoff = now_ms - retention_seconds * 1000
    terminal = {"source-committed", "terminal-exhausted"}
    stale = [
        key for key, record in ledger["records"].items()
        if isinstance(record, dict) and record.get("state") in terminal and int(record.get("updatedAtEpochMs") or 0) < cutoff
    ]
    for key in stale:
        del ledger["records"][key]
    return len(stale)


def hook_url(hook):
    config = hook.get("config") if isinstance(hook.get("config"), dict) else {}
    return config.get("url") if isinstance(config.get("url"), str) else ""


def is_managed_url(value, prefix):
    return value == prefix or value.startswith(prefix.rstrip("/") + "/")


def hook_config_matches(hook, events):
    config = hook.get("config") if isinstance(hook.get("config"), dict) else {}
    return bool(
        hook.get("name") == "web"
        and hook.get("active") is True
        and set(hook.get("events") or []) == set(events)
        and config.get("content_type") == "json"
        and str(config.get("insecure_ssl", "0")) == "0"
    )


def topology_observation(hooks, desired_urls, managed_prefix, events):
    managed = [item for item in hooks if item.get("name") == "web" and is_managed_url(hook_url(item), managed_prefix)]
    desired_set = set(desired_urls)
    drift_types = []
    for desired_url in desired_urls:
        matches = [item for item in managed if hook_url(item) == desired_url]
        if not matches:
            drift_types.append("desired-url-missing")
        elif len(matches) > 1:
            drift_types.append("desired-url-duplicate")
        if matches and not hook_config_matches(matches[0], events):
            drift_types.append("desired-hook-config-mismatch")
    if any(hook_url(item) not in desired_set for item in managed):
        drift_types.append("stale-managed-url")
    return {
        "ready": not drift_types,
        "desiredUrlHashes": sorted(url_hash(item) for item in desired_urls),
        "observedUrlHashes": sorted(url_hash(hook_url(item)) for item in managed),
        "driftTypes": sorted(set(drift_types)),
    }


def list_hook_deliveries(token, timeout_seconds, repository, hook_id, history_per_hook):
    encoded = urllib.parse.quote(repository, safe="/")
    _, rows = request(
        token,
        timeout_seconds,
        "GET",
        f"/repos/{encoded}/hooks/{hook_id}/deliveries?per_page={history_per_hook}",
    )
    if not isinstance(rows, list):
        raise RuntimeError("github-hook-deliveries-response-invalid")
    return [item for item in rows if isinstance(item, dict)]


def hook_delivery_detail(token, timeout_seconds, repository, hook_id, api_delivery_id):
    encoded = urllib.parse.quote(repository, safe="/")
    _, detail = request(token, timeout_seconds, "GET", f"/repos/{encoded}/hooks/{hook_id}/deliveries/{api_delivery_id}")
    if not isinstance(detail, dict):
        raise RuntimeError("github-hook-delivery-detail-invalid")
    payload = detail.get("request", {}).get("payload", {}) if isinstance(detail.get("request"), dict) else {}
    return {
        **detail,
        "repositoryName": payload.get("repository", {}).get("full_name") if isinstance(payload.get("repository"), dict) else None,
        "ref": payload.get("ref"),
        "requestedCommit": payload.get("after"),
    }


def bridge_committed_deliveries(topology, timeout_seconds):
    req = urllib.request.Request(topology["bridgeStatusUrl"], method="GET")
    try:
        with urllib.request.urlopen(req, timeout=timeout_seconds) as response:
            body = response.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
    payload = json.loads(body or "{}")
    return {
        (str(item.get("repo") or ""), str(item.get("deliveryId") or ""))
        for item in payload.get("deliveries", [])
        if isinstance(item, dict) and item.get("state") == "committed"
    }


def delivery_identity(repository, hook_id, delivery_id):
    return f"{repository}|{hook_id}|{delivery_id}"


def persist_record(ledger, path, key, record, now_ms):
    record["updatedAtEpochMs"] = now_ms
    record["updatedAt"] = now_iso(now_ms)
    ledger["records"][key] = record
    atomic_write_ledger(path, ledger)


def hook_body(url, events, secret):
    return {
        "name": "web",
        "active": True,
        "events": events,
        "config": {"url": url, "content_type": "json", "secret": secret, "insecure_ssl": "0"},
    }


def source_committed_record(ledger, state_path, key, record, status_code, now_ms):
    record.update({
        "state": "source-committed",
        "completionReason": "source-committed",
        "lastStatusCode": status_code,
        "nextAt": None,
        "nextAtEpochMs": 0,
    })
    persist_record(ledger, state_path, key, record, now_ms)
    emit(
        "github-hook-delivery-recovery-complete",
        repository=record["repository"],
        hookId=record["hookId"],
        deliveryId=record["deliveryId"],
        state=record["state"],
        completionReason="source-committed",
        attempts=record["attempts"],
        ok=True,
    )


def delivery_succeeded_record(ledger, state_path, key, record, status_code, now_ms):
    if record.get("state") == "delivery-succeeded-awaiting-source":
        return
    record.update({
        "state": "delivery-succeeded-awaiting-source",
        "completionReason": "github-delivery-succeeded",
        "lastStatusCode": status_code,
        "nextAt": None,
        "nextAtEpochMs": 0,
    })
    persist_record(ledger, state_path, key, record, now_ms)
    emit(
        "github-hook-delivery-succeeded-awaiting-source",
        repository=record["repository"],
        hookId=record["hookId"],
        deliveryId=record["deliveryId"],
        state=record["state"],
        attempts=record["attempts"],
        ok=True,
    )


def terminal_record(ledger, state_path, key, record, reason, status_code, now_ms):
    record.update({
        "state": "terminal-exhausted",
        "completionReason": reason,
        "lastStatusCode": status_code,
        "nextAt": None,
        "nextAtEpochMs": 0,
    })
    persist_record(ledger, state_path, key, record, now_ms)
    emit(
        "github-hook-delivery-recovery-terminal",
        repository=record["repository"],
        hookId=record["hookId"],
        deliveryId=record["deliveryId"],
        state=record["state"],
        completionReason=reason,
        attempts=record["attempts"],
        ok=False,
    )


def terminalize_no_longer_desired(ledger, recovery, repository, desired_hook, observed_hook_id):
    now_ms = int(time.time() * 1000)
    desired_url_hash = url_hash(desired_hook["url"])
    for key, record in list(ledger["records"].items()):
        if not isinstance(record, dict) or record.get("repository") != repository:
            continue
        if record.get("targetId") not in (None, recovery["targetId"]):
            continue
        if record.get("state") not in ("pending", "attempted"):
            continue
        if int(record.get("hookId") or 0) == observed_hook_id and record.get("hookUrlSha256") == desired_url_hash:
            continue
        terminal_record(
            ledger,
            recovery["statePath"],
            key,
            record,
            "hook-no-longer-desired",
            int(record.get("lastStatusCode") or 0),
            now_ms,
        )


def close_committed_ledger_records(ledger, recovery, committed):
    now_ms = int(time.time() * 1000)
    for key, record in list(ledger["records"].items()):
        if not isinstance(record, dict) or record.get("targetId") not in (None, recovery["targetId"]):
            continue
        if record.get("state") == "source-committed":
            continue
        delivery_id = str(record.get("deliveryId") or "")
        repo_keys = record.get("repoKeys") if isinstance(record.get("repoKeys"), list) else []
        if not any((str(repo_key), delivery_id) in committed for repo_key in repo_keys):
            continue
        source_committed_record(
            ledger,
            recovery["statePath"],
            key,
            record,
            int(record.get("lastStatusCode") or 0),
            now_ms,
        )


def recover_hook_deliveries(topology, repository_spec, desired_hook, observed_hook, token, ledger, committed, budget):
    reconcile = topology["reconcile"]
    recovery = reconcile["failedDeliveryRecovery"]
    timeout_seconds = reconcile["requestTimeoutMs"] / 1000
    repository = repository_spec["repository"]
    hook_id = int(observed_hook["id"])
    now_ms = int(time.time() * 1000)
    state_path = recovery["statePath"]
    desired_refs = {f"refs/heads/{branch}" for branch in desired_hook["branches"]}
    retry_codes = set(recovery["retryStatusCodes"])
    rows = list_hook_deliveries(token, timeout_seconds, repository, hook_id, recovery["historyPerHook"])
    rows_by_api_id = {str(item.get("id")): item for item in rows if item.get("id") is not None}

    for record in ledger["records"].values():
        if not isinstance(record, dict) or record.get("repository") != repository or int(record.get("hookId") or 0) != hook_id:
            continue
        if record.get("state") not in ("pending", "attempted", "delivery-succeeded-awaiting-source"):
            continue
        api_delivery_id = str(record.get("apiDeliveryId") or "")
        if api_delivery_id and api_delivery_id not in rows_by_api_id:
            detail = hook_delivery_detail(token, timeout_seconds, repository, hook_id, api_delivery_id)
            rows.append(detail)
            rows_by_api_id[api_delivery_id] = detail

    latest_by_delivery = {}
    for row in rows:
        delivery_id = str(row.get("guid") or row.get("id") or "")
        if not delivery_id:
            continue
        current = latest_by_delivery.get(delivery_id)
        score = (parse_epoch_ms(row.get("delivered_at")), int(row.get("id") or 0))
        current_score = (-1, -1) if current is None else (parse_epoch_ms(current.get("delivered_at")), int(current.get("id") or 0))
        if score > current_score:
            latest_by_delivery[delivery_id] = row
    rows = sorted(latest_by_delivery.values(), key=lambda item: (parse_epoch_ms(item.get("delivered_at")), int(item.get("id") or 0)), reverse=True)
    recovery_ok = True
    for row in rows:
        if row.get("event") != "push" or row.get("id") is None:
            continue
        api_delivery_id = str(row["id"])
        delivery_id = str(row.get("guid") or row["id"])
        key = delivery_identity(repository, hook_id, delivery_id)
        record = ledger["records"].get(key)
        if isinstance(record, dict) and record.get("state") in ("source-committed", "terminal-exhausted"):
            continue
        status_code = int(row.get("status_code") or 0)
        eligible_failure = status_code in retry_codes
        if record is None and not eligible_failure:
            continue
        delivered_at_ms = parse_epoch_ms(row.get("delivered_at"))
        if record is None and (delivered_at_ms == 0 or delivered_at_ms < now_ms - recovery["lookbackSeconds"] * 1000):
            continue
        if isinstance(record, dict) and record.get("state") in ("pending", "attempted") and delivered_at_ms > 0 and delivered_at_ms < now_ms - recovery["lookbackSeconds"] * 1000:
            terminal_record(ledger, state_path, key, record, "redelivery-window-expired", status_code, now_ms)
            continue

        detail = hook_delivery_detail(token, timeout_seconds, repository, hook_id, api_delivery_id)
        status_code = int(detail.get("status_code") or status_code or 0)
        if detail.get("repositoryName") != repository or detail.get("ref") not in desired_refs:
            if record is not None and record.get("state") in ("pending", "attempted"):
                terminal_record(ledger, state_path, key, record, "delivery-scope-mismatch", status_code, now_ms)
            continue
        requested_commit = str(detail.get("requestedCommit") or "")
        if record is None:
            record = {
                "repository": repository,
                "targetId": desired_hook["targetId"],
                "repoKeys": desired_hook["repoKeys"],
                "hookId": hook_id,
                "hookUrlSha256": url_hash(desired_hook["url"]),
                "deliveryId": delivery_id,
                "apiDeliveryId": api_delivery_id,
                "ref": detail["ref"],
                "requestedCommit": requested_commit,
                "state": "pending",
                "attempts": 0,
                "cooldownMs": recovery["cooldownMs"],
                "nextAtEpochMs": now_ms,
                "nextAt": now_iso(now_ms),
                "createdAt": now_iso(now_ms),
                "lastStatusCode": status_code,
                "completionReason": None,
            }
            persist_record(ledger, state_path, key, record, now_ms)
            emit(
                "github-hook-delivery-recovery-pending",
                repository=repository,
                hookId=hook_id,
                deliveryId=delivery_id,
                state="pending",
                attempts=0,
                nextAt=record["nextAt"],
                ok=True,
            )

        source_committed = any((repo_key, delivery_id) in committed for repo_key in desired_hook["repoKeys"])
        if source_committed:
            source_committed_record(ledger, state_path, key, record, status_code, now_ms)
            continue
        if 200 <= status_code < 300:
            delivery_succeeded_record(ledger, state_path, key, record, status_code, now_ms)
            continue
        if record.get("state") == "delivery-succeeded-awaiting-source":
            continue
        if status_code == 0:
            continue
        if status_code not in retry_codes:
            terminal_record(ledger, state_path, key, record, "non-retryable-status", status_code, now_ms)
            continue
        if int(record.get("nextAtEpochMs") or 0) > now_ms:
            continue
        if int(record.get("attempts") or 0) >= recovery["maxAttemptsPerDelivery"]:
            terminal_record(ledger, state_path, key, record, "max-attempts-exhausted", status_code, now_ms)
            continue
        if budget["remaining"] < 1:
            continue

        record["state"] = "attempted"
        record["attempts"] = int(record.get("attempts") or 0) + 1
        record["lastStatusCode"] = status_code
        record["lastOutcome"] = "redelivery-requested-write-ahead"
        record["nextAtEpochMs"] = now_ms + recovery["cooldownMs"]
        record["nextAt"] = now_iso(record["nextAtEpochMs"])
        persist_record(ledger, state_path, key, record, now_ms)
        budget["remaining"] -= 1
        encoded = urllib.parse.quote(repository, safe="/")
        try:
            request(
                token,
                timeout_seconds,
                "POST",
                f"/repos/{encoded}/hooks/{hook_id}/deliveries/{api_delivery_id}/attempts",
                expected=(202,),
            )
            record["lastOutcome"] = "redelivery-accepted"
            persist_record(ledger, state_path, key, record, now_ms)
            emit(
                "github-hook-delivery-redelivery-requested",
                repository=repository,
                hookId=hook_id,
                deliveryId=delivery_id,
                state=record["state"],
                attempts=record["attempts"],
                nextAt=record["nextAt"],
                ok=True,
            )
        except Exception as error:
            recovery_ok = False
            record["lastOutcome"] = "redelivery-request-failed"
            persist_record(ledger, state_path, key, record, now_ms)
            emit(
                "github-hook-delivery-redelivery-failed",
                repository=repository,
                hookId=hook_id,
                deliveryId=delivery_id,
                state=record["state"],
                attempts=record["attempts"],
                nextAt=record["nextAt"],
                errorType=type(error).__name__,
                error=str(error)[:160],
                ok=False,
            )
    return recovery_ok


def reconcile_repository(topology, repository_spec, token, secret, force_patch):
    reconcile = topology["reconcile"]
    timeout_seconds = reconcile["requestTimeoutMs"] / 1000
    max_pages = reconcile["listMaxPages"]
    repository = repository_spec["repository"]
    desired_urls = repository_spec["desiredUrls"]
    events = topology["events"]
    prefix = topology["managedUrlPrefix"]
    hooks = list_hooks(token, timeout_seconds, max_pages, repository)
    mutations = {"created": 0, "updated": 0, "deleted": 0}
    encoded = urllib.parse.quote(repository, safe="/")

    for desired_url in desired_urls:
        matches = sorted(
            [item for item in hooks if item.get("name") == "web" and hook_url(item) == desired_url],
            key=lambda item: int(item.get("id") or 0),
        )
        keeper = matches[0] if matches else None
        if keeper is None:
            _, created = request(token, timeout_seconds, "POST", f"/repos/{encoded}/hooks", hook_body(desired_url, events, secret), expected=(201,))
            if isinstance(created, dict):
                hooks.append(created)
            mutations["created"] += 1
        elif force_patch or not hook_config_matches(keeper, events):
            hook_id = int(keeper["id"])
            request(token, timeout_seconds, "PATCH", f"/repos/{encoded}/hooks/{hook_id}", hook_body(desired_url, events, secret))
            mutations["updated"] += 1
        for duplicate in matches[1:]:
            hook_id = int(duplicate["id"])
            request(token, timeout_seconds, "DELETE", f"/repos/{encoded}/hooks/{hook_id}", expected=(204,), tolerate=(404,))
            mutations["deleted"] += 1

    desired_set = set(desired_urls)
    for stale in hooks:
        stale_url = hook_url(stale)
        if stale.get("name") != "web" or not is_managed_url(stale_url, prefix) or stale_url in desired_set:
            continue
        hook_id = int(stale["id"])
        request(token, timeout_seconds, "DELETE", f"/repos/{encoded}/hooks/{hook_id}", expected=(204,), tolerate=(404,))
        mutations["deleted"] += 1

    observed = list_hooks(token, timeout_seconds, max_pages, repository)
    observation = topology_observation(observed, desired_urls, prefix, events)
    emit(
        "github-hook-topology-reconciled",
        repository=repository,
        ok=observation["ready"],
        desiredUrlHashes=observation["desiredUrlHashes"],
        observedUrlHashes=observation["observedUrlHashes"],
        driftTypes=observation["driftTypes"],
        mutations=mutations,
    )
    return observation, observed


def reconcile_once(topology, secret, force_patch):
    started = time.monotonic()
    topology_ready = True
    recovery_ready = True
    recovery = topology["reconcile"]["failedDeliveryRecovery"]
    ledger = None
    committed = set()
    budget = {"remaining": recovery["maxRedeliveriesPerCycle"]}
    if recovery["enabled"]:
        try:
            ledger = load_ledger(recovery["statePath"])
            removed = cleanup_ledger(ledger, recovery["stateRetentionSeconds"], int(time.time() * 1000))
            if removed:
                atomic_write_ledger(recovery["statePath"], ledger)
            committed = bridge_committed_deliveries(topology, topology["reconcile"]["requestTimeoutMs"] / 1000)
            close_committed_ledger_records(ledger, recovery, committed)
        except Exception as error:
            recovery_ready = False
            ledger = None
            emit("github-hook-delivery-recovery-ledger-unavailable", ok=False, errorType=type(error).__name__, error=str(error)[:160])
    for repository_spec in topology["repositories"]:
        try:
            token = os.environ[repository_spec["githubTokenEnv"]]
            observation, observed_hooks = reconcile_repository(topology, repository_spec, token, secret, force_patch)
            if not observation["ready"]:
                topology_ready = False
                continue
            if recovery["enabled"] and ledger is not None:
                desired_hooks = [item for item in repository_spec["desiredHooks"] if item["targetId"] == recovery["targetId"]]
                if len(desired_hooks) != 1:
                    recovery_ready = False
                    emit("github-hook-delivery-recovery-scope-invalid", repository=repository_spec["repository"], ok=False, targetId=recovery["targetId"])
                    continue
                desired_hook = desired_hooks[0]
                exact = [item for item in observed_hooks if item.get("name") == "web" and hook_url(item) == desired_hook["url"]]
                if len(exact) != 1 or not hook_config_matches(exact[0], topology["events"]):
                    recovery_ready = False
                    emit("github-hook-delivery-recovery-skipped-topology-drift", repository=repository_spec["repository"], ok=False, targetId=recovery["targetId"])
                    continue
                terminalize_no_longer_desired(ledger, recovery, repository_spec["repository"], desired_hook, int(exact[0]["id"]))
                if not recover_hook_deliveries(topology, repository_spec, desired_hook, exact[0], token, ledger, committed, budget):
                    recovery_ready = False
        except Exception as error:
            topology_ready = False
            recovery_ready = False
            emit(
                "github-hook-topology-reconcile-failed",
                repository=repository_spec["repository"],
                ok=False,
                errorType=type(error).__name__,
                error=str(error)[:160],
            )
    emit(
        "github-hook-topology-cycle-complete",
        ok=topology_ready and recovery_ready,
        topologyReady=topology_ready,
        recoveryReady=recovery_ready,
        redeliveryRequests=recovery["maxRedeliveriesPerCycle"] - budget["remaining"],
        repositoryCount=len(topology["repositories"]),
        elapsedMs=round((time.monotonic() - started) * 1000),
    )
    return topology_ready


def stop(_signum, _frame):
    STOP.set()


def main():
    if len(sys.argv) != 2:
        raise SystemExit("usage: platform_infra_gitea_hook_reconciler.py <topology.json>")
    topology = load_topology(sys.argv[1])
    if topology.get("ownerTargetId") != os.environ.get("UNIDESK_GITEA_TARGET_ID"):
        raise SystemExit("hook reconciler target is not the YAML owner")
    secret = os.environ["GITHUB_WEBHOOK_SECRET"]
    signal.signal(signal.SIGTERM, stop)
    signal.signal(signal.SIGINT, stop)
    force_patch = True
    while not STOP.is_set():
        if reconcile_once(topology, secret, force_patch):
            force_patch = False
        STOP.wait(topology["reconcile"]["intervalMs"] / 1000)


if __name__ == "__main__":
    main()
