"""Atomic PostgreSQL activation for deterministic Booking Excel imports."""

from __future__ import annotations

import hashlib
import json
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Callable, Mapping, Optional

from arr_ingestion.contracts import ArtifactRef

from .excel import (
    PROCESSOR_NAME,
    PROCESSOR_VERSION,
    RESULT_SCHEMA_VERSION,
    XLSX_MIME,
    ExcelDocument,
    build_excel_parse_result,
    canonical_json_bytes,
    rule_set_sha256,
)


TARGET_DATABASE = "booking_test"
DATABASE_ENV = "ARR_DATABASE_URL"
ARTIFACT_STORAGE_PROVIDER = "oss"
ARTIFACT_BUCKET_ALIAS = "arr-private"


class BookingExcelRepositoryError(RuntimeError):
    def __init__(self, code: str, safe_message: str, status: int = 503):
        super().__init__(safe_message)
        self.code = code
        self.safe_message = safe_message
        self.status = status


@dataclass(frozen=True)
class DatabaseConfig:
    dsn: str

    @classmethod
    def from_environment(cls) -> "DatabaseConfig":
        dsn = os.environ.get(DATABASE_ENV, "").strip()
        if not dsn:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_CONFIG_MISSING",
                "Booking Excel 数据库配置缺失",
            )
        return cls(dsn=dsn)


@dataclass(frozen=True)
class BookingSourceSummary:
    source_batch_id: int
    source_kind: str
    filename: Optional[str]
    source_rows: int
    worksheet_count: int
    distinct_group_codes: int
    room_quantity: int
    activated_at: Optional[datetime]
    disposition: str = "current"

    def to_dict(self) -> dict[str, object]:
        return {
            "source_batch_id": self.source_batch_id,
            "source_type": (
                "excel" if self.source_kind == "booking_excel" else "historical"
            ),
            "filename": self.filename if self.source_kind == "booking_excel" else None,
            "source_rows": self.source_rows,
            "worksheet_count": self.worksheet_count,
            "distinct_group_codes": self.distinct_group_codes,
            "room_quantity": self.room_quantity,
            "activated_at": (
                self.activated_at.isoformat() if self.activated_at is not None else None
            ),
            "disposition": self.disposition,
        }


CURRENT_SOURCE_SQL = """
SELECT
    batch.id,
    batch.source_kind,
    CASE
        WHEN batch.source_kind = 'booking_excel' THEN artifact.original_filename
        ELSE NULL
    END AS uploaded_filename,
    count(source.id)::bigint AS source_rows,
    count(DISTINCT source.source_worksheet)::bigint AS worksheet_count,
    count(DISTINCT source.group_code_key)::bigint AS distinct_group_codes,
    COALESCE(sum(source.no_of_rooms), 0)::bigint AS room_quantity,
    active_source.activated_at
FROM booking.current_source_batch AS active_source
JOIN booking.source_batches AS batch
  ON batch.id = active_source.source_batch_id
JOIN ingestion.artifacts AS artifact
  ON artifact.id = batch.source_artifact_id
LEFT JOIN booking.source_rows AS source
  ON source.source_batch_id = batch.id
WHERE active_source.singleton
  AND batch.batch_status = 'accepted'
GROUP BY
    batch.id,
    batch.source_kind,
    artifact.original_filename,
    active_source.activated_at
""".strip()


BATCH_SUMMARY_SQL = """
SELECT
    batch.id,
    batch.source_kind,
    CASE
        WHEN batch.source_kind = 'booking_excel' THEN artifact.original_filename
        ELSE NULL
    END AS uploaded_filename,
    count(source.id)::bigint AS source_rows,
    count(DISTINCT source.source_worksheet)::bigint AS worksheet_count,
    count(DISTINCT source.group_code_key)::bigint AS distinct_group_codes,
    COALESCE(sum(source.no_of_rooms), 0)::bigint AS room_quantity,
    active_source.activated_at
FROM booking.source_batches AS batch
JOIN ingestion.artifacts AS artifact
  ON artifact.id = batch.source_artifact_id
LEFT JOIN booking.source_rows AS source
  ON source.source_batch_id = batch.id
LEFT JOIN booking.current_source_batch AS active_source
  ON active_source.source_batch_id = batch.id
 AND active_source.singleton
WHERE batch.id = %s
  AND batch.batch_status = 'accepted'
GROUP BY
    batch.id,
    batch.source_kind,
    artifact.original_filename,
    active_source.activated_at
""".strip()


