from __future__ import annotations

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

from src.core.exceptions import NotFoundException, ValidationException
from src.core.logging import get_logger
from src.domain.entities import SyncJobEntity, SyncJobStatus, SyncStrategy
from src.domain.interfaces import ISyncJobRepository, ISyncLogRepository
from src.infrastructure.messaging.rabbitmq import rabbitmq_client

logger = get_logger("sync_service")


class SyncService:
    def __init__(
        self,
        sync_job_repo: ISyncJobRepository,
        sync_log_repo: ISyncLogRepository,
    ) -> None:
        self._sync_job_repo = sync_job_repo
        self._sync_log_repo = sync_log_repo

    async def create_sync_job(
        self,
        pharmacy_id: UUID,
        connector_id: UUID,
        strategy: SyncStrategy,
    ) -> SyncJobEntity:
        entity = SyncJobEntity(
            pharmacy_id=pharmacy_id,
            connector_id=connector_id,
            strategy=strategy,
            status=SyncJobStatus.PENDING,
        )
        created = await self._sync_job_repo.create(entity)
        await self._publish_event("sync.created", created.id, {
            "pharmacy_id": str(pharmacy_id),
            "connector_id": str(connector_id),
            "strategy": strategy,
        })
        return created

    async def get_sync_job(self, job_id: UUID) -> SyncJobEntity | None:
        return await self._sync_job_repo.get_by_id(job_id)

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

    async def trigger_sync(self, job_id: UUID) -> SyncJobEntity:
        entity = await self._sync_job_repo.get_by_id(job_id)
        if not entity:
            raise NotFoundException("SyncJob", str(job_id))

        if entity.status not in (SyncJobStatus.PENDING, SyncJobStatus.FAILED):
            raise ValidationException(
                f"Cannot trigger sync job. Current status: {entity.status}",
                details={"current_status": entity.status},
            )

        entity.status = SyncJobStatus.RUNNING
        entity.started_at = datetime.now(timezone.utc)
        entity.error_message = None
        updated = await self._sync_job_repo.update(entity)

        await self._add_log(job_id, "INFO", "Sync job triggered")
        await self._publish_event("sync.started", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "strategy": entity.strategy,
        })
        return updated

    async def cancel_sync(self, job_id: UUID) -> SyncJobEntity:
        entity = await self._sync_job_repo.get_by_id(job_id)
        if not entity:
            raise NotFoundException("SyncJob", str(job_id))

        if entity.status != SyncJobStatus.RUNNING:
            raise ValidationException(
                f"Cannot cancel sync job. Current status: {entity.status}",
                details={"current_status": entity.status},
            )

        entity.status = SyncJobStatus.CANCELLED
        entity.completed_at = datetime.now(timezone.utc)
        updated = await self._sync_job_repo.update(entity)

        await self._add_log(job_id, "WARNING", "Sync job cancelled by user")
        await self._publish_event("sync.cancelled", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
        })
        return updated

    async def complete_sync(
        self,
        job_id: UUID,
        *,
        items_total: int | None = None,
        items_processed: int | None = None,
        items_failed: int | None = None,
    ) -> SyncJobEntity:
        entity = await self._sync_job_repo.get_by_id(job_id)
        if not entity:
            raise NotFoundException("SyncJob", str(job_id))

        entity.status = SyncJobStatus.COMPLETED
        entity.completed_at = datetime.now(timezone.utc)
        if items_total is not None:
            entity.items_total = items_total
        if items_processed is not None:
            entity.items_processed = items_processed
        if items_failed is not None:
            entity.items_failed = items_failed
        updated = await self._sync_job_repo.update(entity)

        await self._add_log(job_id, "INFO", f"Sync completed. Processed: {items_processed}, Failed: {items_failed}")
        await self._publish_event("sync.completed", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "items_total": items_total,
            "items_processed": items_processed,
            "items_failed": items_failed,
        })
        return updated

    async def fail_sync(self, job_id: UUID, error_message: str) -> SyncJobEntity:
        entity = await self._sync_job_repo.get_by_id(job_id)
        if not entity:
            raise NotFoundException("SyncJob", str(job_id))

        entity.status = SyncJobStatus.FAILED
        entity.completed_at = datetime.now(timezone.utc)
        entity.error_message = error_message
        updated = await self._sync_job_repo.update(entity)

        await self._add_log(job_id, "ERROR", f"Sync failed: {error_message}")
        await self._publish_event("sync.failed", updated.id, {
            "pharmacy_id": str(entity.pharmacy_id),
            "error_message": error_message,
        })
        return updated

    async def get_sync_logs(
        self,
        job_id: UUID,
        *,
        level: str | None = None,
        offset: int = 0,
        limit: int = 100,
    ):
        return await self._sync_log_repo.list_by_job(job_id, level=level, offset=offset, limit=limit)

    async def _add_log(self, job_id: UUID, level: str, message: str, details: dict | None = None) -> None:
        from src.domain.entities import SyncLogEntity
        log = SyncLogEntity(sync_job_id=job_id, level=level, message=message, details=details)
        await self._sync_log_repo.add(log)

    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("sync_event_publish_failed", extra={"event_type": event_type, "error": str(exc)})
