"""Build the program message consumed by the runtime ``fetch_oss_file`` tool."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any, Dict, Protocol
from urllib.parse import quote, urlparse

from arr_ingestion.contracts import ArtifactRef, ROLE_CONTRACTS
from arr_processing.contracts import ProcessingRequest, canonical_json_bytes
from arr_processing.errors import ProcessingError
from arr_storage.aliyun_oss_v2 import AliyunOssConfig
from arr_storage.contracts import valid_object_key


PROGRAM_INPUT_VERSION = "arr-opera-daily-program-input-3"


class ProcessingSourceResolver(Protocol):
    def source_for_attempt(self, job_id: str, attempt_no: int) -> ArtifactRef:
        ...


@dataclass(frozen=True)
class OssProcessingMessageBuilder:
    """Resolve one ARR-owned source to its public-read HTTPS object URL.

    The Agent receives an attachment-shaped descriptor containing the exact URL
    that ``fetch_oss_file`` must use, plus non-secret provenance and integrity
    fields. AccessKey, Secret, local paths and signed URLs never enter the
    message.
    """

    config: AliyunOssConfig
    sources: ProcessingSourceResolver

    def message(self, request: ProcessingRequest) -> str:
        source = self.sources.source_for_attempt(request.job_id, request.attempt_no)
        self._validate_source(request, source)
        endpoint_host = self._endpoint_host()
        if request.submission_grant is None:
            raise ProcessingError(
                "PROCESSING_SUBMISSION_GRANT_MISSING",
                "direct result submission grant is unavailable",
            )
        payload: Dict[str, Any] = {
            "contract_version": PROGRAM_INPUT_VERSION,
            "job_id": request.job_id,
            "attempt_no": request.attempt_no,
            "source_file_id": request.source_file_id,
            "processor_version": request.processor_version,
            "rule_set_sha256": request.rule_set_sha256,
            "oss_attachments": [
                {
                    "id": request.source_file_id,
                    "name": source.original_filename,
                    "content_type": source.mime_type,
                    "size": source.byte_size,
                    "sha256": source.sha256,
                    "current_or_history": "current",
                    "source_ref": {"source": "oss_attachments", "index": 0},
                    "oss": {
                        "bucket": self.config.bucket,
                        "endpoint": endpoint_host,
                        "object_key": source.object_key,
                        "url": self._public_object_url(
                            endpoint_host,
                            source.object_key,
                        ),
                    },
                }
            ],
            "oss_inline_images": [],
            "attachment_fetch_policy": {
                "tool_name": "fetch_oss_file",
                "required_before_skill_call": True,
                "applies_to": ["oss_attachments"],
                "output_field": "current_attachments",
            },
            "result_submission": {
                "tool_name": "arr_submit_processing_result",
                "submission_grant": request.submission_grant,
                "job_id": request.job_id,
                "attempt_no": request.attempt_no,
                "payload_source": "structured-result.json",
                "required_after_successful_skill_call": True,
            },
        }
        return canonical_json_bytes(payload).decode("utf-8")

    @staticmethod
    def _validate_source(request: ProcessingRequest, source: ArtifactRef) -> None:
        expected_mime = ROLE_CONTRACTS["source_xml"][2]
        if (
            not isinstance(source, ArtifactRef)
            or source.role != "source_xml"
            or source.file_kind != "opera_xml"
            or source.mime_type != expected_mime
            or source.original_filename != "source.xml"
            or not valid_object_key(source.object_key)
            or request.job_id not in source.object_key.split("/")
        ):
            raise ProcessingError(
                "PROCESSING_SOURCE_INVALID",
                "processing source object is unavailable",
            )

    def _endpoint_host(self) -> str:
        expected_host = f"oss-{self.config.region}.aliyuncs.com"
        if self.config.endpoint:
            parsed = urlparse(self.config.endpoint)
            if (
                parsed.scheme != "https"
                or parsed.hostname != expected_host
                or parsed.username is not None
                or parsed.password is not None
                or parsed.port is not None
            ):
                raise ProcessingError(
                    "PROCESSING_SOURCE_INVALID",
                    "public OSS endpoint is unavailable",
                )
        return expected_host

    def _public_object_url(self, endpoint_host: str, object_key: str) -> str:
        encoded_key = quote(object_key, safe="/")
        return f"https://{self.config.bucket}.{endpoint_host}/{encoded_key}"