def _default_connect(dsn: str) -> Any:
    try:
        import psycopg  # type: ignore[import-not-found]
    except ImportError:
        raise BookingExcelRepositoryError(
            "BOOKING_EXCEL_DATABASE_DRIVER_UNAVAILABLE",
            "Booking Excel 数据库驱动不可用",
        ) from None
    try:
        return psycopg.connect(dsn, autocommit=False)
    except Exception:
        raise BookingExcelRepositoryError(
            "BOOKING_EXCEL_DATABASE_UNAVAILABLE",
            "Booking Excel 数据库暂不可用",
        ) from None


class PostgresBookingExcelRepository:
    def __init__(
        self,
        config: DatabaseConfig,
        connect: Optional[Callable[[str], Any]] = None,
    ) -> None:
        self._config = config
        self._connect = connect or _default_connect

    def _open(self) -> Any:
        try:
            return self._connect(self._config.dsn)
        except BookingExcelRepositoryError:
            raise
        except Exception:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_UNAVAILABLE",
                "Booking Excel 数据库暂不可用",
            ) from None

    @staticmethod
    def _begin(cursor: Any, *, read_only: bool = False) -> None:
        mode = "REPEATABLE READ READ ONLY" if read_only else "SERIALIZABLE"
        cursor.execute(f"SET TRANSACTION ISOLATION LEVEL {mode}")
        cursor.execute("SET LOCAL lock_timeout = '10s'")
        cursor.execute("SET LOCAL statement_timeout = '120s'")
        cursor.execute(
            "SELECT current_database(), to_regclass('booking.current_source_batch')"
        )
        row = cursor.fetchone()
        if not row or row[0] != TARGET_DATABASE or row[1] is None:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_NOT_READY",
                "Booking Excel 数据库迁移尚未就绪",
            )

    @staticmethod
    def _summary_from_row(
        row: object,
        *,
        disposition: str = "current",
    ) -> Optional[BookingSourceSummary]:
        if row is None:
            return None
        values = tuple(row)  # type: ignore[arg-type]
        return BookingSourceSummary(
            source_batch_id=int(values[0]),
            source_kind=str(values[1]),
            filename=str(values[2]) if values[2] else None,
            source_rows=int(values[3]),
            worksheet_count=int(values[4]),
            distinct_group_codes=int(values[5]),
            room_quantity=int(values[6]),
            activated_at=values[7] if isinstance(values[7], datetime) else None,
            disposition=disposition,
        )

    def current_source(self) -> Optional[BookingSourceSummary]:
        connection = self._open()
        try:
            with connection.cursor() as cursor:
                self._begin(cursor, read_only=True)
                cursor.execute(CURRENT_SOURCE_SQL)
                return self._summary_from_row(cursor.fetchone())
        except BookingExcelRepositoryError:
            raise
        except Exception:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_UNAVAILABLE",
                "当前 Excel 数据源暂时无法读取",
            ) from None
        finally:
            try:
                connection.rollback()
            finally:
                connection.close()

    @staticmethod
    def _find_existing_batch(cursor: Any, source_sha256: str) -> Optional[int]:
        cursor.execute(
            """
            SELECT batch.id
            FROM booking.source_batches AS batch
            JOIN ingestion.artifacts AS artifact
              ON artifact.id = batch.source_artifact_id
            WHERE batch.source_kind = 'booking_excel'
              AND batch.batch_status = 'accepted'
              AND artifact.artifact_kind = 'booking_excel'
              AND artifact.sha256 = %s
            ORDER BY batch.id
            LIMIT 1
            FOR UPDATE OF batch
            """,
            (source_sha256,),
        )
        row = cursor.fetchone()
        return int(row[0]) if row is not None else None

    @staticmethod
    def _activate(cursor: Any, source_batch_id: int) -> bool:
        cursor.execute(
            """
            SELECT source_batch_id
            FROM booking.current_source_batch
            WHERE singleton
            FOR UPDATE
            """
        )
        current = cursor.fetchone()
        already_active = current is not None and int(current[0]) == source_batch_id
        if not already_active:
            cursor.execute(
                """
                INSERT INTO booking.current_source_batch (
                    singleton,
                    source_batch_id,
                    activated_at
                )
                VALUES (true, %s, now())
                ON CONFLICT (singleton) DO UPDATE
                SET source_batch_id = EXCLUDED.source_batch_id,
                    activated_at = EXCLUDED.activated_at
                """,
                (source_batch_id,),
            )
        return already_active

    @staticmethod
    def _batch_summary(
        cursor: Any,
        source_batch_id: int,
        disposition: str,
    ) -> BookingSourceSummary:
        cursor.execute(BATCH_SUMMARY_SQL, (source_batch_id,))
        summary = PostgresBookingExcelRepository._summary_from_row(
            cursor.fetchone(),
            disposition=disposition,
        )
        if summary is None:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_STATE_INVALID",
                "Booking Excel 数据源状态无效",
            )
        return summary

    def activate_existing(
        self,
        source_sha256: str,
    ) -> Optional[BookingSourceSummary]:
        connection = self._open()
        try:
            with connection.cursor() as cursor:
                self._begin(cursor)
                cursor.execute(
                    "SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
                    ("arr_booking_excel_import",),
                )
                source_batch_id = self._find_existing_batch(cursor, source_sha256)
                if source_batch_id is None:
                    connection.rollback()
                    return None
                already_active = self._activate(cursor, source_batch_id)
                summary = self._batch_summary(
                    cursor,
                    source_batch_id,
                    "already_active" if already_active else "reactivated",
                )
            connection.commit()
            return summary
        except BookingExcelRepositoryError:
            connection.rollback()
            raise
        except Exception:
            connection.rollback()
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_WRITE_FAILED",
                "Excel 已校验，但数据源未能完成入库",
            ) from None
        finally:
            connection.close()

    @staticmethod
    def _ensure_artifact(cursor: Any, reference: ArtifactRef) -> int:
        cursor.execute(
            """
            SELECT
                id,
                artifact_kind,
                original_filename,
                sha256,
                byte_size,
                mime_type
            FROM ingestion.artifacts
            WHERE storage_provider = %s
              AND bucket_alias = %s
              AND object_key = %s
              AND object_version_id IS NULL
            FOR SHARE
            """,
            (
                ARTIFACT_STORAGE_PROVIDER,
                ARTIFACT_BUCKET_ALIAS,
                reference.object_key,
            ),
        )
        row = cursor.fetchone()
        if row is not None:
            if (
                str(row[1]) != reference.file_kind
                or str(row[2]) != reference.original_filename
                or str(row[3]) != reference.sha256
                or int(row[4]) != reference.byte_size
                or str(row[5] or "") != reference.mime_type
            ):
                raise BookingExcelRepositoryError(
                    "BOOKING_EXCEL_ARTIFACT_CONFLICT",
                    "Excel 工件身份冲突",
                    409,
                )
            return int(row[0])
        cursor.execute(
            """
            INSERT INTO ingestion.artifacts (
                artifact_kind,
                storage_provider,
                bucket_alias,
                object_key,
                original_filename,
                sha256,
                byte_size,
                mime_type
            )
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
            RETURNING id
            """,
            (
                reference.file_kind,
                ARTIFACT_STORAGE_PROVIDER,
                ARTIFACT_BUCKET_ALIAS,
                reference.object_key,
                reference.original_filename,
                reference.sha256,
                reference.byte_size,
                reference.mime_type,
            ),
        )
        return int(cursor.fetchone()[0])

    def import_document(
        self,
        job_id: str,
        source: ArtifactRef,
        document: ExcelDocument,
    ) -> BookingSourceSummary:
        if (
            source.role != "booking_source"
            or source.file_kind != "booking_excel"
            or source.mime_type != XLSX_MIME
            or source.sha256 != document.source_sha256
            or source.byte_size != document.source_byte_size
        ):
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_ARTIFACT_INVALID",
                "Excel 工件身份无效",
                422,
            )
        if document.pending_item_count:
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_REVIEW_REQUIRED",
                "仍有房型需要人工确认或删除",
                409,
            )

        connection = self._open()
        try:
            with connection.cursor() as cursor:
                self._begin(cursor)
                cursor.execute(
                    "SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
                    ("arr_booking_excel_import",),
                )
                existing_batch_id = self._find_existing_batch(
                    cursor,
                    document.source_sha256,
                )
                if existing_batch_id is not None:
                    already_active = self._activate(cursor, existing_batch_id)
                    summary = self._batch_summary(
                        cursor,
                        existing_batch_id,
                        "already_active" if already_active else "reactivated",
                    )
                    connection.commit()
                    return summary

                artifact_id = self._ensure_artifact(cursor, source)
                delivery = {
                    "source_sha256": document.source_sha256,
                    "source_rows": len(document.rows),
                    "worksheet_count": document.worksheet_count,
                    "distinct_group_codes": document.distinct_group_code_count,
                    "room_quantity": document.room_quantity,
                }
                delivery_bytes = canonical_json_bytes(delivery)
                delivery_sha256 = hashlib.sha256(delivery_bytes).hexdigest()
                cursor.execute(
                    """
                    INSERT INTO ingestion.processing_runs (
                        run_key,
                        pipeline_type,
                        source_artifact_id,
                        run_status,
                        requested_processor_version,
                        requested_rule_set_sha256,
                        delivered_processor_version,
                        delivered_rule_set_sha256,
                        result_schema_version,
                        delivery_sha256,
                        delivery_json
                    )
                    VALUES (
                        %s,
                        'booking_source_import',
                        %s,
                        'validating',
                        %s,
                        %s,
                        %s,
                        %s,
                        %s,
                        %s,
                        %s::jsonb
                    )
                    RETURNING id
                    """,
                    (
                        job_id,
                        artifact_id,
                        PROCESSOR_VERSION,
                        rule_set_sha256(),
                        PROCESSOR_VERSION,
                        rule_set_sha256(),
                        RESULT_SCHEMA_VERSION,
                        delivery_sha256,
                        delivery_bytes.decode("utf-8"),
                    ),
                )
                processing_run_id = int(cursor.fetchone()[0])
                cursor.execute(
                    """
                    INSERT INTO booking.source_batches (
                        source_artifact_id,
                        source_kind,
                        source_format_version,
                        batch_status,
                        source_rows,
                        accepted_rows,
                        failed_rows
                    )
                    VALUES (
                        %s,
                        'booking_excel',
                        'xlsx/1.0',
                        'processing',
                        %s,
                        0,
                        0
                    )
                    RETURNING id
                    """,
                    (artifact_id, len(document.rows)),
                )
                source_batch_id = int(cursor.fetchone()[0])

                for row in document.rows:
                    row_sha256 = row.sha256()
                    result = build_excel_parse_result(row)
                    result_bytes = canonical_json_bytes(result)
                    result_sha256 = hashlib.sha256(result_bytes).hexdigest()
                    cursor.execute(
                        """
                        INSERT INTO booking.source_rows (
                            source_batch_id,
                            source_worksheet,
                            source_row_no,
                            group_code_raw,
                            type_of_room_raw,
                            no_of_rooms,
                            source_row_sha256
                        )
                        VALUES (%s, %s, %s, %s, %s, %s, %s)
                        RETURNING id
                        """,
                        (
                            source_batch_id,
                            row.worksheet,
                            row.row_no,
                            row.group_code_raw,
                            row.type_of_room_raw,
                            row.no_of_rooms,
                            row_sha256,
                        ),
                    )
                    source_row_id = int(cursor.fetchone()[0])
                    cursor.execute(
                        """
                        INSERT INTO booking.parse_versions (
                            source_row_id,
                            version_no,
                            parse_status,
                            result_schema_version,
                            processor_name,
                            processor_version,
                            rule_set_sha256,
                            input_row_sha256,
                            result_sha256,
                            result_json,
                            validated_at
                        )
                        VALUES (
                            %s,
                            1,
                            'accepted',
                            %s,
                            %s,
                            %s,
                            %s,
                            %s,
                            %s,
                            %s::jsonb,
                            now()
                        )
                        RETURNING id
                        """,
                        (
                            source_row_id,
                            RESULT_SCHEMA_VERSION,
                            PROCESSOR_NAME,
                            PROCESSOR_VERSION,
                            rule_set_sha256(),
                            row_sha256,
                            result_sha256,
                            result_bytes.decode("utf-8"),
                        ),
                    )
                    parse_version_id = int(cursor.fetchone()[0])
                    for item in row.room_items:
                        cursor.execute(
                            """
                            INSERT INTO booking.room_items (
                                parse_version_id,
                                item_no,
                                room_type_raw,
                                room_type_code,
                                quantity,
                                unit_price,
                                currency_code,
                                price_token_raw,
                                source_fragment
                            )
                            VALUES (%s, %s, %s, %s, %s, NULL, NULL, NULL, %s)
                            """,
                            (
                                parse_version_id,
                                item.item_no,
                                item.room_type_raw,
                                item.room_type_code,
                                item.quantity,
                                item.source_fragment,
                            ),
                        )

                cursor.execute(
                    """
                    UPDATE booking.source_batches
                    SET batch_status = 'accepted',
                        accepted_rows = source_rows,
                        failed_rows = 0,
                        validated_at = now(),
                        finished_at = now()
                    WHERE id = %s
                    """,
                    (source_batch_id,),
                )
                cursor.execute(
                    """
                    INSERT INTO booking.current_row_parses (
                        source_row_id,
                        parse_version_id
                    )
                    SELECT source.id, parsed.id
                    FROM booking.source_rows AS source
                    JOIN booking.parse_versions AS parsed
                      ON parsed.source_row_id = source.id
                     AND parsed.version_no = 1
                     AND parsed.parse_status = 'accepted'
                    WHERE source.source_batch_id = %s
                    """,
                    (source_batch_id,),
                )
                self._activate(cursor, source_batch_id)
                cursor.execute(
                    """
                    UPDATE ingestion.processing_runs
                    SET run_status = 'accepted',
                        updated_at = now(),
                        validated_at = now(),
                        finished_at = now()
                    WHERE id = %s
                    """,
                    (processing_run_id,),
                )
                cursor.execute(
                    """
                    INSERT INTO ingestion.outbox_events (
                        event_key,
                        aggregate_type,
                        aggregate_id,
                        event_type,
                        payload
                    )
                    VALUES (
                        %s,
                        'booking_source_batch',
                        %s,
                        'booking.source_batch.accepted',
                        %s::jsonb
                    )
                    """,
                    (
                        f"booking-source-batch:{source_batch_id}:accepted",
                        source_batch_id,
                        json.dumps(
                            {
                                "source_batch_id": source_batch_id,
                                "source_rows": len(document.rows),
                                "distinct_group_codes": document.distinct_group_code_count,
                            },
                            separators=(",", ":"),
                        ),
                    ),
                )
                summary = self._batch_summary(
                    cursor,
                    source_batch_id,
                    "imported_and_activated",
                )
            connection.commit()
            return summary
        except BookingExcelRepositoryError:
            connection.rollback()
            raise
        except Exception:
            connection.rollback()
            raise BookingExcelRepositoryError(
                "BOOKING_EXCEL_DATABASE_WRITE_FAILED",
                "Excel 已校验，但数据源未能完成入库",
            ) from None
        finally:
            connection.close()
