#!/usr/bin/env python3
"""Fail-closed, read-only entry gate for the scoped Phase 1 E2E run.

This script deliberately does not execute an E2E mutation. It proves that the
dedicated identity, approved fixture manifest, and Timetabler source readiness
signals exist before a later, separately reviewed execution stage is allowed.
"""

from __future__ import annotations

import argparse
from dataclasses import dataclass
import datetime
import hashlib
import hmac
import json
import os
import re
import sys
import time
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlsplit, urlunsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError


REQUIRED_CASE_DRIVERS = {
    "E2E-01": "timetabler_admin_api",
    "E2E-02": "scheduling_engine_final_response",
    "E2E-03": "preschedule_advice",
    "E2E-04": "accepted_drag_drop",
    "E2E-05": "timetabler_booking_api",
    "E2E-06": "timetabler_reschedule_api",
    "E2E-07": "timetabler_lifecycle_api",
    "E2E-08": "manual_constraint_break",
    "E2E-09": "immutable_transport_redelivery",
    "E2E-10": "cross_service_mirror_only_race",
    "E2E-11": "coordinated_process_restart",
    "E2E-12": "safe_release_config_rollback",
    "E2E-13": "unfiltered_reconciliation",
}
RUN_ID_PATTERN = re.compile(r"^phase1-e2e-[a-z0-9][a-z0-9-]{5,80}$")
MAX_RESPONSE_BYTES = 1_048_576
E2E_ACTIONS = {
    "entry",
    "functional",
    "redelivery",
    "conflict",
    "restart",
    "rollback",
    "cleanup",
    "final",
}
FINAL_FIXTURE_SELECTOR_MODELS = {
    "all_fixture_activity_ids": ("activity", "TtActivity"),
    "all_fixture_staff_ids": ("staff", "TtStaff"),
    "all_fixture_location_ids": ("location", "TtLocation"),
}
MAX_FINAL_FIXTURE_IDS = 64


class EntryGateError(RuntimeError):
    """A safe entry condition was not met."""

    def __init__(self, message: str, *, safe_details: dict[str, Any] | None = None):
        super().__init__(message)
        self.safe_details = dict(safe_details or {})

    def with_safe_details(self, **details: Any) -> "EntryGateError":
        return EntryGateError(
            str(self), safe_details={**self.safe_details, **details}
        )


