"""Tracking application services — Phase 10.7."""
from __future__ import annotations

import secrets
from datetime import datetime, timezone
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import TransactionalEventPublisher
from app.events.types import DeliveryEventType
from app.models.tracking import (
    CustomerTrackingToken,
    ProofOfDelivery,
    TrackingPoint,
    TrackingSession,
)
from app.models.types import CustomerTrackingTokenStatus, ProofOfDeliveryStatus, TrackingSessionStatus
from app.repositories.dispatch import DispatchJobRepository
from app.repositories.tracking import (
    CustomerTrackingTokenRepository,
    ProofOfDeliveryRepository,
    TrackingPointRepository,
    TrackingSessionRepository,
)
from app.schemas.tracking import (
    CustomerTrackingTokenCreate,
    ProofOfDeliveryCreate,
    ProofOfDeliveryUpdate,
    TrackingPointCreate,
    TrackingSessionCreate,
    TrackingSessionUpdate,
)
from app.services._helpers import actor_id
from app.specifications.tracking import (
    CustomerTrackingTokenListSpec,
    ProofOfDeliveryListSpec,
    TrackingSessionListSpec,
)
from app.validators import ensure_optimistic_version
from app.validators.tracking import validate_coordinates, validate_pod_refs
from shared.exceptions import AppError, NotFoundError
from shared.security import CurrentUser


