"""Authenticated Booking Excel extraction and review coordinator."""

from __future__ import annotations

import os
import tempfile
import uuid
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional, Protocol

from arr_ingestion.contracts import ArtifactRef, IngestionError
from arr_storage.store import ManagedObjectStore
from booking_ingestion.excel import (
    BookingExcelError,
    ExcelDocument,
    parse_excel_bytes,
    validate_excel_filename,
)
from booking_ingestion.excel_postgres import (
    BookingExcelRepositoryError,
    BookingSourceSummary,
)
from booking_ingestion.excel_review import (
    BookingDraftItem,
    BookingDraftPage,
    validate_review_item_ids,
)

from .contracts import PortalError


class BookingSourceRepository(Protocol):
    def current_source(self) -> Optional[BookingSourceSummary]:
        ...

    def current_draft(
        self,
        *,
        limit: int = 50,
        offset: int = 0,
    ) -> Optional[BookingDraftPage]:
        ...

    def create_draft(
        self,
        draft_id: str,
        source: ArtifactRef,
        document: ExcelDocument,
        *,
        limit: int = 50,
    ) -> BookingDraftPage:
        ...

    def update_item(
        self,
        draft_id: str,
        item_id: int,
        room_type_code: str,
        quantity: int,
    ) -> BookingDraftItem:
        ...

    def delete_item(self, draft_id: str, item_id: int) -> None:
        ...

    def delete_items(self, draft_id: str, item_ids: object) -> None:
        ...

    def discard_draft(self, draft_id: str) -> None:
        ...

    def activate_draft(self, draft_id: str) -> BookingSourceSummary:
        ...


class BookingSourceCoordinator(Protocol):
    def current(self) -> Optional[Dict[str, object]]:
        ...

    def draft(self, limit: int = 50, offset: int = 0) -> Optional[Dict[str, object]]:
        ...

    def submit(self, original_filename: str, payload: bytes) -> Dict[str, object]:
        ...

    def update_item(
        self,
        draft_id: str,
        item_id: int,
        room_type: str,
        quantity: int,
    ) -> Dict[str, object]:
        ...

    def delete_item(self, draft_id: str, item_id: int) -> Dict[str, object]:
        ...

    def delete_items(self, draft_id: str, item_ids: object) -> Dict[str, object]:
        ...

    def discard(self, draft_id: str) -> Dict[str, object]:
        ...

    def activate(self, draft_id: str) -> Dict[str, object]:
        ...


class UnavailableBookingSourceCoordinator:
    @staticmethod
    def _unavailable() -> PortalError:
        return PortalError(
            "BOOKING_EXCEL_SERVICE_UNAVAILABLE",
            "Excel 数据源服务暂未就绪",
            503,
        )

    def current(self) -> Optional[Dict[str, object]]:
        raise self._unavailable()

    def draft(self, limit: int = 50, offset: int = 0) -> Optional[Dict[str, object]]:
        _ = (limit, offset)
        raise self._unavailable()

    def submit(self, original_filename: str, payload: bytes) -> Dict[str, object]:
        _ = (original_filename, payload)
        raise self._unavailable()

    def update_item(
        self,
        draft_id: str,
        item_id: int,
        room_type: str,
        quantity: int,
    ) -> Dict[str, object]:
        _ = (draft_id, item_id, room_type, quantity)
        raise self._unavailable()

    def delete_item(self, draft_id: str, item_id: int) -> Dict[str, object]:
        _ = (draft_id, item_id)
        raise self._unavailable()

    def delete_items(self, draft_id: str, item_ids: object) -> Dict[str, object]:
        _ = (draft_id, item_ids)
        raise self._unavailable()

    def discard(self, draft_id: str) -> Dict[str, object]:
        _ = draft_id
        raise self._unavailable()

    def activate(self, draft_id: str) -> Dict[str, object]:
        _ = draft_id
        raise self._unavailable()


@dataclass
class ProgramBookingSourceCoordinator:
    object_store: ManagedObjectStore
    repository: BookingSourceRepository

    def current(self) -> Optional[Dict[str, object]]:
        try:
            summary = self.repository.current_source()
            return summary.to_dict() if summary is not None else None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    def draft(self, limit: int = 50, offset: int = 0) -> Optional[Dict[str, object]]:
        try:
            page = self.repository.current_draft(limit=limit, offset=offset)
            return page.to_dict() if page is not None else None
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    def submit(self, original_filename: str, payload: bytes) -> Dict[str, object]:
        try:
            filename = validate_excel_filename(original_filename)
            document = parse_excel_bytes(payload)
            draft_id = "bookingdraft-" + uuid.uuid4().hex
            with tempfile.TemporaryDirectory(prefix="arr-booking-excel-") as temporary:
                source_path = Path(temporary) / "booking-source.xlsx"
                self._write_private(source_path, payload)
                stored = self.object_store.upload_committed(
                    job_id=draft_id,
                    attempt_no=1,
                    role="booking_source",
                    source=source_path,
                    original_filename=filename,
                    expected_sha256=document.source_sha256,
                    expected_byte_size=document.source_byte_size,
                )
                page = self.repository.create_draft(
                    draft_id,
                    stored.to_artifact_ref(),
                    document,
                )
            return page.to_dict()
        except PortalError:
            raise
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except IngestionError as error:
            retryable = error.retryable or error.code.startswith("OBJECT_")
            raise PortalError(
                error.code,
                error.safe_message,
                503 if retryable else 422,
            ) from None
        except Exception:
            raise PortalError(
                "BOOKING_EXCEL_PROCESSING_FAILED",
                "Excel 文件未能完成提取",
                503,
            ) from None

    def update_item(
        self,
        draft_id: str,
        item_id: int,
        room_type: str,
        quantity: int,
    ) -> Dict[str, object]:
        try:
            return self.repository.update_item(
                draft_id,
                item_id,
                room_type,
                quantity,
            ).to_dict()
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    def delete_item(self, draft_id: str, item_id: int) -> Dict[str, object]:
        result = self.delete_items(draft_id, [item_id])
        return {"draft_id": draft_id, "item_id": item_id, "deleted": result["deleted"]}

    def delete_items(self, draft_id: str, item_ids: object) -> Dict[str, object]:
        try:
            validated_ids = validate_review_item_ids(item_ids)
            self.repository.delete_items(draft_id, validated_ids)
            return {
                "draft_id": draft_id,
                "item_ids": list(validated_ids),
                "deleted_count": len(validated_ids),
                "deleted": True,
            }
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    def discard(self, draft_id: str) -> Dict[str, object]:
        try:
            self.repository.discard_draft(draft_id)
            return {"draft_id": draft_id, "discarded": True}
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    def activate(self, draft_id: str) -> Dict[str, object]:
        try:
            return self.repository.activate_draft(draft_id).to_dict()
        except BookingExcelError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None
        except BookingExcelRepositoryError as error:
            raise PortalError(error.code, error.safe_message, error.status) from None

    @staticmethod
    def _write_private(path: Path, payload: bytes) -> None:
        path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
        descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
        try:
            with os.fdopen(descriptor, "wb") as target:
                descriptor = -1
                target.write(payload)
                target.flush()
                os.fsync(target.fileno())
        finally:
            if descriptor >= 0:
                os.close(descriptor)