class NoRedirects(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        raise EntryGateError(f"unexpected HTTP redirect ({code})")


@dataclass(frozen=True)
class EntryConfig:
    run_id: str
    environment: str
    source_scope: str
    timetabler_base_url: str
    timetabler_email: str
    timetabler_password: str
    fixture_json: str

    @classmethod
    def from_environment(cls) -> "EntryConfig":
        names = {
            "run_id": "TT_PHASE1_E2E_RUN_ID",
            "environment": "TT_PHASE1_E2E_ENVIRONMENT",
            "source_scope": "TT_PHASE1_E2E_SOURCE_SCOPE",
            "timetabler_base_url": "TT_PHASE1_E2E_BASE_URL",
            "timetabler_email": "TT_PHASE1_E2E_EMAIL",
            "timetabler_password": "TT_PHASE1_E2E_PASSWORD",
            "fixture_json": "TT_PHASE1_E2E_FIXTURE_JSON",
        }
        values = {
            field: (
                os.environ.get(name, "")
                if field == "timetabler_password"
                else os.environ.get(name, "").strip()
            )
            for field, name in names.items()
        }
        missing = sorted(name for field, name in names.items() if not values[field])
        if missing:
            raise EntryGateError(
                "dedicated E2E configuration is incomplete: " + ", ".join(missing)
            )
        return cls(**values)


class TimetablerAdminSession:
    """Use the deployed application's own crypto/signing code for an ephemeral login."""

    def __init__(self, *, base_url: str, email: str, password: str, run_id: str):
        self.base_url = base_url
        self.email = email
        self.password = password
        self.run_id = run_id
        self.token: str | None = None
        self.user_id: int | None = None
        self.permission_count = 0
        self.permissions: frozenset[str] = frozenset()

    @staticmethod
    def _signed_payload(payload: dict[str, Any]) -> dict[str, Any]:
        from django.conf import settings

        from api.views.admin.base import AdminApiBase

        signed = {**payload, "timestamp": int(time.time())}
        signed["signature"] = AdminApiBase.sign(signed.copy(), settings.API_SECRET_KEY)
        return signed

    def _post(
        self,
        path: str,
        payload: dict[str, Any],
        *,
        authenticated: bool,
        request_id: str,
    ) -> dict[str, Any]:
        headers = {
            "User-Agent": f"timetabler-phase1-e2e/{self.run_id}",
            "X-App-Type": "phase1-e2e",
            "X-Request-ID": request_id,
            "X-Correlation-ID": request_id,
        }
        if authenticated:
            if not self.token:
                raise EntryGateError(
                    "dedicated Timetabler E2E session is not authenticated"
                )
            headers["Authorization"] = f"Token {self.token}"
        return _request_json(
            "POST",
            f"{self.base_url}{path}",
            headers=headers,
            payload=self._signed_payload(payload),
        )

    def login_and_attest(self) -> None:
        from api.models import RolePermission, User
        from api.utils import encrypt_aes_128_cbc

        login = self._post(
            "/api/admin/login",
            {
                "email": self.email,
                "password": encrypt_aes_128_cbc(self.password),
            },
            authenticated=False,
            request_id=f"{self.run_id}-login",
        )
        data = login.get("data")
        if not isinstance(data, dict) or not isinstance(data.get("token"), str):
            raise EntryGateError("dedicated Timetabler E2E login failed")
        self.token = data["token"]
        user_data = data.get("user")
        if not isinstance(user_data, dict) or user_data.get("email") != self.email:
            raise EntryGateError("dedicated Timetabler E2E login identity mismatch")
        self.user_id = user_data.get("id")

        info = self._post(
            "/api/admin/info",
            {},
            authenticated=True,
            request_id=f"{self.run_id}-identity",
        ).get("data")
        permissions = info.get("permission") if isinstance(info, dict) else None
        if not isinstance(permissions, list) or len(set(permissions)) != 53:
            raise EntryGateError(
                "dedicated Timetabler E2E identity must expose 53 permissions"
            )
        self.permissions = frozenset(permissions)
        self.permission_count = len(self.permissions)

        user = User.objects.filter(pk=self.user_id, email=self.email).first()
        if user is None or user.status != User.STATUS_TO_CODE["active"]:
            raise EntryGateError("dedicated Timetabler E2E identity is not active")
        if user.is_superuser:
            raise EntryGateError(
                "dedicated Timetabler E2E identity must not be Super Admin"
            )
        roles = list(user.role.values_list("id", "name"))
        if roles != [(5, "Phase 1 E2E Runner")]:
            raise EntryGateError("dedicated Timetabler E2E role attestation failed")
        role_permissions = set(
            RolePermission.objects.filter(role_id=5).values_list(
                "permission", flat=True
            )
        )
        if role_permissions != self.permissions:
            raise EntryGateError(
                "dedicated Timetabler E2E permission attestation failed"
            )

    def post(
        self, path: str, payload: dict[str, Any], *, request_id: str
    ) -> dict[str, Any]:
        return self._post(
            path,
            payload,
            authenticated=True,
            request_id=request_id,
        )

    def logout(self) -> None:
        if not self.token:
            return
        try:
            self._post(
                "/api/admin/logout",
                {},
                authenticated=True,
                request_id=f"{self.run_id}-logout",
            )
        finally:
            self.token = None


def bootstrap_django_runtime() -> None:
    # These tools are executed as ``venv/bin/python deploy/<script>.py`` on
    # staging.  In that invocation Python adds deploy/, but not the repository
    # root containing the sibling backend/ and api/ packages, to sys.path.
    repository_root = str(Path(__file__).resolve().parents[1])
    if repository_root not in sys.path:
        sys.path.insert(0, repository_root)
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings")
    try:
        import django

        django.setup()
    except Exception as error:
        raise EntryGateError(
            "deployed Django runtime could not be initialized "
            f"({type(error).__name__})"
        ) from error


def validate_base_url(value: str, label: str) -> str:
    parsed = urlsplit(value)
    if parsed.scheme != "https" or not parsed.netloc:
        raise EntryGateError(f"{label} must be an absolute HTTPS URL")
    if parsed.username or parsed.password or parsed.query or parsed.fragment:
        raise EntryGateError(
            f"{label} must not contain credentials, query, or fragment"
        )
    path = parsed.path.rstrip("/")
    return urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))


