"""
Service layer for visit orders and pricing snapshots.
"""

import asyncio
import math
from datetime import UTC, datetime
from decimal import Decimal
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import NotFoundException, ValidationException
from app.core.money import npr
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.core.security import hash_password
from app.models.appointment import AppointmentStatus
from app.models.order import OrderPackage, OrderStatus, OrderTest, OrderTestStatus, VisitOrder
from app.models.package import TestPackageItem
from app.models.report import Report, ReportStatus
from app.models.test import CategoryModality, Test, TestParameter
from app.repositories.appointment_repository import AppointmentRepository
from app.repositories.lab_repository import LabRepository
from app.repositories.order_repository import OrderRepository
from app.repositories.package_repository import PackageRepository
from app.repositories.patient_repository import PatientRepository
from app.repositories.referral_repository import ReferralRepository
from app.repositories.test_repository import TestCatalogRepository
from app.schemas.order import (
    OrderCreate,
    OrderPackageResponse,
    OrderTestResponse,
    OrderUpdate,
    VisitOrderDetailResponse,
    VisitOrderResponse,
)


class OrderService:
    """Business logic for laboratory visit orders."""

    def __init__(
        self,
        order_repo: OrderRepository | None = None,
        patient_repo: PatientRepository | None = None,
        appointment_repo: AppointmentRepository | None = None,
        test_repo: TestCatalogRepository | None = None,
        package_repo: PackageRepository | None = None,
        referral_repo: ReferralRepository | None = None,
    ) -> None:
        self.repo = order_repo or OrderRepository()
        self.patient_repo = patient_repo or PatientRepository()
        self.appointment_repo = appointment_repo or AppointmentRepository()
        self.test_repo = test_repo or TestCatalogRepository()
        self.package_repo = package_repo or PackageRepository()
        self.referral_repo = referral_repo or ReferralRepository()

    def _calc_total(self, order: VisitOrder) -> Decimal:
        """Calculate total amount from snapshot prices."""
        total = Decimal("0.00")
        for t in order.order_tests:
            if t.status != OrderTestStatus.CANCELLED:
                total += t.price_at_order
        for p in order.order_packages:
            total += p.price_at_order
        return total

    def _to_summary_response(self, order: VisitOrder) -> VisitOrderResponse:
        bill = getattr(order, "bill", None)
        return VisitOrderResponse(
            id=order.id,
            lab_id=order.lab_id,
            patient_id=order.patient_id,
            appointment_id=order.appointment_id,
            referral_id=order.referral_id,
            order_number=order.order_number,
            status=order.status,
            referred_by_custom=order.referred_by_custom,
            next_date=order.next_date,
            destination_country=order.destination_country,
            is_abroad=bool(order.destination_country and order.destination_country.strip()),
            notes=order.notes,
            total_amount=self._calc_total(order),
            patient_name=order.patient.name if order.patient else None,
            patient_code=order.patient.patient_code if order.patient else None,
            bill_number=bill.bill_number if bill else None,
            bill_status=bill.status.value if bill else None,
            created_at=order.created_at,
            updated_at=order.updated_at,
        )

    def _to_detail_response(self, order: VisitOrder) -> VisitOrderDetailResponse:
        tests = [
            OrderTestResponse(
                id=t.id,
                order_id=t.order_id,
                test_id=t.test_id,
                price_at_order=t.price_at_order,
                status=t.status,
                test_name=t.test.name if t.test else None,
                test_code=t.test.code if t.test else None,
            )
            for t in order.order_tests
        ]
        packages = [
            OrderPackageResponse(
                id=p.id,
                order_id=p.order_id,
                package_id=p.package_id,
                price_at_order=p.price_at_order,
                package_name=p.package.name if p.package else None,
                package_code=p.package.code if p.package else None,
            )
            for p in order.order_packages
        ]
        return VisitOrderDetailResponse(
            id=order.id,
            lab_id=order.lab_id,
            patient_id=order.patient_id,
            appointment_id=order.appointment_id,
            referral_id=order.referral_id,
            order_number=order.order_number,
            status=order.status,
            referred_by_custom=order.referred_by_custom,
            next_date=order.next_date,
            destination_country=order.destination_country,
            is_abroad=bool(order.destination_country and order.destination_country.strip()),
            notes=order.notes,
            total_amount=self._calc_total(order),
            patient_name=order.patient.name if order.patient else None,
            patient_code=order.patient.patient_code if order.patient else None,
            tests=tests,
            packages=packages,
            created_at=order.created_at,
            updated_at=order.updated_at,
        )

    async def list_orders(
        self,
        db: AsyncSession,
        pagination: PaginationParams,
        lab_id: UUID | None = None,
        status: OrderStatus | None = None,
        patient_id: UUID | None = None,
        is_abroad: bool | None = None,
        destination_country: str | None = None,
        q: str | None = None,
        created_from: datetime | None = None,
        created_to: datetime | None = None,
    ) -> PaginatedResponse[VisitOrderResponse]:
        """Fetch paginated orders."""
        orders, total = await self.repo.list_orders(
            db,
            limit=pagination.limit,
            offset=pagination.offset,
            lab_id=lab_id,
            status=status,
            patient_id=patient_id,
            is_abroad=is_abroad,
            destination_country=destination_country,
            q=q,
            created_from=created_from,
            created_to=created_to,
        )
        items = [self._to_summary_response(o) for o in orders]
        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_order(
        self, db: AsyncSession, order_id: UUID, lab_id: UUID | None = None
    ) -> VisitOrderDetailResponse:
        """Fetch detailed order by ID."""
        order = await self.repo.get_by_id(db, order_id, lab_id=lab_id)
        if not order:
            raise NotFoundException(message=f"Order with id '{order_id}' was not found.")
        return self._to_detail_response(order)

    async def create_order(self, db: AsyncSession, data: OrderCreate) -> VisitOrderDetailResponse:
        """Create a visit order with pricing snapshot."""
        if data.lab_id is None:
            data.lab_id = await LabRepository().get_default_lab_id(db)

        patient = await self.patient_repo.get_by_id(db, data.patient_id, lab_id=data.lab_id)
        if not patient:
            raise NotFoundException(message=f"Patient with id '{data.patient_id}' was not found.")

        # Portal PIN rotates every visit: new credential for the patient portal,
        # printed on the bill receipt and the signed report.
        from app.core.portal_pin import encrypt_pin, generate_pin

        visit_pin = generate_pin()
        patient.pin_hash = await asyncio.to_thread(hash_password, visit_pin)
        patient.portal_pin_enc = encrypt_pin(visit_pin)
        await db.flush()

        appointment = None
        if data.appointment_id:
            appointment = await self.appointment_repo.get_by_id(db, data.appointment_id, lab_id=data.lab_id)
            if not appointment:
                raise NotFoundException(message=f"Appointment with id '{data.appointment_id}' was not found.")

        if data.referral_id and not await self.referral_repo.get_by_id(
            db, data.referral_id, lab_id=data.lab_id
        ):
            raise NotFoundException(message=f"Referral with id '{data.referral_id}' was not found.")

        if not data.test_ids and not data.package_ids:
            raise ValidationException(message="An order must contain at least one test or health package.")

        order_tests: list[OrderTest] = []
        scope_map: dict[UUID, list[str]] = {}
        # Analyte prices per test for scoped (partial-panel) pricing. Mirrors the
        # desk preview: explicit analyte price wins, otherwise the panel tariff
        # is split evenly across its analytes. Whole panels keep panel price.
        scope_price_parts: dict[UUID, tuple[dict[UUID, Decimal | None], int]] = {}
        if data.test_parameters:
            wanted: dict[UUID, set[UUID]] = {}
            for scope in data.test_parameters:
                wanted.setdefault(scope.test_id, set()).update(scope.parameter_ids)
            if wanted:
                rows = (
                    (
                        await db.execute(
                            select(TestParameter.id, TestParameter.test_id)
                            .join(Test, Test.id == TestParameter.test_id)
                            .where(
                                Test.lab_id == data.lab_id,
                                TestParameter.id.in_(list({p for ids in wanted.values() for p in ids})),
                            )
                        )
                    )
                    .tuples()
                    .all()
                )
                found: dict[UUID, UUID] = dict(rows)
                for tid, pids in wanted.items():
                    if tid not in data.test_ids:
                        raise ValidationException(
                            message="test_parameters reference a test that is not booked on this order."
                        )
                    for pid in pids:
                        if found.get(pid) != tid:
                            raise ValidationException(
                                message="One or more scoped analytes do not belong to their test."
                            )
                    scope_map[tid] = [str(p) for p in pids]
                price_rows = (
                    await db.execute(
                        select(TestParameter.id, TestParameter.test_id, TestParameter.price).where(
                            TestParameter.test_id.in_(list(wanted))
                        )
                    )
                ).all()
                grouped: dict[UUID, list[tuple[UUID, Decimal | None]]] = {}
                for pid, tid, price in price_rows:
                    grouped.setdefault(tid, []).append((pid, price))
                for tid in wanted:
                    items = grouped.get(tid, [])
                    scope_price_parts[tid] = (dict(items), len(items))
        for tid in data.test_ids:
            t = await self.test_repo.get_test_by_id(db, tid, lab_id=data.lab_id)
            if not t:
                raise NotFoundException(message=f"Test with id '{tid}' was not found.")
            scoped = scope_map.get(tid)
            test_price = t.price
            if scoped:
                # Partial-panel selection: price the picked analytes, not the panel.
                pmap, total = scope_price_parts.get(tid, ({}, 0))
                divisor = total or 1
                analyte_total = Decimal("0.00")
                for pid_str in scoped:
                    explicit = pmap.get(UUID(pid_str))
                    if explicit is not None and explicit > 0:
                        analyte_total += explicit
                    else:
                        analyte_total += (t.price or Decimal("0.00")) / divisor
                test_price = npr(analyte_total)
            order_tests.append(
                OrderTest(
                    test_id=t.id,
                    price_at_order=test_price,
                    status=OrderTestStatus.DRAFT,
                    selected_parameter_ids=scoped,
                    test=t,
                )
            )

        order_packages: list[OrderPackage] = []
        for pid in data.package_ids:
            pkg = await self.package_repo.get_by_id(db, pid, lab_id=data.lab_id)
            if not pkg:
                raise NotFoundException(message=f"Package with id '{pid}' was not found.")
            order_packages.append(
                OrderPackage(
                    package_id=pkg.id,
                    price_at_order=pkg.price,
                    package=pkg,
                )
            )

        # Expand package members into zero-priced order_test rows so bundled
        # panels are enterable/trackable like direct ones. Billing sums the
        # package snapshot, so member rows stay at 0 (no double count).
        covered_test_ids = set(data.test_ids)
        for pid in data.package_ids:
            member_ids = await db.execute(
                select(TestPackageItem.test_id).where(TestPackageItem.package_id == pid)
            )
            for (member_id,) in member_ids.all():
                if member_id in covered_test_ids:
                    continue
                covered_test_ids.add(member_id)
                t_obj = await self.test_repo.get_test_by_id(db, member_id, lab_id=data.lab_id)
                order_tests.append(
                    OrderTest(
                        test_id=member_id,
                        price_at_order=Decimal("0.00"),
                        status=OrderTestStatus.DRAFT,
                        test=t_obj,
                    )
                )

        order_number = await self.repo.get_next_order_number(db, data.lab_id)

        order = VisitOrder(
            lab_id=data.lab_id,
            patient_id=data.patient_id,
            patient=patient,
            appointment_id=data.appointment_id,
            referral_id=data.referral_id,
            order_number=order_number,
            status=OrderStatus.PENDING,
            referred_by_custom=data.referred_by_custom,
            next_date=data.next_date,
            destination_country=data.destination_country,
            notes=data.notes,
        )

        created_order = await self.repo.create(db, order, order_tests, order_packages)

        if appointment and appointment.status == AppointmentStatus.PENDING:
            appointment.status = AppointmentStatus.CHECKED_IN
            await db.flush()

        # One DRAFT report per discipline side from birth: mixed orders split
        # into pathology + imaging reports so each queue sees only its own side.
        from app.repositories.report_repository import ReportRepository

        mix = await ReportRepository().modality_by_order_ids(db, [created_order.id])
        side = mix.get(created_order.id, "pathology")
        sides = {"imaging", "pathology"} if side == "mixed" else {side}
        for side in sides:
            db.add(
                Report(
                    order_id=created_order.id,
                    modality=CategoryModality(side),
                    version=1,
                    status=ReportStatus.DRAFT,
                )
            )
        await db.flush()

        if appointment:
            appointment.visit_order_id = created_order.id
            appointment.status = AppointmentStatus.COMPLETED
            await db.flush()

        full_order = await self.repo.get_by_id(db, created_order.id, lab_id=data.lab_id)
        return self._to_detail_response(full_order or created_order)

    async def update_order(
        self, db: AsyncSession, order_id: UUID, data: OrderUpdate, lab_id: UUID | None = None
    ) -> VisitOrderDetailResponse:
        """Update order details."""
        order = await self.repo.get_by_id(db, order_id, lab_id=lab_id)
        if not order:
            raise NotFoundException(message=f"Order with id '{order_id}' was not found.")

        updated = await self.repo.update(db, order, data)
        return self._to_detail_response(updated)

    async def _ensure_order_editable(
        self, db: AsyncSession, order_id: UUID, lab_id: UUID | None = None
    ) -> VisitOrder:
        """Fetch an order and block panel changes once its report is signed out."""
        order = await self.repo.get_by_id(db, order_id, lab_id=lab_id)
        if not order:
            raise NotFoundException(message=f"Order with id '{order_id}' was not found.")
        stmt = select(Report.status).where(Report.order_id == order_id)
        statuses = set((await db.execute(stmt)).scalars().all())
        if statuses & {ReportStatus.VERIFIED, ReportStatus.RELEASED}:
            raise ValidationException(
                message="Report is already verified/released — order panels can no longer be changed."
            )
        return order

    async def attach_tests(
        self, db: AsyncSession, order_id: UUID, test_ids: list[UUID], lab_id: UUID | None = None
    ) -> VisitOrderDetailResponse:
        """Attach panels to an existing order (rescue for legacy test-less orders)."""
        order = await self._ensure_order_editable(db, order_id, lab_id=lab_id)
        existing = {ot.test_id for ot in order.order_tests}
        added = 0
        for tid in test_ids:
            if tid in existing:
                continue
            t = await self.test_repo.get_test_by_id(db, tid, lab_id=order.lab_id)
            if not t:
                raise NotFoundException(message=f"Test with id '{tid}' was not found.")
            db.add(
                OrderTest(
                    order_id=order.id,
                    test_id=t.id,
                    price_at_order=t.price,
                    status=OrderTestStatus.DRAFT,
                    test=t,
                )
            )
            existing.add(tid)
            added += 1
        if added == 0:
            raise ValidationException(message="All selected tests are already on this order.")
        await db.flush()
        await self._ensure_side_reports(db, order_id)
        reloaded_order = await self.repo.get_by_id(db, order_id, lab_id=order.lab_id)
        assert reloaded_order is not None
        return self._to_detail_response(reloaded_order)

    async def _ensure_side_reports(self, db: AsyncSession, order_id: UUID) -> None:
        """Create DRAFT reports for newly introduced discipline sides (attach flows)."""
        from app.repositories.report_repository import ReportRepository

        repo = ReportRepository()
        mix = await repo.modality_by_order_ids(db, [order_id])
        sides = {"imaging", "pathology"} if mix.get(order_id) == "mixed" else {mix.get(order_id, "pathology")}
        missing = sides - await repo.modalities_present(db, order_id)
        for side in missing:
            db.add(
                Report(
                    order_id=order_id,
                    modality=CategoryModality(side),
                    version=1,
                    status=ReportStatus.DRAFT,
                )
            )
        if missing:
            await db.flush()

    async def attach_packages(
        self, db: AsyncSession, order_id: UUID, package_ids: list[UUID], lab_id: UUID | None = None
    ) -> VisitOrderDetailResponse:
        """Attach health packages to an existing order (billing bundle; see note)."""
        order = await self._ensure_order_editable(db, order_id, lab_id=lab_id)
        existing = {op.package_id for op in order.order_packages}
        covered = {ot.test_id for ot in order.order_tests}
        added = 0
        for pid in package_ids:
            if pid in existing:
                continue
            pkg = await self.package_repo.get_by_id(db, pid, lab_id=order.lab_id)
            if not pkg:
                raise NotFoundException(message=f"Package with id '{pid}' was not found.")
            db.add(
                OrderPackage(
                    order_id=order.id,
                    package_id=pkg.id,
                    price_at_order=pkg.price,
                    package=pkg,
                )
            )
            existing.add(pid)
            added += 1
            member_ids = await db.execute(
                select(TestPackageItem.test_id).where(TestPackageItem.package_id == pid)
            )
            for (member_id,) in member_ids.all():
                if member_id in covered:
                    continue
                covered.add(member_id)
                db.add(
                    OrderTest(
                        order_id=order.id,
                        test_id=member_id,
                        price_at_order=Decimal("0.00"),
                        status=OrderTestStatus.DRAFT,
                    )
                )
        if added == 0:
            raise ValidationException(message="All selected packages are already on this order.")
        await db.flush()
        await self._ensure_side_reports(db, order_id)
        reloaded_order = await self.repo.get_by_id(db, order_id, lab_id=order.lab_id)
        assert reloaded_order is not None
        return self._to_detail_response(reloaded_order)

    async def cancel_order(
        self, db: AsyncSession, order_id: UUID, lab_id: UUID | None = None
    ) -> VisitOrderDetailResponse:
        """Cancel an order."""
        order = await self.repo.get_by_id(db, order_id, lab_id=lab_id)
        if not order:
            raise NotFoundException(message=f"Order with id '{order_id}' was not found.")
        if order.status in (OrderStatus.CANCELLED, OrderStatus.COMPLETED):
            raise ValidationException(message=f"Cannot cancel order with status '{order.status.value}'.")

        order.status = OrderStatus.CANCELLED
        order.updated_at = datetime.now(UTC)
        for t in order.order_tests:
            t.status = OrderTestStatus.CANCELLED
            t.updated_at = datetime.now(UTC)

        await db.flush()
        return self._to_detail_response(order)
