"""
Service layer for health packages and bundled items.
"""

import math
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import AlreadyExistsException, NotFoundException
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.models.audit import AuditCategory
from app.models.user import User
from app.repositories.lab_repository import LabRepository
from app.repositories.package_repository import PackageRepository
from app.repositories.test_repository import TestCatalogRepository
from app.schemas.package import (
    TestPackageCreate,
    TestPackageDetailResponse,
    TestPackageItemCreate,
    TestPackageItemResponse,
    TestPackageResponse,
    TestPackageUpdate,
)
from app.services.audit_service import AuditService


class PackageService:
    """Business logic for test packages."""

    def __init__(
        self,
        package_repo: PackageRepository | None = None,
        test_repo: TestCatalogRepository | None = None,
        audit_service: AuditService | None = None,
    ) -> None:
        self.repo = package_repo or PackageRepository()
        self.test_repo = test_repo or TestCatalogRepository()
        self.audit = audit_service or AuditService()

    async def _record_event(
        self,
        db: AsyncSession,
        *,
        action: str,
        description: str,
        lab_id: UUID,
        package_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.BILLING.value,
            actor_name=actor.name if actor else "System",
            actor_role=str(role),
            actor_id=actor.id if actor else None,
            target_type="health_package",
            target_id=str(package_id),
            ip_address=None,
            details=details,
            lab_id=lab_id,
        )

    async def list_packages(
        self,
        db: AsyncSession,
        pagination: PaginationParams,
        lab_id: UUID | None = None,
        is_active: bool | None = None,
        q: str | None = None,
    ) -> PaginatedResponse[TestPackageResponse]:
        """List paginated packages with optional filters."""
        if lab_id is None:
            lab_id = await LabRepository().get_default_lab_id(db)
        packages, total = await self.repo.list_packages(
            db,
            limit=pagination.limit,
            offset=pagination.offset,
            lab_id=lab_id,
            is_active=is_active,
            q=q,
        )
        items = []
        for p in packages:
            res = TestPackageResponse.model_validate(p)
            res.test_count = len(p.items) if p.items else 0
            items.append(res)
        pages = math.ceil(total / pagination.limit) if total > 0 else 1
        return PaginatedResponse(
            data=items,
            meta=PaginationMeta(
                total=total,
                page=pagination.page,
                limit=pagination.limit,
                pages=pages,
            ),
        )

    async def get_package(
        self, db: AsyncSession, package_id: UUID, lab_id: UUID | None = None
    ) -> TestPackageDetailResponse:
        """Fetch package details with bundled test items."""
        pkg = await self.repo.get_with_items(db, package_id, lab_id)
        if not pkg:
            raise NotFoundException(message=f"Package with id '{package_id}' was not found.")

        item_responses: list[TestPackageItemResponse] = []
        for it in pkg.items:
            item_responses.append(
                TestPackageItemResponse(
                    id=it.id,
                    package_id=it.package_id,
                    test_id=it.test_id,
                    parameter_id=it.parameter_id,
                    sort_order=it.sort_order,
                    test_name=it.test.name if it.test else None,
                    test_code=it.test.code if it.test else None,
                    parameter_name=it.parameter.name if it.parameter else None,
                )
            )

        return TestPackageDetailResponse(
            id=pkg.id,
            lab_id=pkg.lab_id,
            category_id=pkg.category_id,
            name=pkg.name,
            code=pkg.code,
            description=pkg.description,
            price=pkg.price,
            is_active=pkg.is_active,
            created_at=pkg.created_at,
            updated_at=pkg.updated_at,
            items=item_responses,
        )

    async def create_package(
        self, db: AsyncSession, data: TestPackageCreate, lab_id: UUID | None = None, actor: User | None = None
    ) -> TestPackageResponse:
        """Create a new package."""
        lab_id = lab_id or data.lab_id or await LabRepository().get_default_lab_id(db)
        data = data.model_copy(update={"lab_id": lab_id})

        if data.category_id:
            cat = await self.test_repo.get_category_by_id(db, data.category_id)
            if not cat or cat.lab_id != lab_id:
                raise NotFoundException(message=f"Category with id '{data.category_id}' was not found.")

        if data.code:
            existing = await self.repo.get_by_code(db, data.lab_id, data.code)
            if existing:
                raise AlreadyExistsException(
                    message=f"A package with code '{data.code}' already exists in this lab."
                )

            # A soft-deleted package still holds the unique (lab_id, code).
            # Never-ordered → drop it and create fresh; ordered → rename the
            # archived row aside so history stays intact (mirrors test create).
            archived = await self.repo.get_by_code_including_deleted(db, data.lab_id, data.code)
            if archived is not None and archived.deleted_at is not None:
                order_count = await self.repo.get_package_order_count(db, archived.id)
                if order_count == 0:
                    await self.repo.hard_delete_package(db, archived)
                else:
                    archived.code = f"{archived.code}_archived_{archived.id.hex[:6]}"
                    await db.flush()

        pkg = await self.repo.create(db, data)
        await self._record_event(
            db,
            action="HEALTH_PACKAGE_CREATED",
            description="Created a health package.",
            lab_id=lab_id,
            package_id=pkg.id,
            actor=actor,
            details={"code_present": bool(data.code), "price": str(data.price)},
        )
        return TestPackageResponse.model_validate(pkg)

    async def update_package(
        self,
        db: AsyncSession,
        package_id: UUID,
        data: TestPackageUpdate,
        lab_id: UUID | None = None,
        actor: User | None = None,
    ) -> TestPackageResponse:
        """Update package details."""
        pkg = await self.repo.get_by_id(db, package_id, lab_id)
        if not pkg:
            raise NotFoundException(message=f"Package with id '{package_id}' was not found.")

        if data.category_id is not None and data.category_id != pkg.category_id:
            cat = await self.test_repo.get_category_by_id(db, data.category_id)
            if not cat or cat.lab_id != pkg.lab_id:
                raise NotFoundException(message=f"Category with id '{data.category_id}' was not found.")

        if data.code is not None and data.code != pkg.code:
            existing = await self.repo.get_by_code(db, pkg.lab_id, data.code)
            if existing and existing.id != pkg.id:
                raise AlreadyExistsException(
                    message=f"A package with code '{data.code}' already exists in this lab."
                )

        changed_fields = [
            key for key, value in data.model_dump(exclude_unset=True).items() if getattr(pkg, key) != value
        ]
        updated = await self.repo.update(db, pkg, data)
        if changed_fields:
            await self._record_event(
                db,
                action="HEALTH_PACKAGE_UPDATED",
                description="Updated a health package.",
                lab_id=pkg.lab_id,
                package_id=pkg.id,
                actor=actor,
                details={"changed_fields": changed_fields},
            )
        return TestPackageResponse.model_validate(updated)

    async def delete_package(
        self, db: AsyncSession, package_id: UUID, lab_id: UUID | None = None, actor: User | None = None
    ) -> None:
        """Soft-delete a package."""
        pkg = await self.repo.get_by_id(db, package_id, lab_id)
        if not pkg:
            raise NotFoundException(message=f"Package with id '{package_id}' was not found.")
        await self.repo.delete(db, pkg)
        await self._record_event(
            db,
            action="HEALTH_PACKAGE_DELETED",
            description="Archived a health package.",
            lab_id=pkg.lab_id,
            package_id=pkg.id,
            actor=actor,
        )

    async def add_item(
        self,
        db: AsyncSession,
        package_id: UUID,
        data: TestPackageItemCreate,
        lab_id: UUID | None = None,
        actor: User | None = None,
    ) -> TestPackageItemResponse:
        """Add a test or specific analyte item to a package."""
        pkg = await self.repo.get_by_id(db, package_id, lab_id)
        if not pkg:
            raise NotFoundException(message=f"Package with id '{package_id}' was not found.")

        test = await self.test_repo.get_test_by_id(db, data.test_id)
        if not test or test.lab_id != pkg.lab_id:
            raise NotFoundException(message=f"Test with id '{data.test_id}' was not found.")

        param_name = None
        if data.parameter_id:
            param = await self.test_repo.get_parameter_by_id(db, data.parameter_id)
            if not param or param.test_id != test.id:
                raise NotFoundException(
                    message=f"Parameter with id '{data.parameter_id}' was not found in this test."
                )
            param_name = param.name

        existing = await self.repo.get_item_by_test(db, package_id, data.test_id, data.parameter_id)
        if existing:
            msg = (
                "This analyte is already in this package."
                if data.parameter_id
                else f"Test '{test.name}' is already in this package."
            )
            raise AlreadyExistsException(message=msg)

        item = await self.repo.add_item(db, package_id, data.test_id, data.parameter_id, data.sort_order)

        await self._record_event(
            db,
            action="HEALTH_PACKAGE_ITEM_ADDED",
            description="Added a test to a health package.",
            lab_id=pkg.lab_id,
            package_id=pkg.id,
            actor=actor,
            details={
                "test_id": str(test.id),
                "parameter_id": str(data.parameter_id) if data.parameter_id else None,
            },
        )

        return TestPackageItemResponse(
            id=item.id,
            package_id=item.package_id,
            test_id=item.test_id,
            parameter_id=item.parameter_id,
            sort_order=item.sort_order,
            test_name=test.name,
            test_code=test.code,
            parameter_name=param_name,
        )

    async def remove_item(
        self,
        db: AsyncSession,
        package_id: UUID,
        item_id: UUID,
        lab_id: UUID | None = None,
        actor: User | None = None,
    ) -> None:
        """Remove a test item from a package."""
        pkg = await self.repo.get_by_id(db, package_id, lab_id)
        if not pkg:
            raise NotFoundException(message=f"Package with id '{package_id}' was not found.")
        item = await self.repo.get_item(db, package_id, item_id)
        if not item:
            raise NotFoundException(
                message=f"Item with id '{item_id}' was not found in package '{package_id}'."
            )
        await self.repo.remove_item(db, item)
        await self._record_event(
            db,
            action="HEALTH_PACKAGE_ITEM_REMOVED",
            description="Removed a test from a health package.",
            lab_id=pkg.lab_id,
            package_id=pkg.id,
            actor=actor,
            details={
                "test_id": str(item.test_id),
                "parameter_id": str(item.parameter_id) if item.parameter_id else None,
            },
        )