def validate_run_id(value: str) -> None:
    if not RUN_ID_PATTERN.fullmatch(value):
        raise EntryGateError("run id must match phase1-e2e-[a-z0-9][a-z0-9-]{5,80}")


def load_and_validate_manifest(
    raw: str, *, environment: str, source_scope: str, require_approval: bool = True
) -> dict[str, Any]:
    try:
        manifest = json.loads(raw)
    except (TypeError, json.JSONDecodeError) as error:
        raise EntryGateError("fixture manifest must be valid JSON") from error
    if not isinstance(manifest, dict) or manifest.get("schema_version") != 1:
        raise EntryGateError("fixture manifest schema_version must equal 1")
    if manifest.get("environment") != environment or environment != "staging":
        raise EntryGateError("fixture manifest must be explicitly scoped to staging")
    if manifest.get("source_scope") != source_scope:
        raise EntryGateError("fixture manifest source scope does not match runtime")
    if require_approval:
        if manifest.get("approved") is not True:
            raise EntryGateError("fixture manifest is not approved")
        if not str(manifest.get("approval_ref") or "").strip():
            raise EntryGateError("fixture manifest approval_ref is required")
    if not isinstance(manifest.get("conflict_armed"), bool):
        raise EntryGateError("fixture manifest must explicitly declare conflict_armed")
    stage_watermarks = manifest.get("expected_stage_start_watermarks")
    if not isinstance(stage_watermarks, dict) or set(stage_watermarks) != E2E_ACTIONS:
        raise EntryGateError(
            "fixture manifest must declare every expected stage start watermark"
        )
    if any(
        not isinstance(value, int) or value < 1 for value in stage_watermarks.values()
    ):
        raise EntryGateError(
            "expected stage start watermarks must be positive integers"
        )

    cases = manifest.get("cases")
    if not isinstance(cases, list):
        raise EntryGateError("fixture manifest cases must be an array")
    actual: dict[str, dict[str, Any]] = {}
    for case in cases:
        if not isinstance(case, dict) or not isinstance(case.get("id"), str):
            raise EntryGateError("each fixture case must have a string id")
        case_id = case["id"]
        if case_id in actual:
            raise EntryGateError(f"duplicate fixture case: {case_id}")
        actual[case_id] = case
    if set(actual) != set(REQUIRED_CASE_DRIVERS):
        missing = sorted(set(REQUIRED_CASE_DRIVERS) - set(actual))
        unexpected = sorted(set(actual) - set(REQUIRED_CASE_DRIVERS))
        raise EntryGateError(
            f"fixture case inventory mismatch; missing={missing}, unexpected={unexpected}"
        )
    for case_id, expected_driver in REQUIRED_CASE_DRIVERS.items():
        case = actual[case_id]
        if case.get("driver") != expected_driver:
            raise EntryGateError(f"{case_id} must use driver {expected_driver}")
        fixture_refs = case.get("fixture_refs")
        if not isinstance(fixture_refs, list) or not fixture_refs:
            raise EntryGateError(f"{case_id} must name its approved fixture references")
        if any(
            not isinstance(value, str) or not value.strip() for value in fixture_refs
        ):
            raise EntryGateError(f"{case_id} contains an invalid fixture reference")
        if case.get("reverse_delivery_expected") is not False:
            raise EntryGateError(f"{case_id} must require reverse delivery false")
    return manifest


def attest_staging_host() -> dict[str, Any]:
    host = os.environ.get("TT_STAGING_HOST_VALUE", "")
    expected = os.environ.get("TT_PHASE1_E2E_HOST_SHA256", "").lower()
    if not host or not re.fullmatch(r"[0-9a-f]{64}", expected):
        raise EntryGateError("staging host fingerprint configuration is incomplete")
    actual = hashlib.sha256(host.encode()).hexdigest()
    if not hmac.compare_digest(actual, expected):
        raise EntryGateError(
            "workflow host does not match the attested E2E staging host"
        )
    return {"staging_host_attested": True}


