from __future__ import annotations

from datetime import datetime, timezone
from typing import Any
from uuid import UUID

from src.core.exceptions import ConflictException, NotFoundException, ValidationException
from src.core.logging import get_logger
from src.domain.entities import (
    InventoryHistoryEntity,
    InventoryRecordEntity,
    ReservationEntity,
    ReservationStatus,
)
from src.domain.interfaces import IInventoryRepository, IReservationRepository
from src.infrastructure.messaging.rabbitmq import rabbitmq_client

logger = get_logger("inventory_service")

RESERVATION_DEFAULT_TTL_MINUTES = 30


class InventoryService:
    def __init__(
        self,
        inventory_repo: IInventoryRepository,
        reservation_repo: IReservationRepository,
    ) -> None:
        self._inventory_repo = inventory_repo
        self._reservation_repo = reservation_repo

    async def get_inventory_record(self, pharmacy_id: UUID, medication_id: UUID) -> InventoryRecordEntity | None:
        return await self._inventory_repo.get_record(pharmacy_id, medication_id)

    async def list_inventory(self, pharmacy_id: UUID, *, offset: int = 0, limit: int = 50) -> tuple[list[InventoryRecordEntity], int]:
        return await self._inventory_repo.list_by_pharmacy(pharmacy_id, offset=offset, limit=limit)

    async def upsert_inventory_record(self, entity: InventoryRecordEntity) -> InventoryRecordEntity:
        existing = await self._inventory_repo.get_record(entity.pharmacy_id, entity.medication_id)
        previous_stock = existing.stock if existing else 0

        upserted = await self._inventory_repo.upsert(entity)

        delta = entity.stock - previous_stock
        if delta != 0:
            history = InventoryHistoryEntity(
                inventory_record_id=upserted.id,
                pharmacy_id=entity.pharmacy_id,
                medication_id=entity.medication_id,
                previous_stock=previous_stock,
                new_stock=entity.stock,
                delta=delta,
                reason=entity.source or "upsert",
                source=entity.source,
            )
            await self._inventory_repo.add_history(history)

        await self._publish_event("inventory.updated", upserted.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "medication_id": str(entity.medication_id),
            "previous_stock": previous_stock,
            "new_stock": entity.stock,
            "delta": delta,
        })
        return upserted

    async def get_inventory_history(self, pharmacy_id: UUID, medication_id: UUID, *, offset: int = 0, limit: int = 50) -> list[InventoryHistoryEntity]:
        records = await self._inventory_repo.get_history(pharmacy_id, medication_id, offset=offset, limit=limit)
        return list(records)

    async def create_reservation(
        self,
        pharmacy_id: UUID,
        medication_id: UUID,
        quantity: int,
        *,
        user_id: UUID | None = None,
        external_reference: str | None = None,
        idempotency_key: str | None = None,
        ttl_minutes: int = RESERVATION_DEFAULT_TTL_MINUTES,
    ) -> ReservationEntity:
        if idempotency_key:
            existing = await self._reservation_repo.get_by_idempotency_key(idempotency_key)
            if existing:
                return existing

        record = await self._inventory_repo.get_record(pharmacy_id, medication_id)
        if not record:
            raise NotFoundException("InventoryRecord", f"{pharmacy_id}/{medication_id}")

        if record.available_stock < quantity:
            raise ValidationException(
                f"Insufficient available stock. Requested: {quantity}, Available: {record.available_stock}",
                details={"available_stock": record.available_stock, "requested": quantity},
            )

        record.reserved_stock += quantity
        record.available_stock = record.stock - record.reserved_stock
        await self._inventory_repo.upsert(record)

        now = datetime.now(timezone.utc)
        entity = ReservationEntity(
            pharmacy_id=pharmacy_id,
            medication_id=medication_id,
            user_id=user_id,
            external_reference=external_reference,
            quantity=quantity,
            status=ReservationStatus.PENDING,
            expires_at=now + __import__("datetime").timedelta(minutes=ttl_minutes),
            idempotency_key=idempotency_key,
        )
        created = await self._reservation_repo.create(entity)

        await self._publish_event("reservation.created", created.id, {
            "pharmacy_id": str(pharmacy_id),
            "medication_id": str(medication_id),
            "quantity": quantity,
        })
        return created

    async def confirm_reservation(self, reservation_id: UUID) -> ReservationEntity:
        entity = await self._reservation_repo.get_by_id(reservation_id)
        if not entity:
            raise NotFoundException("Reservation", str(reservation_id))

        if entity.status != ReservationStatus.PENDING:
            raise ValidationException(f"Reservation cannot be confirmed. Current status: {entity.status}")

        now = datetime.now(timezone.utc)
        if entity.expires_at and entity.expires_at < now:
            entity.status = ReservationStatus.EXPIRED
            await self._reservation_repo.update(entity)
            await self._release_reserved_stock(entity)
            await self._publish_event("reservation.expired", entity.id, {"pharmacy_id": str(entity.pharmacy_id)})
            raise ValidationException("Reservation has expired")

        entity.status = ReservationStatus.CONFIRMED
        entity.confirmed_at = now
        updated = await self._reservation_repo.update(entity)

        await self._publish_event("reservation.confirmed", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "medication_id": str(entity.medication_id),
            "quantity": entity.quantity,
        })
        return updated

    async def cancel_reservation(self, reservation_id: UUID) -> ReservationEntity:
        entity = await self._reservation_repo.get_by_id(reservation_id)
        if not entity:
            raise NotFoundException("Reservation", str(reservation_id))

        if entity.status not in (ReservationStatus.PENDING, ReservationStatus.CONFIRMED):
            raise ValidationException(f"Reservation cannot be cancelled. Current status: {entity.status}")

        entity.status = ReservationStatus.CANCELLED
        entity.cancelled_at = datetime.now(timezone.utc)
        updated = await self._reservation_repo.update(entity)

        await self._release_reserved_stock(entity)

        await self._publish_event("reservation.cancelled", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "medication_id": str(entity.medication_id),
            "quantity": entity.quantity,
        })
        return updated

    async def get_reservation(self, reservation_id: UUID) -> ReservationEntity | None:
        return await self._reservation_repo.get_by_id(reservation_id)

    async def list_reservations(self, pharmacy_id: UUID, *, status: str | None = None, offset: int = 0, limit: int = 50) -> tuple[list[ReservationEntity], int]:
        return await self._reservation_repo.list_by_pharmacy(pharmacy_id, status=status, offset=offset, limit=limit)

    async def expire_pending_reservations(self) -> int:
        pass

    async def _release_reserved_stock(self, reservation: ReservationEntity) -> None:
        record = await self._inventory_repo.get_record(reservation.pharmacy_id, reservation.medication_id)
        if not record:
            return

        record.reserved_stock = max(0, record.reserved_stock - reservation.quantity)
        record.available_stock = record.stock - record.reserved_stock
        updated = await self._inventory_repo.upsert(record)

        history = InventoryHistoryEntity(
            inventory_record_id=updated.id,
            pharmacy_id=reservation.pharmacy_id,
            medication_id=reservation.medication_id,
            previous_stock=record.stock + reservation.quantity if reservation.status == ReservationStatus.PENDING else record.stock,
            new_stock=record.stock,
            delta=-reservation.quantity,
            reason=f"reservation_{reservation.status}",
            source="inventory_service",
        )
        await self._inventory_repo.add_history(history)

    async def _publish_event(self, event_type: str, resource_id: UUID, payload: dict[str, Any]) -> None:
        try:
            routing_key = event_type.replace(".", "_")
            full_payload = {
                "event_type": event_type,
                "resource_id": str(resource_id),
                "timestamp": datetime.now(timezone.utc).isoformat(),
                **payload,
            }
            await rabbitmq_client.publish(routing_key, full_payload)
        except Exception as exc:
            logger.warning("inventory_event_publish_failed", extra={"event_type": event_type, "error": str(exc)})
