"""
Service layer for clinical result worksheets, parameter entries, and reference range evaluation.
"""

from collections.abc import Collection
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any
from uuid import UUID

if TYPE_CHECKING:
    from app.models.order import VisitOrder
    from app.models.test import Test as TestModel

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

from app.core.exceptions import NotFoundException, ValidationException
from app.models.audit import AuditCategory
from app.models.order import OrderStatus, OrderTestStatus, Sample, SampleStatus
from app.models.package import TestPackageItem
from app.models.report import Report, ReportStatus
from app.models.test import Test, TestParameter
from app.models.user import User
from app.repositories.order_repository import OrderRepository
from app.repositories.report_repository import ReportRepository
from app.repositories.test_repository import TestCatalogRepository
from app.schemas.result import (
    EvaluatedParameterResult,
    OrderResultEntryRequest,
    OrderResultEntryResponse,
    OrderWorksheetResponse,
    ParameterWorksheetItem,
    TestWorksheetItem,
)
from app.services.audit_service import AuditService
from app.services.clinical_engine import ClinicalEngine, store_value


class ResultService:
    """Business logic for laboratory diagnostic worksheets and result entry."""

    def __init__(
        self,
        order_repo: OrderRepository | None = None,
        report_repo: ReportRepository | None = None,
        catalog_repo: TestCatalogRepository | None = None,
        audit_service: AuditService | None = None,
    ) -> None:
        self.order_repo = order_repo or OrderRepository()
        self.report_repo = report_repo or ReportRepository()
        self.catalog_repo = catalog_repo or TestCatalogRepository()
        self.audit = audit_service or AuditService()
        self.engine = ClinicalEngine()

    async def _package_member_items(self, db: AsyncSession, order: "VisitOrder") -> list[TestPackageItem]:
        """Package member tests lacking an order_test row (deduped, order preserved)."""
        # Explicitly cancelled rows must not reappear through their package.
        covered = {ot.test_id for ot in order.order_tests}
        pkg_ids = [op.package_id for op in order.order_packages]
        if not pkg_ids:
            return []
        stmt = (
            select(TestPackageItem)
            .where(TestPackageItem.package_id.in_(pkg_ids))
            .options(
                selectinload(TestPackageItem.test)
                .selectinload(Test.parameters)
                .selectinload(TestParameter.ranges),
                selectinload(TestPackageItem.test).selectinload(Test.category),
            )
            .order_by(TestPackageItem.package_id, TestPackageItem.sort_order)
        )
        items = (await db.execute(stmt)).scalars().all()
        out: list[TestPackageItem] = []
        seen: set[UUID] = set()
        for item in items:
            if not item.test or item.test_id in covered or item.test_id in seen:
                continue
            seen.add(item.test_id)
            out.append(item)
        return out

    @staticmethod
    def _is_imaging_test(test: object) -> bool:
        """True when the test's catalog category routes to the radiology queue."""
        category = getattr(test, "category", None)
        modality = getattr(category, "modality", None) if category else None
        value = getattr(modality, "value", modality)
        return value == "imaging"

    def _build_worksheet_item(
        self,
        test: "TestModel",
        status_value: str,
        row_id: UUID,
        allowed_param_ids: Collection[UUID | str] | None,
        existing_results: dict[str, Any],
        patient_gender: str | None,
        patient_age: int | None,
    ) -> TestWorksheetItem:
        """One worksheet card for a test; allowed_param_ids narrows analyte-specific bundles."""
        allowed: set[str] | None = (
            {str(x) for x in allowed_param_ids} if allowed_param_ids is not None else None
        )
        param_items: list[ParameterWorksheetItem] = []
        sorted_params = sorted(test.parameters, key=lambda p: p.sort_order)

        for param in sorted_params:
            if not param.is_active:
                continue
            if allowed is not None and str(param.id) not in allowed:
                continue

            ref_range = self.engine.select_reference_range(
                param.ranges,
                patient_gender=patient_gender,
                patient_age=patient_age,
            )

            param_id_str = str(param.id)
            saved_obs = existing_results.get(param_id_str, {})
            current_val = saved_obs.get("value")
            saved_flag = saved_obs.get("flag")
            saved_abnormal = saved_obs.get("is_abnormal", False)
            saved_notes = saved_obs.get("notes")
            saved_remark = saved_obs.get("pathologist_remark")

            param_items.append(
                ParameterWorksheetItem(
                    parameter_id=param.id,
                    parameter_name=param.name,
                    unit=param.unit,
                    decimal_places=param.decimal_places,
                    sort_order=param.sort_order,
                    method_note=param.method_note,
                    reference_low=ref_range.numeric_low if ref_range else None,
                    reference_high=ref_range.numeric_high if ref_range else None,
                    reference_text=ref_range.text_normal if ref_range else None,
                    current_value=str(current_val) if current_val is not None else None,
                    flag=str(saved_flag) if saved_flag is not None else None,
                    is_abnormal=bool(saved_abnormal),
                    notes=str(saved_notes) if saved_notes is not None else None,
                    pathologist_remark=str(saved_remark) if saved_remark is not None else None,
                )
            )

        return TestWorksheetItem(
            order_test_id=row_id,
            test_id=test.id,
            test_name=test.name,
            test_code=test.code,
            result_type=test.result_type.value,
            status=status_value,
            parameters=param_items,
        )

    async def get_worksheet(
        self,
        db: AsyncSession,
        order_id: UUID,
        current_user: User,
    ) -> OrderWorksheetResponse:
        """
        Generate a patient-specific worksheet listing ordered tests, analytes,
        and demographic-tailored biological reference ranges.
        """
        order = await self.order_repo.get_by_id(db, order_id)
        if not order:
            raise NotFoundException(message=f"Order {order_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != order.lab_id:
            raise NotFoundException(message=f"Order {order_id} not found in this branch")

        patient = order.patient
        patient_age = (
            self.engine.calculate_patient_age(patient.date_of_birth, patient.age) if patient else None
        )
        patient_gender = patient.gender if patient else None

        # Bench works the pathology side; imaging has no analytes to enter.
        report = await self.report_repo.get_latest_by_order_id(db, order_id, "pathology")
        existing_results: dict[str, dict[str, object]] = {}
        if report and report.result_data and isinstance(report.result_data, dict):
            raw_data = report.result_data.get("parameters", {})
            if isinstance(raw_data, dict):
                existing_results = raw_data

        test_items: list[TestWorksheetItem] = []

        for ot in order.order_tests:
            if ot.status == OrderTestStatus.CANCELLED:
                continue

            test = ot.test
            if not test or self._is_imaging_test(test):
                continue

            test_items.append(
                self._build_worksheet_item(
                    test=test,
                    status_value=ot.status.value,
                    row_id=ot.id,
                    allowed_param_ids=set(ot.selected_parameter_ids) if ot.selected_parameter_ids else None,
                    existing_results=existing_results,
                    patient_gender=patient_gender,
                    patient_age=patient_age,
                )
            )

        # Package members with no order_test row (legacy + package-only orders):
        # expand read-only so bundled panels are enterable like direct ones.
        for item in await self._package_member_items(db, order):
            if not item.test or self._is_imaging_test(item.test):
                continue
            test_items.append(
                self._build_worksheet_item(
                    test=item.test,
                    status_value="pending",
                    row_id=item.id,
                    allowed_param_ids={item.parameter_id} if item.parameter_id else None,
                    existing_results=existing_results,
                    patient_gender=patient_gender,
                    patient_age=patient_age,
                )
            )

        latest = report
        verifier_name = None
        verifier_id = None
        rejected_by_id = None
        if latest:
            if latest.verifier:
                verifier_name = latest.verifier.name
                verifier_id = latest.verified_by
            if latest.status == ReportStatus.REJECTED and latest.verified_by:
                rejected_by_id = latest.verified_by
            elif latest.previous_report_id:
                prev_report = await self.report_repo.get_by_id(db, latest.previous_report_id)
                if prev_report and prev_report.verified_by:
                    rejected_by_id = prev_report.verified_by
        is_dispatched = bool(latest and latest.released_at is not None)

        return OrderWorksheetResponse(
            order_id=order.id,
            order_number=order.order_number,
            patient_id=patient.id if patient else order.patient_id,
            patient_name=patient.name if patient else "Unknown",
            patient_age=patient_age,
            patient_gender=patient_gender.value if patient_gender else None,
            tests=test_items,
            report_id=latest.id if latest else None,
            report_status=latest.status.value if latest else None,
            report_version=latest.version if latest else None,
            verification_reason=latest.verification_reason if latest else None,
            verified_by_name=verifier_name,
            verified_by_id=verifier_id,
            rejected_by_id=rejected_by_id,
            is_dispatched=is_dispatched,
        )

    async def enter_results(
        self,
        db: AsyncSession,
        order_id: UUID,
        data: OrderResultEntryRequest,
        current_user: User,
    ) -> OrderResultEntryResponse:
        """
        Record clinical parameter observations, evaluate biological reference
        flags, and update report draft findings.
        """
        order = await self.order_repo.get_by_id(db, order_id)
        if not order:
            raise NotFoundException(message=f"Order {order_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != order.lab_id:
            raise NotFoundException(message=f"Order {order_id} not found in this branch")

        if order.status == OrderStatus.CANCELLED:
            raise ValidationException(message="Cannot enter results for a cancelled order")

        # Bench writes land on the pathology side; imaging-only orders have
        # nothing enterable (narrative lives with the radiologist).
        package_items = await self._package_member_items(db, order)
        has_pathology_tests = any(
            ot.test and not self._is_imaging_test(ot.test) and ot.status != OrderTestStatus.CANCELLED
            for ot in order.order_tests
        ) or any(item.test and not self._is_imaging_test(item.test) for item in package_items)
        if not has_pathology_tests:
            raise ValidationException(message="This order has no laboratory panels to enter results for.")
        report = await self.report_repo.get_latest_by_order_id(db, order_id, "pathology")
        if report and report.status in (ReportStatus.VERIFIED, ReportStatus.RELEASED):
            raise ValidationException(
                message=f"Report is already '{report.status.value}'. Create an amendment to modify findings."
            )

        # Rejected rework starts a new version: v1 keeps the rejected record
        # (reason + old values) intact, the technician re-enters into v2.
        reworked_from_version: int | None = None
        if report is not None and report.status == ReportStatus.REJECTED:
            from app.models.test import CategoryModality

            reworked_from_version = report.version
            report = Report(
                order_id=order.id,
                modality=CategoryModality.PATHOLOGY,
                previous_report_id=report.id,
                version=report.version + 1,
                status=ReportStatus.DRAFT,
                verification_reason=f"Rework of rejected v{reworked_from_version}",
            )
            db.add(report)
            await db.flush()

        patient = order.patient
        patient_age = (
            self.engine.calculate_patient_age(patient.date_of_birth, patient.age) if patient else None
        )
        patient_gender = patient.gender if patient else None

        # Build map of valid parameters under this order (direct + bundled).
        # Order-level analyte scopes narrow each test to its ordered analytes.
        valid_params: dict[UUID, tuple[TestParameter, Test]] = {}
        for ot in order.order_tests:
            test = ot.test
            if not test or self._is_imaging_test(test) or ot.status == OrderTestStatus.CANCELLED:
                continue
            scope = {str(x) for x in ot.selected_parameter_ids} if ot.selected_parameter_ids else None
            for param in test.parameters:
                if scope is not None and str(param.id) not in scope:
                    continue
                valid_params[param.id] = (param, test)
        for item in package_items:
            if not item.test or self._is_imaging_test(item.test):
                continue
            for param in item.test.parameters:
                if item.parameter_id and param.id != item.parameter_id:
                    continue
                valid_params.setdefault(param.id, (param, item.test))

        evaluated_results: list[EvaluatedParameterResult] = []
        abnormal_count = 0

        # Existing stored observations
        param_dict: dict[str, dict[str, object]] = {}
        if report and report.result_data and isinstance(report.result_data, dict):
            raw_params = report.result_data.get("parameters", {})
            if isinstance(raw_params, dict):
                param_dict = raw_params.copy()

        for entry in data.results:
            if entry.parameter_id not in valid_params:
                raise ValidationException(
                    message=f"Parameter '{entry.parameter_id}' does not belong to any ordered test."
                )

            param, _ = valid_params[entry.parameter_id]
            ref_range = self.engine.select_reference_range(
                param.ranges,
                patient_gender=patient_gender,
                patient_age=patient_age,
            )

            flag, is_abnormal = self.engine.evaluate_value(entry.value, param, ref_range)
            if is_abnormal:
                abnormal_count += 1

            # Store the normalized reading ("10,000" → "10000") so reports,
            # flags, and re-entry all agree on what was measured.
            stored_value = store_value(entry.value)

            evaluated = EvaluatedParameterResult(
                parameter_id=param.id,
                parameter_name=param.name,
                unit=param.unit,
                value=stored_value,
                reference_low=ref_range.numeric_low if ref_range else None,
                reference_high=ref_range.numeric_high if ref_range else None,
                reference_text=ref_range.text_normal if ref_range else None,
                flag=flag,
                is_abnormal=is_abnormal,
                notes=entry.notes,
            )
            evaluated_results.append(evaluated)

            # Store in result_data payload
            param_dict[str(param.id)] = {
                "parameter_id": str(param.id),
                "parameter_name": param.name,
                "unit": param.unit,
                "value": stored_value,
                "reference_low": str(ref_range.numeric_low)
                if ref_range and ref_range.numeric_low is not None
                else None,
                "reference_high": str(ref_range.numeric_high)
                if ref_range and ref_range.numeric_high is not None
                else None,
                "reference_text": ref_range.text_normal if ref_range else None,
                "flag": flag,
                "is_abnormal": is_abnormal,
                "notes": entry.notes,
                "entered_at": datetime.now(UTC).isoformat(),
            }

        # Package updated result_data
        result_data_payload = {
            "order_number": order.order_number,
            "patient_age_at_test": patient_age,
            "patient_gender": patient_gender.value if patient_gender else None,
            "parameters": param_dict,
            "last_updated_at": datetime.now(UTC).isoformat(),
        }
        if reworked_from_version is not None:
            result_data_payload["reworked_from_version"] = reworked_from_version

        # Create or update Report draft
        if not report:
            from app.models.test import CategoryModality

            report = Report(
                order_id=order.id,
                modality=CategoryModality.PATHOLOGY,
                version=1,
                status=ReportStatus.DRAFT,
                result_data=result_data_payload,
                notes=data.notes,
            )
            report = await self.report_repo.create(db, report)
        else:
            report.result_data = result_data_payload
            if data.notes:
                report.notes = data.notes

        # Advance order tests to PROCESSING
        for ot in order.order_tests:
            if ot.status == OrderTestStatus.DRAFT:
                ot.status = OrderTestStatus.PROCESSING

        # If order was sample_collected or pending, advance to PROCESSING
        if order.status in (OrderStatus.PENDING, OrderStatus.CONFIRMED, OrderStatus.SAMPLE_COLLECTED):
            order.status = OrderStatus.PROCESSING

        # Auto-collect and advance any pending specimens for this order so the
        # dashboard and worklists do not leave them in "to collect" after results are entered.
        sample_stmt = select(Sample).where(Sample.order_id == order.id)
        sample_res = await db.execute(sample_stmt)
        order_samples = sample_res.scalars().all()
        now_utc = datetime.now(UTC)
        for s in order_samples:
            if s.status == SampleStatus.PENDING:
                s.status = SampleStatus.PROCESSING
                s.collected_at = s.collected_at or now_utc
                s.collected_by = s.collected_by or current_user.id
            elif s.status == SampleStatus.COLLECTED:
                s.status = SampleStatus.PROCESSING

        actor_role = getattr(current_user.role, "value", current_user.role)
        await self.audit.record_event(
            db,
            action="RESULTS_ENTERED",
            description=f"{len(evaluated_results)} analyte result(s) recorded for order {order.order_number}",
            category=AuditCategory.CLINICAL.value,
            actor_name=current_user.name,
            actor_role=str(actor_role),
            actor_id=current_user.id,
            target_type="report",
            target_id=str(report.id),
            ip_address=None,
            details={
                "order_id": str(order.id),
                "version": report.version,
                "status": report.status.value,
                "analyte_count": len(evaluated_results),
                "abnormal_count": abnormal_count,
            },
            lab_id=order.lab_id,
        )

        await db.commit()
        await db.refresh(report)

        return OrderResultEntryResponse(
            order_id=order.id,
            order_number=order.order_number,
            report_id=report.id,
            report_status=report.status.value,
            total_parameters_entered=len(evaluated_results),
            abnormal_count=abnormal_count,
            evaluated_results=evaluated_results,
        )