def _request_json(
    method: str,
    url: str,
    *,
    headers: dict[str, str],
    payload: dict[str, Any] | None = None,
    timeout_seconds: int = 15,
) -> dict[str, Any]:
    encoded = None
    request_headers = {"Accept": "application/json", **headers}
    if payload is not None:
        encoded = json.dumps(payload, separators=(",", ":")).encode()
        request_headers["Content-Type"] = "application/json"
    request = Request(url, data=encoded, headers=request_headers, method=method)
    endpoint_path = urlsplit(url).path
    request_id = headers.get("X-Request-ID")

    def http_failure(status_code: int, response_body: bytes) -> EntryGateError:
        response_sha256 = None
        if len(response_body) <= MAX_RESPONSE_BYTES:
            response_sha256 = hashlib.sha256(response_body).hexdigest()
        return EntryGateError(
            f"endpoint returned HTTP {status_code}",
            safe_details={
                "method": method,
                "path": endpoint_path,
                "request_id": request_id,
                "http_status": int(status_code),
                "response_body_sha256": response_sha256,
            },
        )

    try:
        with build_opener(NoRedirects()).open(
            request, timeout=timeout_seconds
        ) as response:
            body = response.read(MAX_RESPONSE_BYTES + 1)
            if len(body) > MAX_RESPONSE_BYTES:
                raise EntryGateError("endpoint response exceeded safety bound")
            if response.status != 200:
                raise http_failure(response.status, body)
    except EntryGateError:
        raise
    except HTTPError as error:
        error_body = error.read(MAX_RESPONSE_BYTES + 1)
        raise http_failure(error.code, error_body) from error
    except (URLError, TimeoutError, OSError) as error:
        raise EntryGateError("endpoint was unavailable") from error
    try:
        result = json.loads(body)
    except json.JSONDecodeError as error:
        raise EntryGateError("endpoint did not return JSON") from error
    if not isinstance(result, dict):
        raise EntryGateError("endpoint returned a non-object JSON value")
    return result


def validate_timetabler_health(payload: dict[str, Any], source_scope: str) -> int:
    expected = {
        "ready": True,
        "phase": "phase1",
        "reverse_delivery_enabled": False,
        "capture_enabled": True,
        "publish_enabled": True,
        "activation_approved": True,
        "source_scope": source_scope,
        "schema_version": "2",
        "transport": "kafka",
        "dead_letter_count": 0,
        "publisher_live": True,
    }
    mismatches = [key for key, value in expected.items() if payload.get(key) != value]
    watermark = payload.get("transport_watermark")
    if not isinstance(watermark, int) or watermark < 1:
        mismatches.append("transport_watermark")
    if payload.get("publisher_last_sequence") != watermark:
        mismatches.append("publisher_last_sequence")
    if payload.get("errors") not in ([], None):
        mismatches.append("errors")
    if mismatches:
        raise EntryGateError(
            "Timetabler entry invariants failed: " + ", ".join(sorted(set(mismatches)))
        )
    return watermark


def validate_snapshot(payload: dict[str, Any], expected_watermark: int) -> None:
    if str(payload.get("snapshot_watermark")) != str(expected_watermark):
        raise EntryGateError(
            "snapshot watermark changed during the read-only entry check"
        )
    if payload.get("complete") not in (True, False):
        raise EntryGateError(
            "snapshot response is missing bounded-page completion evidence"
        )
    if not isinstance(payload.get("envelopes"), list):
        raise EntryGateError("snapshot response is missing transaction envelopes")


