"""
Service layer for Sample collection and specimen tracking workflows.
"""

import math
from datetime import UTC, datetime
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import NotFoundException, ValidationException
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.models.audit import AuditCategory
from app.models.order import OrderStatus, OrderTestStatus, Sample, SampleStatus
from app.models.user import User
from app.repositories.order_repository import OrderRepository
from app.repositories.sample_repository import SampleRepository
from app.schemas.sample import (
    SampleCollectRequest,
    SampleCreate,
    SampleDetailResponse,
    SampleRejectRequest,
    SampleResponse,
)
from app.services.audit_service import AuditService


class SampleService:
    """Business logic for laboratory specimen collection and tracking."""

    def __init__(
        self,
        sample_repo: SampleRepository | None = None,
        order_repo: OrderRepository | None = None,
        audit_service: AuditService | None = None,
    ) -> None:
        self.repo = sample_repo or SampleRepository()
        self.order_repo = order_repo or OrderRepository()
        self.audit = audit_service or AuditService()

    async def _record_sample_event(
        self,
        db: AsyncSession,
        *,
        action: str,
        description: str,
        sample: Sample,
        actor: User,
        previous_status: str | None = None,
    ) -> None:
        role = getattr(actor.role, "value", actor.role)
        details = {"status": sample.status.value if hasattr(sample.status, "value") else str(sample.status)}
        if previous_status is not None:
            details["previous_status"] = previous_status
        await self.audit.record_event(
            db,
            action=action,
            description=description,
            category=AuditCategory.CLINICAL.value,
            actor_name=actor.name,
            actor_role=str(role),
            actor_id=actor.id,
            target_type="sample",
            target_id=str(sample.id),
            ip_address=None,
            details=details,
            lab_id=sample.lab_id,
        )

    def _to_response(self, sample: Sample) -> SampleResponse:
        """Convert Sample ORM model to response schema."""
        collector_name = None
        if sample.collected_by_user:
            collector_name = sample.collected_by_user.name

        return SampleResponse(
            id=sample.id,
            order_id=sample.order_id,
            lab_id=sample.lab_id,
            sample_code=sample.sample_code,
            barcode=sample.barcode,
            sample_type=sample.sample_type,
            status=sample.status,
            collected_by=sample.collected_by,
            collected_at=sample.collected_at,
            notes=sample.notes,
            collector_name=collector_name,
            created_at=sample.created_at,
            updated_at=sample.updated_at,
        )

    def _to_detail_response(self, sample: Sample) -> SampleDetailResponse:
        """Convert Sample to detailed schema with order and patient context."""
        resp = self._to_response(sample)

        order_number = None
        patient_id = None
        patient_name = None
        patient_code = None
        patient_phone = None
        patient_gender = None
        patient_age = None
        tests_summary = None
        report_status = None
        verified_by_name = None
        is_dispatched = False

        if sample.order:
            order_number = sample.order.order_number
            if sample.order.order_tests:
                test_names = [ot.test.name for ot in sample.order.order_tests if ot.test and ot.test.name]
                if test_names:
                    tests_summary = ", ".join(test_names[:5])
                    if len(test_names) > 5:
                        tests_summary += f" +{len(test_names) - 5}"
            if sample.order.patient:
                patient_id = sample.order.patient.id
                patient_name = sample.order.patient.name
                patient_code = sample.order.patient.patient_code
                patient_phone = sample.order.patient.phone
                if sample.order.patient.gender:
                    patient_gender = sample.order.patient.gender.value
                if sample.order.patient.age is not None:
                    patient_age = sample.order.patient.age
                elif sample.order.patient.date_of_birth:
                    from datetime import date

                    dob = sample.order.patient.date_of_birth
                    today = date.today()
                    patient_age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))

            if hasattr(sample.order, "reports") and sample.order.reports:
                latest_report = sorted(sample.order.reports, key=lambda r: r.version, reverse=True)[0]
                if latest_report:
                    report_status = (
                        latest_report.status.value
                        if hasattr(latest_report.status, "value")
                        else str(latest_report.status)
                    )
                    if latest_report.verifier:
                        verified_by_name = latest_report.verifier.name
                    is_dispatched = bool(latest_report.released_at is not None)

        return SampleDetailResponse(
            **resp.model_dump(),
            order_number=order_number,
            patient_id=patient_id,
            patient_name=patient_name,
            patient_code=patient_code,
            patient_phone=patient_phone,
            patient_gender=patient_gender,
            patient_age=patient_age,
            tests_summary=tests_summary,
            report_status=report_status,
            verified_by_name=verified_by_name,
            is_dispatched=is_dispatched,
        )

    async def create_sample_for_order(
        self,
        db: AsyncSession,
        order_id: UUID,
        data: SampleCreate,
        current_user: User,
    ) -> SampleDetailResponse:
        """Create a new sample tracking record under an order."""
        order = await self.order_repo.get_by_id(db, order_id)
        if not order:
            raise NotFoundException(message=f"Order {order_id} not found")

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

        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")

        # Imaging-only orders (e.g. X-ray) need no specimen: chain of custody
        # is specimen-based, and stray samples corrupt backlog counts.
        from app.repositories.report_repository import ReportRepository

        modalities = await ReportRepository().modality_by_order_ids(db, [order.id])
        if modalities.get(order.id, "mixed") == "imaging":
            raise ValidationException(message="Imaging-only orders do not require specimens.")

        sample_code = await self.repo.get_next_sample_code(db, order.lab_id)
        barcode = sample_code

        sample = Sample(
            order_id=order.id,
            lab_id=order.lab_id,
            sample_code=sample_code,
            barcode=barcode,
            sample_type=data.sample_type,
            status=SampleStatus.PENDING,
            notes=data.notes,
        )

        sample = await self.repo.create(db, sample)
        await self._record_sample_event(
            db,
            action="SPECIMEN_CREATED",
            description="Created a specimen record for a visit order.",
            sample=sample,
            actor=current_user,
        )
        await db.commit()

        # Re-fetch with relationships loaded
        reloaded = await self.repo.get_by_id(db, sample.id)
        return self._to_detail_response(reloaded or sample)

    async def collect_sample(
        self,
        db: AsyncSession,
        sample_id: UUID,
        data: SampleCollectRequest,
        current_user: User,
    ) -> SampleDetailResponse:
        """
        Record specimen collection by phlebotomist.

        Transitions:
          status: PENDING -> COLLECTED
          collected_by: current_user.id
          collected_at: timestamp

        If all samples for the parent order are collected, cascades
        order.status -> SAMPLE_COLLECTED.
        """
        sample = await self.repo.get_by_id(db, sample_id)
        if not sample:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if sample.status != SampleStatus.PENDING:
            raise ValidationException(
                message=f"Cannot collect sample with status '{sample.status.value}'. Must be 'pending'."
            )

        previous_status = sample.status.value
        collection_time = data.collected_at or datetime.now(UTC)
        sample.status = SampleStatus.COLLECTED
        sample.collected_by = current_user.id
        sample.collected_at = collection_time

        if data.notes:
            if sample.notes:
                sample.notes = f"{sample.notes}\nCollection note: {data.notes}"
            else:
                sample.notes = data.notes

        # Status cascading check for parent order
        order_samples = await self.repo.get_by_order_id(db, sample.order_id)
        all_collected = all(s.status != SampleStatus.PENDING for s in order_samples)

        if all_collected and sample.order:
            if sample.order.status in (OrderStatus.PENDING, OrderStatus.CONFIRMED):
                sample.order.status = OrderStatus.SAMPLE_COLLECTED

        await self._record_sample_event(
            db,
            action="SPECIMEN_COLLECTED",
            description="Collected a specimen.",
            sample=sample,
            actor=current_user,
            previous_status=previous_status,
        )

        await db.commit()
        await db.refresh(sample)

        reloaded = await self.repo.get_by_id(db, sample.id)
        return self._to_detail_response(reloaded or sample)

    async def receive_sample(
        self,
        db: AsyncSession,
        sample_id: UUID,
        current_user: User,
    ) -> SampleDetailResponse:
        """
        Receive specimen into the laboratory testing department.

        Transitions:
          Sample: COLLECTED -> PROCESSING
          Order: SAMPLE_COLLECTED -> PROCESSING
          OrderTests: DRAFT -> PROCESSING
        """
        sample = await self.repo.get_by_id(db, sample_id)
        if not sample:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if sample.status not in (SampleStatus.PENDING, SampleStatus.COLLECTED, SampleStatus.PROCESSING):
            raise ValidationException(message="Only pending or collected specimens can be received.")
        if sample.order and sample.order.status == OrderStatus.CANCELLED:
            raise ValidationException(message="Cannot receive a specimen for a cancelled order.")

        if sample.status == SampleStatus.PROCESSING:
            # Already received in lab processing — idempotent return
            return self._to_detail_response(sample)

        previous_status = sample.status.value
        if sample.status == SampleStatus.PENDING:
            # Directly received in laboratory — auto-record collection
            sample.status = SampleStatus.COLLECTED
            sample.collected_at = datetime.now(UTC)
            sample.collected_by = current_user.id

        sample.status = SampleStatus.PROCESSING

        if sample.order:
            sample.order.status = OrderStatus.PROCESSING
            for ot in sample.order.order_tests:
                if ot.status == OrderTestStatus.DRAFT:
                    ot.status = OrderTestStatus.PROCESSING

        await self._record_sample_event(
            db,
            action="SPECIMEN_RECEIVED",
            description="Received a specimen into laboratory processing.",
            sample=sample,
            actor=current_user,
            previous_status=previous_status,
        )

        await db.commit()
        await db.refresh(sample)

        reloaded = await self.repo.get_by_id(db, sample.id)
        return self._to_detail_response(reloaded or sample)

    async def complete_sample(
        self,
        db: AsyncSession,
        sample_id: UUID,
        current_user: User,
    ) -> SampleDetailResponse:
        """Mark physical specimen testing as completed."""
        sample = await self.repo.get_by_id(db, sample_id)
        if not sample:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if sample.status != SampleStatus.PROCESSING:
            raise ValidationException(
                message=f"Cannot complete sample with status '{sample.status.value}'. Must be 'processing'."
            )

        previous_status = sample.status.value
        sample.status = SampleStatus.COMPLETED
        await self._record_sample_event(
            db,
            action="SPECIMEN_COMPLETED",
            description="Completed physical specimen processing.",
            sample=sample,
            actor=current_user,
            previous_status=previous_status,
        )
        await db.commit()
        await db.refresh(sample)

        reloaded = await self.repo.get_by_id(db, sample.id)
        return self._to_detail_response(reloaded or sample)

    async def reject_sample(
        self,
        db: AsyncSession,
        sample_id: UUID,
        data: SampleRejectRequest,
        current_user: User,
    ) -> SampleDetailResponse:
        """
        Reject specimen due to pre-analytical defect (clotted, hemolyzed, insufficient volume).
        Appends reason to specimen notes for auditability.
        """
        sample = await self.repo.get_by_id(db, sample_id)
        if not sample:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        previous_status = sample.status.value
        timestamp = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
        rejection_entry = f"[REJECTED by {current_user.name} at {timestamp}]: {data.notes}"
        if sample.notes:
            sample.notes = f"{sample.notes}\n{rejection_entry}"
        else:
            sample.notes = rejection_entry

        # Leave in pending for re-collection, or reset collection time
        sample.status = SampleStatus.PENDING
        sample.collected_by = None
        sample.collected_at = None

        await self._record_sample_event(
            db,
            action="SPECIMEN_REJECTED",
            description="Rejected a specimen and flagged it for redraw.",
            sample=sample,
            actor=current_user,
            previous_status=previous_status,
        )

        await db.commit()
        await db.refresh(sample)

        reloaded = await self.repo.get_by_id(db, sample.id)
        return self._to_detail_response(reloaded or sample)

    async def get_by_barcode(
        self,
        db: AsyncSession,
        barcode: str,
        current_user: User,
    ) -> SampleDetailResponse:
        """Lookup sample by tube barcode for scanner stations."""
        sample = await self.repo.get_by_barcode(
            db, barcode.strip(), None if current_user.is_superadmin else current_user.lab_id
        )
        if not sample:
            raise NotFoundException(message=f"Sample with barcode '{barcode}' not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample with barcode '{barcode}' not found")

        return self._to_detail_response(sample)

    async def get_sample(
        self,
        db: AsyncSession,
        sample_id: UUID,
        current_user: User,
    ) -> SampleDetailResponse:
        """Fetch sample details by primary key."""
        sample = await self.repo.get_by_id(db, sample_id)
        if not sample:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        if not current_user.is_superadmin and current_user.lab_id != sample.lab_id:
            raise NotFoundException(message=f"Sample {sample_id} not found")

        return self._to_detail_response(sample)

    async def list_samples(
        self,
        db: AsyncSession,
        current_user: User,
        pagination: PaginationParams,
        status: SampleStatus | str | None = None,
        sample_type: str | None = None,
        order_id: UUID | str | None = None,
        collected_by: UUID | None = None,
    ) -> PaginatedResponse[SampleDetailResponse]:
        """List samples with filters and pagination."""
        lab_id = None if current_user.is_superadmin else current_user.lab_id

        total_items = await self.repo.count(
            db,
            lab_id=lab_id,
            status=status,
            sample_type=sample_type,
            order_id=order_id,
            collected_by=collected_by,
        )

        samples = await self.repo.list(
            db,
            lab_id=lab_id,
            status=status,
            sample_type=sample_type,
            order_id=order_id,
            collected_by=collected_by,
            offset=pagination.offset,
            limit=pagination.limit,
        )

        data = [self._to_detail_response(s) for s in samples]
        total_pages = math.ceil(total_items / pagination.limit) if total_items > 0 else 0

        return PaginatedResponse(
            data=data,
            meta=PaginationMeta(
                total=total_items,
                page=pagination.page,
                limit=pagination.limit,
                pages=total_pages,
            ),
        )
