# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

"""OpenAPI fragment for the native-client observer routes."""

from __future__ import annotations

from solstone.convey.contract import (
    FieldSpec,
    OperationSpec,
    ParamSpec,
    RequestSpec,
    ResponseSpec,
)
from solstone.think.media import FORMATS

_OBSERVER_AUTH_PARAMS = (
    ParamSpec(
        "X-Solstone-Observer",
        "header",
        required=False,
        description="Observer handle. Preferred over Authorization when present.",
    ),
    ParamSpec(
        "Authorization",
        "header",
        required=False,
        description="Bearer observer handle.",
    ),
)


def _json_error(
    status: int,
    reason_codes: tuple[str, ...],
    description: str,
) -> ResponseSpec:
    return ResponseSpec(
        status=status,
        description=description,
        reason_codes=reason_codes,
    )


def _observer_auth_errors() -> tuple[ResponseSpec, ResponseSpec]:
    return (
        _json_error(
            401,
            ("auth_key_invalid", "auth_required"),
            "Observer handle missing or invalid.",
        ),
        _json_error(
            403,
            ("feature_unavailable", "pl_revoked"),
            "Observer is disabled or revoked.",
        ),
    )


def _media_format_description() -> str:
    by_kind: dict[str, list[str]] = {}
    for ext, mime, kind in FORMATS:
        by_kind.setdefault(kind, []).append(f"{ext.lstrip('.')} ({mime})")
    return "; ".join(
        f"{kind}: {', '.join(entries)}" for kind, entries in by_kind.items()
    )


_DAY_SEGMENT_COUNT_MAP = {
    "type": "object",
    "additionalProperties": {
        "type": "object",
        "additionalProperties": True,
        "properties": {"segments": {"type": "integer"}},
    },
}

_MANIFEST_SEGMENT_MAP = {
    "type": "object",
    "additionalProperties": {
        "type": "object",
        "additionalProperties": True,
        "properties": {
            "files": {
                "type": "array",
                "items": {
                    "type": "object",
                    "additionalProperties": True,
                    "properties": {
                        "name": {"type": "string"},
                        "sha256": {"type": "string"},
                        "size": {"type": "integer"},
                    },
                    "required": ["name", "sha256", "size"],
                },
            }
        },
        "required": ["files"],
    },
}

_SEGMENTS_RESPONSE_SCHEMA = {
    "oneOf": [
        {"$ref": "#/components/schemas/SegmentsEnvelope"},
        {
            "type": "array",
            "items": {"$ref": "#/components/schemas/SegmentItem"},
        },
    ]
}

_INGEST_UPLOAD_STATUS_SCHEMA = {
    "type": "string",
    "enum": ["ok", "duplicate", "collision", "conflict", "failed"],
    "x-vocabulary": {
        "classification": "closed",
        "id": "observer.ingestUpload.status",
        "unknown_value_behavior": "reject",
    },
}

_OBSERVER_STATUS_OK_SCHEMA = {
    "type": "string",
    "enum": ["ok"],
    "x-vocabulary": {
        "classification": "closed",
        "id": "observer.status.ok",
        "unknown_value_behavior": "reject",
    },
}

_SEGMENTS_RESPONSE_EXTENSIONS = {
    "x-vocabularies": {
        "X-Solstone-Protocol-Version": {
            "absent_or_unparseable": 1,
            "classification": "extensible_integer",
            "current": 2,
            "supported_response_variants": [1, 2],
            "unknown_value_behavior": "version_greater_or_equal_current_uses_v2",
        },
    }
}

_OBSERVER_SSE_EXTENSIONS = {
    "x-sse-frame-kinds": {
        "classification": "closed",
        "id": "observer.callosumStream.sse_frames",
        "unknown_value_behavior": "reject",
        "values": ["data", "error", "heartbeat"],
    },
}

_SEGMENT_ITEM_EXAMPLE = {
    "key": "143022_300",
    "observed": True,
    "files": [
        {
            "name": "screen.png",
            "size": 2048,
            "sha256": "5f70bf18a086007016bb522ec180fd0b",
            "status": "present",
        }
    ],
}