def validate_final_fixture_selectors(
    manifest: dict[str, Any], run_id: str
) -> list[tuple[str, str, dict[str, Any], int]]:
    selectors = manifest.get("reference_selectors")
    if not isinstance(selectors, dict):
        raise EntryGateError("final fixture selectors are unavailable")
    validated = []
    for selector_name, (model_kind, model_name) in (
        FINAL_FIXTURE_SELECTOR_MODELS.items()
    ):
        selector = selectors.get(selector_name)
        if not isinstance(selector, dict):
            raise EntryGateError(f"final fixture selector is missing: {selector_name}")
        filters = selector.get("filters")
        expected_count = selector.get("expected_count")
        expected_prefix = (
            f"{run_id}-"
            if model_kind == "activity"
            else f"{run_id}-fixture-{model_kind}-"
        )
        if (
            selector.get("model") != model_kind
            or selector.get("dedicated_e2e") is not True
            or filters != {"code__startswith": expected_prefix}
            or not isinstance(expected_count, int)
            or not 1 <= expected_count <= MAX_FINAL_FIXTURE_IDS
        ):
            raise EntryGateError(
                f"final fixture selector is not bounded and run-owned: {selector_name}"
            )
        validated.append((selector_name, model_name, filters, expected_count))
    return validated


def final_fixture_absence(manifest: dict[str, Any], run_id: str) -> dict[str, Any]:
    from api import models

    remaining = {}
    expected_deleted_counts = {}
    for selector_name, model_name, filters, expected_count in (
        validate_final_fixture_selectors(manifest, run_id)
    ):
        model = getattr(models, model_name)
        remaining[selector_name] = list(
            model.objects.filter(**filters)
            .order_by("id")
            .values_list("id", flat=True)[: MAX_FINAL_FIXTURE_IDS + 1]
        )
        expected_deleted_counts[selector_name] = expected_count
    if any(remaining.values()):
        raise EntryGateError("run-owned fixture rows remain at the final gate")
    return {
        "fixtures_absent": True,
        "expected_deleted_counts": expected_deleted_counts,
        "remaining_fixture_ids": remaining,
        "fixture_recreation_executed": False,
        "direct_database_mutation": False,
    }