class TrackingSessionService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = TrackingSessionRepository(session)
        self.points = TrackingPointRepository(session)
        self.jobs = DispatchJobRepository(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def start(
        self, tenant_id: UUID, body: TrackingSessionCreate, *, actor: CurrentUser | None = None
    ) -> TrackingSession:
        if body.dispatch_job_id and await self.jobs.get(tenant_id, body.dispatch_job_id) is None:
            raise NotFoundError("کار ارسال یافت نشد")
        entity = TrackingSession(
            tenant_id=tenant_id,
            dispatch_job_id=body.dispatch_job_id,
            driver_id=body.driver_id,
            status=TrackingSessionStatus.ACTIVE,
            started_at=datetime.now(timezone.utc),
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        await self.repo.add(entity)
        await self.publisher.publish(
            event_type=DeliveryEventType.TRACKING_SESSION_STARTED,
            aggregate_type="tracking_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(body.dispatch_job_id) if body.dispatch_job_id else None},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, session_id: UUID) -> TrackingSession:
        entity = await self.repo.get(tenant_id, session_id)
        if entity is None:
            raise NotFoundError("جلسه ردیابی یافت نشد")
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        offset: int = 0,
        limit: int = 20,
        spec: TrackingSessionListSpec | None = None,
    ) -> tuple[list[TrackingSession], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        return items, await self.repo.count_filtered(tenant_id, spec=spec)

    async def update(
        self,
        tenant_id: UUID,
        session_id: UUID,
        body: TrackingSessionUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> TrackingSession:
        entity = await self.get(tenant_id, session_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        for key, value in data.items():
            setattr(entity, key, value)
        if entity.status in {TrackingSessionStatus.COMPLETED, TrackingSessionStatus.CANCELLED}:
            entity.ended_at = datetime.now(timezone.utc)
        entity.version += 1
        entity.updated_by = actor_id(actor)
        event = DeliveryEventType.TRACKING_SESSION_UPDATED
        if entity.status == TrackingSessionStatus.COMPLETED:
            event = DeliveryEventType.TRACKING_SESSION_ENDED
        await self.publisher.publish(
            event_type=event,
            aggregate_type="tracking_session",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"status": entity.status.value},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def record_point(
        self,
        tenant_id: UUID,
        session_id: UUID,
        body: TrackingPointCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> TrackingPoint:
        await self.get(tenant_id, session_id)
        validate_coordinates(latitude=body.latitude, longitude=body.longitude)
        entity = TrackingPoint(
            tenant_id=tenant_id,
            tracking_session_id=session_id,
            latitude=body.latitude,
            longitude=body.longitude,
            recorded_at=body.recorded_at,
            metadata_json=body.metadata_json,
        )
        await self.points.add(entity)
        await self.publisher.publish(
            event_type=DeliveryEventType.TRACKING_POINT_RECORDED,
            aggregate_type="tracking_point",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"tracking_session_id": str(session_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def list_points(
        self, tenant_id: UUID, session_id: UUID, *, limit: int = 500
    ) -> list[TrackingPoint]:
        await self.get(tenant_id, session_id)
        return list(await self.points.list_for_session(tenant_id, session_id, limit=limit))


class ProofOfDeliveryService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = ProofOfDeliveryRepository(session)
        self.jobs = DispatchJobRepository(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def capture(
        self, tenant_id: UUID, body: ProofOfDeliveryCreate, *, actor: CurrentUser | None = None
    ) -> ProofOfDelivery:
        if await self.jobs.get(tenant_id, body.dispatch_job_id) is None:
            raise NotFoundError("کار ارسال یافت نشد")
        validate_pod_refs(
            signature_file_ref=body.signature_file_ref,
            photo_file_ref=body.photo_file_ref,
        )
        entity = ProofOfDelivery(
            tenant_id=tenant_id,
            dispatch_job_id=body.dispatch_job_id,
            status=ProofOfDeliveryStatus.CAPTURED,
            signature_file_ref=body.signature_file_ref,
            photo_file_ref=body.photo_file_ref,
            delivery_code=body.delivery_code,
            notes=body.notes,
            captured_at=datetime.now(timezone.utc),
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        await self.repo.add(entity)
        await self.publisher.publish(
            event_type=DeliveryEventType.PROOF_OF_DELIVERY_CAPTURED,
            aggregate_type="proof_of_delivery",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(body.dispatch_job_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, pod_id: UUID) -> ProofOfDelivery:
        entity = await self.repo.get(tenant_id, pod_id)
        if entity is None:
            raise NotFoundError("اثبات تحویل یافت نشد")
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        offset: int = 0,
        limit: int = 20,
        spec: ProofOfDeliveryListSpec | None = None,
    ) -> tuple[list[ProofOfDelivery], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        return items, await self.repo.count_filtered(tenant_id, spec=spec)

    async def update(
        self,
        tenant_id: UUID,
        pod_id: UUID,
        body: ProofOfDeliveryUpdate,
        *,
        actor: CurrentUser | None = None,
    ) -> ProofOfDelivery:
        entity = await self.get(tenant_id, pod_id)
        ensure_optimistic_version(entity, body.version)
        data = body.model_dump(exclude_unset=True, exclude={"version"})
        for key, value in data.items():
            setattr(entity, key, value)
        entity.version += 1
        entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.PROOF_OF_DELIVERY_UPDATED,
            aggregate_type="proof_of_delivery",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(entity.dispatch_job_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity


class CustomerTrackingTokenService:
    def __init__(
        self,
        session: AsyncSession,
        *,
        publisher: TransactionalEventPublisher | None = None,
    ) -> None:
        self.session = session
        self.repo = CustomerTrackingTokenRepository(session)
        self.jobs = DispatchJobRepository(session)
        self.publisher = publisher or TransactionalEventPublisher(session)

    async def create(
        self,
        tenant_id: UUID,
        body: CustomerTrackingTokenCreate,
        *,
        actor: CurrentUser | None = None,
    ) -> CustomerTrackingToken:
        if await self.jobs.get(tenant_id, body.dispatch_job_id) is None:
            raise NotFoundError("کار ارسال یافت نشد")
        token = secrets.token_urlsafe(32)
        entity = CustomerTrackingToken(
            tenant_id=tenant_id,
            dispatch_job_id=body.dispatch_job_id,
            token=token,
            status=CustomerTrackingTokenStatus.ACTIVE,
            expires_at=body.expires_at,
            metadata_json=body.metadata_json,
            created_by=actor_id(actor),
            updated_by=actor_id(actor),
        )
        try:
            await self.repo.add(entity)
        except Exception as exc:
            raise AppError(
                "ایجاد توکن ردیابی ناموفق بود",
                status_code=409,
                error_code="token_create_failed",
            ) from exc
        await self.publisher.publish(
            event_type=DeliveryEventType.CUSTOMER_TRACKING_TOKEN_CREATED,
            aggregate_type="customer_tracking_token",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(body.dispatch_job_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity

    async def get(self, tenant_id: UUID, token_id: UUID) -> CustomerTrackingToken:
        entity = await self.repo.get(tenant_id, token_id)
        if entity is None:
            raise NotFoundError("توکن ردیابی یافت نشد")
        return entity

    async def list(
        self,
        tenant_id: UUID,
        *,
        offset: int = 0,
        limit: int = 20,
        spec: CustomerTrackingTokenListSpec | None = None,
    ) -> tuple[list[CustomerTrackingToken], int]:
        items = list(await self.repo.list_filtered(tenant_id, offset=offset, limit=limit, spec=spec))
        return items, await self.repo.count_filtered(tenant_id, spec=spec)

    async def revoke(
        self, tenant_id: UUID, token_id: UUID, *, actor: CurrentUser | None = None
    ) -> CustomerTrackingToken:
        entity = await self.get(tenant_id, token_id)
        entity.status = CustomerTrackingTokenStatus.REVOKED
        entity.updated_by = actor_id(actor)
        await self.publisher.publish(
            event_type=DeliveryEventType.CUSTOMER_TRACKING_TOKEN_REVOKED,
            aggregate_type="customer_tracking_token",
            aggregate_id=entity.id,
            tenant_id=tenant_id,
            payload={"dispatch_job_id": str(entity.dispatch_job_id)},
        )
        await self.session.commit()
        await self.session.refresh(entity)
        return entity