OPERATIONS: list[OperationSpec] = [
    OperationSpec(
        operation_id="observer.register",
        method="POST",
        rule="/app/observer/register",
        summary="Register observer",
        description=(
            "Register a trusted local or paired-link observer and lock its stream "
            "identity."
        ),
        request=RequestSpec(
            fields=(
                FieldSpec("platform", "string", required=True),
                FieldSpec("hostname", "string", required=True),
                FieldSpec("stream_type", "string", required=True),
                FieldSpec("version", "string", required=True),
                FieldSpec("label", "string"),
            ),
            example={
                "platform": "linux",
                "hostname": "archon",
                "stream_type": "desktop",
                "version": "1.4.0",
                "label": "Archon desktop",
            },
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Observer registration descriptor.",
                named_fields=(
                    FieldSpec("key", "string", required=True),
                    FieldSpec("prefix", "string", required=True),
                    FieldSpec("name", "string", required=True),
                    FieldSpec("ingest_url", "string", required=True),
                    FieldSpec("protocol_version", "integer", required=True),
                ),
                example={
                    "key": "x7J7k2observerHandle",
                    "prefix": "x7J7k2ob",
                    "name": "archon",
                    "ingest_url": "/app/observer/ingest",
                    "protocol_version": 2,
                },
            ),
            _json_error(
                400,
                ("invalid_segment_or_stream", "missing_required_field"),
                "Register request failed validation.",
            ),
            _json_error(
                403,
                ("local_request_only",),
                "Register caller was not trusted localhost or paired link.",
            ),
            _json_error(
                500,
                ("settings_operation_failed",),
                "Observer record could not be saved.",
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.ingestUpload",
        method="POST",
        rule="/app/observer/ingest",
        summary="Upload observer segment files",
        description=(
            "Upload one capture segment as multipart form data and trigger local "
            "observe processing. Media parts must use a registry container format "
            f"— {_media_format_description()}. Headerless raw streams are not "
            "accepted as media containers; raw PCM must be WAV-wrapped before "
            "upload. Video segment timestamps are boundary-relative real capture "
            "offsets (seconds from the segment start), never synthetic frame "
            "indices. Duplicate identity is derived from the segment directory, "
            "with terminal processing proof holding absent legacy raw media and "
            "a journal-authored ingest manifest adding hash-strength when "
            "present, not from the observer history log. `collision` means "
            "conflicting content at the requested key (same stored filename, "
            "different bytes); an occupied but non-conflicting key joins that "
            "segment. Reserved names such as `stream.json` and `ingest.json` are "
            "never written from client bytes and never appear in segment "
            "listings as held. Every `duplicate` appends a corroborating history "
            "record so `/ingest/segments/<day>` can prove the held files by "
            "filename and sha."
        ),
        parameters=_OBSERVER_AUTH_PARAMS,
        request=RequestSpec(
            content_type="multipart/form-data",
            fields=(
                FieldSpec("segment", "string", required=True),
                FieldSpec("day", "string", required=True),
                FieldSpec(
                    "files",
                    "array",
                    required=True,
                    raw_schema={
                        "type": "array",
                        "items": {"type": "string", "format": "binary"},
                    },
                ),
                FieldSpec("host", "string"),
                FieldSpec("platform", "string"),
                FieldSpec("meta", "string"),
            ),
            example={
                "segment": "143022_300",
                "day": "20260618",
                "files": ["screen.png", "audio.flac"],
                "host": "archon",
                "platform": "linux",
                "meta": '{"facet":"work"}',
            },
        ),
        responses=(
            ResponseSpec(
                status=200,
                description=(
                    "Upload accepted, collision-adjusted, or duplicate. On "
                    "`duplicate`, `existing_segment` is the authoritative stored "
                    "segment key the client must adopt (do not re-upload); the "
                    "server also appends a history record so the segment listing "
                    "corroborates the duplicate. On `collision`, the requested "
                    "key held conflicting content (same stored filename, "
                    "different bytes), and the returned `segment` is the "
                    "authoritative remapped key the client must adopt for "
                    "subsequent references. An occupied key without conflicting "
                    "content is not a collision; the upload joins that segment."
                ),
                named_fields=(
                    FieldSpec(
                        "status",
                        "string",
                        required=True,
                        raw_schema=_INGEST_UPLOAD_STATUS_SCHEMA,
                    ),
                    FieldSpec("segment", "string"),
                    FieldSpec("files", "array", item_type="string"),
                    FieldSpec("bytes", "integer"),
                    FieldSpec("existing_segment", "string"),
                    FieldSpec("segment_original", "string"),
                    FieldSpec("message", "string"),
                ),
                example={
                    "normal": {
                        "summary": "New segment accepted",
                        "value": {
                            "status": "ok",
                            "segment": "143022_300",
                            "files": ["screen.png", "audio.flac"],
                            "bytes": 524288,
                        },
                    },
                    "duplicate": {
                        "summary": "Duplicate segment",
                        "value": {
                            "status": "duplicate",
                            "existing_segment": "143022_300",
                            "message": "All files already received",
                        },
                    },
                },
            ),
            *_observer_auth_errors(),
            _json_error(
                400,
                (
                    "ingest_no_files",
                    "invalid_day",
                    "invalid_segment_or_stream",
                    "missing_required_field",
                ),
                "Upload request failed validation.",
            ),
            _json_error(
                409,
                ("ingest_sidecar_conflict",),
                "Uploaded sidecar metadata conflicts with an existing segment. "
                "The journal already holds different bytes under the named "
                "filename; strip the conflicting file and re-send. The response "
                "names `conflicting_files` and the held `existing_segment`.",
            ),
            _json_error(
                422,
                ("ingest_contract_invalid",),
                "Uploaded contract-covered file failed journal contract validation.",
            ),
            _json_error(
                500,
                ("ingest_storage_failed",),
                "Uploaded file could not be stored.",
            ),
            _json_error(
                507,
                ("ingest_storage_failed",),
                "No segment slot was available after collision handling.",
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.ingestEvent",
        method="POST",
        rule="/app/observer/ingest/event",
        summary="Relay observer event",
        description="Relay an observer-originated event onto the local Callosum bus.",
        parameters=_OBSERVER_AUTH_PARAMS,
        request=RequestSpec(
            fields=(
                FieldSpec("tract", "string", required=True),
                FieldSpec("event", "string", required=True),
            ),
            example={
                "tract": "observe",
                "event": "status",
                "state": "recording",
            },
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Event relayed.",
                named_fields=(
                    FieldSpec(
                        "status",
                        "string",
                        required=True,
                        raw_schema=_OBSERVER_STATUS_OK_SCHEMA,
                    ),
                ),
                example={"status": "ok"},
            ),
            *_observer_auth_errors(),
            _json_error(
                400,
                ("missing_required_field",),
                "Tract or event was missing.",
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.health",
        method="POST",
        rule="/app/observer/health",
        summary="Record observer health beacon",
        description=(
            "Record a diagnostics-only observer health beacon on the observer "
            "registry record."
        ),
        parameters=_OBSERVER_AUTH_PARAMS,
        request=RequestSpec(
            fields=(
                FieldSpec("name", "string", required=False),
                FieldSpec("stream_type", "string", required=False),
                FieldSpec("version", "string", required=False),
                FieldSpec("uptime", "integer", required=False),
                FieldSpec("last_successful_sync", "integer", required=False),
                FieldSpec("pending_queue_depth", "integer", required=False),
                FieldSpec("recent_error_count", "integer", required=False),
                FieldSpec("last_error_reason", "string", required=False),
            ),
            example={
                "name": "phone",
                "stream_type": "phone",
                "version": "1.2.3",
                "uptime": 42,
                "last_successful_sync": 1767100000000,
                "pending_queue_depth": 3,
                "recent_error_count": 0,
                "last_error_reason": None,
            },
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Health beacon recorded.",
                named_fields=(FieldSpec("status", "string", required=True),),
                example={"status": "ok"},
            ),
            *_observer_auth_errors(),
        ),
    ),
    OperationSpec(
        operation_id="observer.ingestManifest",
        method="GET",
        rule="/app/observer/ingest/manifest",
        summary="List observer manifest days",
        description="Return days with uploaded observer segment history.",
        parameters=_OBSERVER_AUTH_PARAMS,
        responses=(
            ResponseSpec(
                status=200,
                description="Available manifest days keyed by day.",
                named_fields=(
                    FieldSpec(
                        "days",
                        "object",
                        required=True,
                        raw_schema=_DAY_SEGMENT_COUNT_MAP,
                    ),
                ),
                example={"days": {"20260618": {"segments": 2}}},
            ),
            *_observer_auth_errors(),
        ),
    ),
    OperationSpec(
        operation_id="observer.ingestManifestDay",
        method="GET",
        rule="/app/observer/ingest/manifest/<day>",
        summary="Read transfer manifest for a day",
        description="Return file hashes and sizes for every segment on one day.",
        parameters=(
            *_OBSERVER_AUTH_PARAMS,
            ParamSpec("day", "path", required=True, description="YYYYMMDD day."),
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Day transfer manifest.",
                named_fields=(
                    FieldSpec("version", "integer", required=True),
                    FieldSpec("day", "string", required=True),
                    FieldSpec("created_at", "integer", required=True),
                    FieldSpec("host", "string", required=True),
                    FieldSpec(
                        "segments",
                        "object",
                        required=True,
                        raw_schema=_MANIFEST_SEGMENT_MAP,
                    ),
                ),
                example={
                    "version": 1,
                    "day": "20260618",
                    "created_at": 1781803200000,
                    "host": "archon",
                    "segments": {
                        "archon/143022_300": {
                            "files": [
                                {
                                    "name": "screen.png",
                                    "sha256": "5f70bf18a086007016bb522ec180fd0b",
                                    "size": 2048,
                                }
                            ]
                        }
                    },
                },
            ),
            *_observer_auth_errors(),
            _json_error(
                400,
                ("invalid_day",),
                "Day path parameter was invalid.",
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.ingestSegments",
        method="GET",
        rule="/app/observer/ingest/segments/<day>",
        summary="List uploaded observer segments",
        description=(
            "Return segment upload history for one day. Protocol version 2 and "
            "newer receive a collection envelope; older or absent protocol "
            "headers receive a legacy bare array. Per-file `status` is `present` "
            "(file at its recorded path), `processed` (the recorded raw media is "
            "absent because journal processing terminally consumed it, proven by "
            "a validated `solstone.processing.v1` record in the same-stem sidecar "
            "at the recorded segment path), or `missing` (not found). A local file "
            "is proven held by the journal only when its listing entry is "
            "`present` or `processed` AND the entry `sha256` matches the local "
            "file; a `missing` status — or any local file the listing does not "
            "positively account for — is needs-upload, and the client must never "
            "delete a local copy on that basis. Confirm-before-delete matches "
            "every upload-eligible local file to a listing entry per (filename, "
            "sha256) — the listing filename is `submitted_name` when present, "
            "else `name` — and deletes only on a proven-held match. Re-uploading "
            "the same bytes for a `processed` entry returns `duplicate` and "
            "creates no new segment directory. A peer that does not recognize "
            "`processed` degrades safely: it treats the file as not-held, "
            "re-uploads, and the server answers `duplicate`. `submitted_name` "
            "appears only when the stored filename differs from the submitted "
            "one; segment-level `original_key` carries the client's originally "
            "requested segment key when a collision remapped it."
        ),
        parameters=(
            *_OBSERVER_AUTH_PARAMS,
            ParamSpec("day", "path", required=True, description="YYYYMMDD day."),
            ParamSpec(
                "stream",
                "query",
                required=False,
                description="Fallback stream for legacy observer history rows.",
            ),
            ParamSpec(
                "X-Solstone-Protocol-Version",
                "header",
                type="integer",
                required=False,
                description=(
                    ">= current protocol version returns the collection envelope; "
                    "lower or absent returns the legacy bare array."
                ),
            ),
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Segment verification response.",
                raw_schema=_SEGMENTS_RESPONSE_SCHEMA,
                example={
                    "legacy": {
                        "summary": "Legacy bare array",
                        "value": [_SEGMENT_ITEM_EXAMPLE],
                    },
                    "v2": {
                        "summary": "Protocol v2 envelope",
                        "value": {
                            "items": [_SEGMENT_ITEM_EXAMPLE],
                            "total": 1,
                            "protocol_version": 2,
                        },
                    },
                },
                extensions=_SEGMENTS_RESPONSE_EXTENSIONS,
            ),
            *_observer_auth_errors(),
            _json_error(
                400,
                ("invalid_day",),
                "Day path parameter was invalid.",
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.callosumStream",
        method="GET",
        rule="/app/observer/callosum",
        summary="Stream Callosum events",
        description=(
            "Open an observer-authenticated Server-Sent Events feed. Frames: data "
            "`data: {json}\\n\\n`; heartbeat `: heartbeat\\n\\n`; error "
            "`event: error\\ndata: {Error}\\n\\n`."
        ),
        parameters=_OBSERVER_AUTH_PARAMS,
        responses=(
            ResponseSpec(
                status=200,
                description=(
                    "Callosum event stream. Data frames carry CallosumEvent JSON; "
                    "heartbeat frames are comments; error frames carry Error JSON."
                ),
                content_type="text/event-stream",
                free_form=True,
                raw_schema={"$ref": "#/components/schemas/CallosumEvent"},
                example={
                    "tract": "observe",
                    "event": "observing",
                    "ts": 1781803200000,
                    "day": "20260618",
                    "segment": "143022_300",
                },
                extensions={
                    **_OBSERVER_SSE_EXTENSIONS,
                    "x-sse-error-frame": {
                        "schema": {"$ref": "#/components/schemas/Error"},
                        "x-reason-codes": [
                            "auth_required",
                            "feature_unavailable",
                            "pl_revoked",
                        ],
                        "description": (
                            "In-stream `event: error` frames emit only these. "
                            "auth_key_invalid is excluded here because key "
                            "validity is established at stream open "
                            "(routes.py:259-261); mid-stream re-checks only "
                            "cover observer missing/revoked/disabled "
                            "(routes.py:281-292)."
                        ),
                    },
                },
            ),
            _json_error(
                401,
                (
                    "auth_key_invalid",
                    "auth_required",
                    "feature_unavailable",
                    "pl_revoked",
                ),
                (
                    "Stream-open observer auth failure; codes include 401 and "
                    "403 outcomes."
                ),
            ),
        ),
    ),
    OperationSpec(
        operation_id="observer.deleteSource",
        method="DELETE",
        rule="/app/observer/source/<stream>",
        summary="Delete observer source",
        description="Delete an allowed observer-owned source stream from the journal.",
        parameters=(
            *_OBSERVER_AUTH_PARAMS,
            ParamSpec(
                "stream",
                "path",
                required=True,
                description="Source stream; only 'location' is deletable.",
            ),
        ),
        responses=(
            ResponseSpec(
                status=200,
                description="Source deletion receipt.",
                named_fields=(
                    FieldSpec("target", "object", required=True),
                    FieldSpec("removed", "object", required=True),
                    FieldSpec("not_confirmed", "array", required=True),
                    FieldSpec("not_removed", "array", required=True),
                    FieldSpec("backup_hosted", "string", required=True),
                ),
                example={
                    "target": {
                        "stream": "location",
                        "journal": "/journal",
                    },
                    "removed": {
                        "originals": 0,
                        "segments": 0,
                        "mixed_segments": 0,
                        "in_segment_derived": 0,
                        "index_chunks": 0,
                        "stream_identity": 0,
                        "history_rows": 0,
                        "days": 0,
                    },
                    "not_confirmed": [],
                    "not_removed": [],
                    "backup_hosted": "not confirmed",
                },
            ),
            _json_error(
                400,
                ("invalid_segment_or_stream",),
                "Only known source streams can be deleted.",
            ),
            *_observer_auth_errors(),
        ),
    ),
]

__all__ = ["OPERATIONS"]