def _conflict_occurrence_window(
    *,
    week_start: datetime.date,
    slot: int,
    slot_per_day: int,
    minute_per_slot: int,
    duration_minutes: int,
    timezone_name: str,
) -> dict[str, str]:
    if slot < 0 or slot_per_day < 1 or minute_per_slot < 1 or duration_minutes < 1:
        raise EntryGateError("conflict occurrence inputs are invalid")
    try:
        source_timezone = ZoneInfo(timezone_name)
    except ZoneInfoNotFoundError as error:
        raise EntryGateError("conflict occurrence timezone is unavailable") from error
    day = slot // slot_per_day
    minute_of_day = (slot % slot_per_day) * minute_per_slot
    if minute_of_day >= 24 * 60:
        raise EntryGateError("conflict slot is outside the configured day")
    local_date = week_start + datetime.timedelta(days=day)
    local_start = datetime.datetime.combine(
        local_date,
        datetime.time(hour=minute_of_day // 60, minute=minute_of_day % 60),
        tzinfo=source_timezone,
    )
    utc_start = local_start.astimezone(datetime.UTC)
    utc_end = utc_start + datetime.timedelta(minutes=duration_minutes)
    return {
        "utc_start": utc_start.isoformat().replace("+00:00", "Z"),
        "utc_end": utc_end.isoformat().replace("+00:00", "Z"),
        "timezone": timezone_name,
        "local_start": local_start.isoformat(),
        "local_end": utc_end.astimezone(source_timezone).isoformat(),
    }


def _validate_conflict_allocation_state(
    manifest: dict[str, Any],
    *,
    scheduled: bool,
    scheduled_slot: int | None,
    actual_staff_ids: set[int],
    actual_location_ids: set[int],
    expected_staff_id: int,
    expected_location_id: int,
    normalized_slot: int,
    slot_per_week: int,
) -> str:
    fences = manifest["expected_stage_start_watermarks"]
    post_conflict = fences["entry"] == fences["conflict"] + 1
    if not scheduled:
        if post_conflict:
            raise EntryGateError(
                "post-conflict entry expected the dedicated fixture allocation"
            )
        return "unscheduled"
    if not post_conflict or manifest["conflict_armed"] is not False:
        raise EntryGateError(
            "scheduled conflict fixture requires a disarmed post-conflict entry fence"
        )
    if (
        scheduled_slot is None
        or scheduled_slot % slot_per_week != normalized_slot
        or actual_staff_ids != {expected_staff_id}
        or actual_location_ids != {expected_location_id}
    ):
        raise EntryGateError(
            "post-conflict fixture allocation does not match Staff, Location, and slot evidence"
        )
    return "allocated"


def conflict_contender_basis(manifest: dict[str, Any]) -> dict[str, Any]:
    from django.conf import settings

    from api.models import TtActivity, TtLocation, TtSetting, TtStaff

    selectors = manifest["reference_selectors"]
    resolved = {}
    for name, model in (
        ("conflict_activity_id", TtActivity),
        ("conflict_location_id", TtLocation),
        ("conflict_staff_id", TtStaff),
    ):
        selector = selectors[name]
        filters = selector["filters"]
        if selector.get("expected_count") != 1 or set(filters) != {"code"}:
            raise EntryGateError("conflict selector must resolve one exact run-owned code")
        rows = list(model.objects.filter(**filters).order_by("id")[:2])
        if len(rows) != 1:
            raise EntryGateError("conflict selector did not resolve exactly one fixture")
        resolved[name] = rows[0]

    activity = resolved["conflict_activity_id"]
    slot = manifest["variables"].get("conflict_slot")
    if not isinstance(slot, int):
        raise EntryGateError("conflict slot must be an integer")
    values = TtSetting.get_multiple_setting(
        {"minute_per_slot", "slot_per_day", "slot_per_week"}
    )
    minute_per_slot = int(values["minute_per_slot"])
    slot_per_day = int(values["slot_per_day"])
    slot_per_week = int(values["slot_per_week"])
    normalized_slot = slot % slot_per_week
    allocation_state = _validate_conflict_allocation_state(
        manifest,
        scheduled=bool(activity.scheduled),
        scheduled_slot=(
            int(activity.scheduled_start_slot)
            if activity.scheduled_start_slot is not None
            else None
        ),
        actual_staff_ids=set(activity.staff.values_list("id", flat=True)),
        actual_location_ids=set(activity.location.values_list("id", flat=True)),
        expected_staff_id=int(resolved["conflict_staff_id"].id),
        expected_location_id=int(resolved["conflict_location_id"].id),
        normalized_slot=normalized_slot,
        slot_per_week=slot_per_week,
    )
    weeks = (
        activity.week_pattern.week.all()
        if activity.week_pattern_id
        else activity.week.all()
    ).order_by("start_date", "id")
    occurrences = [
        {
            "week_id": int(week.id),
            **_conflict_occurrence_window(
                week_start=week.start_date,
                slot=normalized_slot,
                slot_per_day=slot_per_day,
                minute_per_slot=minute_per_slot,
                duration_minutes=int(activity.duration),
                timezone_name=settings.TIME_ZONE,
            ),
        }
        for week in weeks
    ]
    if not occurrences:
        raise EntryGateError("conflict fixture has no occurrence weeks")
    return {
        "activity_source_ref": int(activity.id),
        "location_source_ref": int(resolved["conflict_location_id"].id),
        "staff_source_ref": int(resolved["conflict_staff_id"].id),
        "conflict_slot": normalized_slot,
        "duration_minutes": int(activity.duration),
        "timezone": settings.TIME_ZONE,
        "occurrences": occurrences,
        "allocation_state": allocation_state,
        "scheduled_start_slot": (
            int(activity.scheduled_start_slot) if activity.scheduled else None
        ),
        "actual_staff_source_refs": sorted(
            int(value) for value in activity.staff.values_list("id", flat=True)
        ),
        "actual_location_source_refs": sorted(
            int(value) for value in activity.location.values_list("id", flat=True)
        ),
        "conflict_armed": manifest["conflict_armed"],
        "manifest_entry_fence": manifest["expected_stage_start_watermarks"][
            "entry"
        ],
        "manifest_conflict_fence": manifest["expected_stage_start_watermarks"][
            "conflict"
        ],
        "execute_at_epoch": next(
            case["execute_at_epoch"]
            for case in manifest["cases"]
            if case["id"] == "E2E-10"
        ),
    }


def execute_entry(config: EntryConfig) -> dict[str, Any]:
    bootstrap_django_runtime()
    from django.conf import settings

    validate_run_id(config.run_id)
    timetabler_base = validate_base_url(
        config.timetabler_base_url, "Timetabler base URL"
    )
    manifest = load_and_validate_manifest(
        config.fixture_json,
        environment=config.environment,
        source_scope=config.source_scope,
        require_approval=True,
    )
    snapshot_token = settings.RESOURCE_BOOKING_INTEGRATION.get("SNAPSHOT_SERVICE_TOKEN")
    if not snapshot_token:
        raise EntryGateError(
            "deployed Timetabler snapshot service token is unavailable"
        )
    action = os.environ.get("TT_PHASE1_E2E_ACTION", "entry")
    if action not in E2E_ACTIONS:
        raise EntryGateError("unsupported E2E action for watermark attestation")
    conflict_basis = None
    fixture_absence = None
    if action == "final":
        fixture_absence = final_fixture_absence(manifest, config.run_id)
    else:
        conflict_basis = conflict_contender_basis(manifest)

    session = TimetablerAdminSession(
        base_url=timetabler_base,
        email=config.timetabler_email,
        password=config.timetabler_password,
        run_id=config.run_id,
    )
    try:
        try:
            session.login_and_attest()
        except EntryGateError as error:
            raise EntryGateError(f"identity attestation failed: {error}") from error
        bearer = {
            "Authorization": f"Bearer {snapshot_token}",
            "User-Agent": f"timetabler-phase1-e2e/{config.run_id}",
            "X-Request-ID": f"{config.run_id}-entry-source-read",
        }
        try:
            health = _request_json(
                "GET",
                f"{timetabler_base}/api/integration/resource-booking/v1/health",
                headers=bearer,
            )
        except EntryGateError as error:
            raise EntryGateError(f"source health read failed: {error}") from error
        before_watermark = validate_timetabler_health(health, config.source_scope)
        if before_watermark != manifest["expected_stage_start_watermarks"][action]:
            raise EntryGateError(
                "Timetabler watermark does not match the approved stage fence"
            )
        snapshot_query = urlencode(
            {
                "source_scope": config.source_scope,
                "snapshot_watermark": before_watermark,
                "page_size": 1,
            }
        )
        try:
            snapshot = _request_json(
                "GET",
                f"{timetabler_base}/api/integration/resource-booking/v1/snapshot?{snapshot_query}",
                headers=bearer,
            )
        except EntryGateError as error:
            raise EntryGateError(
                f"fixed-watermark snapshot read failed: {error}"
            ) from error
        validate_snapshot(snapshot, before_watermark)

    finally:
        session.logout()
    result = {
        "entry_gate": "passed",
        "environment": config.environment,
        "run_id": config.run_id,
        "source_scope": config.source_scope,
        "before_outbox_watermark": before_watermark,
        "approved_fixture_case_count": len(manifest["cases"]),
        "timetabler_permission_count": session.permission_count,
        "reverse_delivery_enabled": False,
        "mutation_executed": False,
        "load_executed": False,
        "dr_executed": False,
    }
    if conflict_basis is not None:
        result["conflict_contender_basis"] = conflict_basis
    if fixture_absence is not None:
        result["fixture_absence"] = fixture_absence
    return result


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("mode", choices=("entry", "check-example", "host-check"))
    parser.add_argument("--manifest", help="Path used only by check-example")
    args = parser.parse_args()
    try:
        if args.mode == "host-check":
            print(json.dumps(attest_staging_host(), sort_keys=True))
            return 0
        if args.mode == "check-example":
            if not args.manifest:
                raise EntryGateError("--manifest is required for check-example")
            with open(args.manifest, encoding="utf-8") as stream:
                load_and_validate_manifest(
                    stream.read(),
                    environment="staging",
                    source_scope="default",
                    require_approval=False,
                )
            print(json.dumps({"fixture_inventory": "valid", "case_count": 13}))
            return 0
        result = execute_entry(EntryConfig.from_environment())
        print(json.dumps(result, sort_keys=True))
        return 0
    except EntryGateError as error:
        print(
            json.dumps({"entry_gate": "blocked", "reason": str(error)}), file=sys.stderr
        )
        return 3


if __name__ == "__main__":
    raise SystemExit(main())
