"""
Service layer for laboratory schedule configuration and holiday/leave management.
"""

from datetime import UTC, date, datetime, timedelta
from uuid import UUID
from zoneinfo import ZoneInfo

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import NotFoundException, ValidationException
from app.models.audit import AuditCategory
from app.models.user import User
from app.repositories.appointment_repository import AppointmentRepository
from app.repositories.lab_repository import LabRepository
from app.repositories.schedule_repository import ScheduleRepository
from app.schemas.schedule import (
    LabHolidayCreate,
    LabHolidayResponse,
    LabScheduleConfigResponse,
    LabScheduleConfigUpdate,
)
from app.services.audit_service import AuditService


class ScheduleService:
    """Business logic for lab operating hours, slot generation rules, and holidays."""

    def __init__(
        self, schedule_repo: ScheduleRepository | None = None, audit_service: AuditService | None = None
    ) -> None:
        self.repo = schedule_repo or ScheduleRepository()
        self.lab_repo = LabRepository()
        self.slot_repo = AppointmentRepository()
        self.audit = audit_service or AuditService()

    async def _record_event(
        self,
        db: AsyncSession,
        *,
        action: str,
        description: str,
        lab_id: UUID,
        actor: User | None,
        details: dict[str, object] | None = None,
    ) -> None:
        role = getattr(actor.role, "value", actor.role) if actor else "system"
        await self.audit.record_event(
            db,
            action=action,
            description=description,
            category=AuditCategory.ADMIN.value,
            actor_name=actor.name if actor else "System",
            actor_role=str(role),
            actor_id=actor.id if actor else None,
            target_type="lab_schedule",
            target_id=str(lab_id),
            ip_address=None,
            details=details,
            lab_id=lab_id,
        )

    async def get_config(self, db: AsyncSession, lab_id: UUID | None = None) -> LabScheduleConfigResponse:
        """Fetch lab operating schedule configuration."""
        if lab_id is None:
            lab_id = await self.lab_repo.get_default_lab_id(db)
        config = await self.repo.get_or_create_config(db, lab_id)
        return LabScheduleConfigResponse.model_validate(config)

    async def update_config(
        self,
        db: AsyncSession,
        data: LabScheduleConfigUpdate,
        lab_id: UUID | None = None,
        actor: User | None = None,
    ) -> LabScheduleConfigResponse:
        """Update lab operating schedule configuration."""
        if lab_id is None:
            lab_id = await self.lab_repo.get_default_lab_id(db)

        config = await self.repo.get_or_create_config(db, lab_id)

        # Validate open/close times
        open_time = data.open_time or config.open_time
        close_time = data.close_time or config.close_time
        if open_time >= close_time:
            raise ValidationException(
                message=f"Opening time ({open_time}) must be earlier than closing time ({close_time})."
            )

        changed_fields = [
            key for key, value in data.model_dump(exclude_unset=True).items() if getattr(config, key) != value
        ]
        updated = await self.repo.update_config(db, config, data)
        local_tz = ZoneInfo("Asia/Kathmandu")
        today_local_start = datetime.combine(
            datetime.now(local_tz).date(), datetime.min.time(), tzinfo=local_tz
        ).astimezone(UTC)
        await self.slot_repo.clear_unbooked_from(db, lab_id=lab_id, start=today_local_start)
        if changed_fields:
            await self._record_event(
                db,
                action="LAB_SCHEDULE_UPDATED",
                description="Updated operating hours and slot rules.",
                lab_id=lab_id,
                actor=actor,
                details={"changed_fields": changed_fields},
            )
        return LabScheduleConfigResponse.model_validate(updated)

    async def list_holidays(
        self,
        db: AsyncSession,
        lab_id: UUID | None = None,
        from_date: date | None = None,
        to_date: date | None = None,
    ) -> list[LabHolidayResponse]:
        """List active holidays for the laboratory."""
        if lab_id is None:
            lab_id = await self.lab_repo.get_default_lab_id(db)
        holidays = await self.repo.list_holidays(db, lab_id, from_date, to_date)
        return [LabHolidayResponse.model_validate(h) for h in holidays]

    async def add_holiday(
        self,
        db: AsyncSession,
        data: LabHolidayCreate,
        lab_id: UUID | None = None,
        actor: User | None = None,
    ) -> LabHolidayResponse:
        """Add a holiday or closure date range."""
        if data.start_date > data.end_date:
            raise ValidationException(message="Start date cannot be after end date.")

        if lab_id is None:
            lab_id = await self.lab_repo.get_default_lab_id(db)

        holiday = await self.repo.create_holiday(db, lab_id, data)
        if data.is_closed:
            # Drop unbooked windows inside the closure so day views regenerate empty.
            # Booked appointments are kept and must be rescheduled manually from the desk.
            local_tz = ZoneInfo("Asia/Kathmandu")
            range_start = datetime.combine(data.start_date, datetime.min.time(), tzinfo=local_tz).astimezone(
                UTC
            )
            range_end = datetime.combine(
                data.end_date + timedelta(days=1), datetime.min.time(), tzinfo=local_tz
            ).astimezone(UTC)
            await self.slot_repo.clear_unbooked_from(db, lab_id=lab_id, start=range_start, end=range_end)
        await self._record_event(
            db,
            action="LAB_HOLIDAY_CREATED",
            description="Added a holiday or closure period.",
            lab_id=lab_id,
            actor=actor,
            details={
                "start_date": data.start_date.isoformat(),
                "end_date": data.end_date.isoformat(),
                "is_closed": data.is_closed,
            },
        )
        return LabHolidayResponse.model_validate(holiday)

    async def delete_holiday(
        self, db: AsyncSession, holiday_id: UUID, lab_id: UUID | None = None, actor: User | None = None
    ) -> None:
        """Delete an active holiday / closure."""
        holiday = await self.repo.get_holiday_by_id(db, holiday_id, lab_id)
        if not holiday:
            raise NotFoundException(message=f"Holiday with id '{holiday_id}' not found.")
        await self.repo.delete_holiday(db, holiday)
        await self._record_event(
            db,
            action="LAB_HOLIDAY_REMOVED",
            description="Removed a holiday or closure.",
            lab_id=holiday.lab_id,
            actor=actor,
            details={"holiday_id": str(holiday.id)},
        )

    async def check_holiday(
        self, db: AsyncSession, target_date: date, lab_id: UUID | None = None
    ) -> LabHolidayResponse | None:
        """Check if target date is an active holiday."""
        if lab_id is None:
            lab_id = await self.lab_repo.get_default_lab_id(db)
        holiday = await self.repo.find_holiday_for_date(db, lab_id, target_date)
        if holiday:
            return LabHolidayResponse.model_validate(holiday)
        return None
