"""
Service layer for diagnostic reports, pathologist verification, rejections, and versioned amendments.
"""

import asyncio
import math
import secrets
from copy import deepcopy
from datetime import UTC, datetime
from decimal import Decimal
from typing import Any
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ForbiddenException, NotFoundException, ValidationException
from app.core.pagination import PaginationParams
from app.core.responses import PaginatedResponse, PaginationMeta
from app.models.order import OrderStatus, OrderTestStatus
from app.models.report import Report, ReportStatus
from app.models.user import User, UserRole
from app.repositories.order_repository import OrderRepository
from app.repositories.report_repository import ReportRepository
from app.schemas.report import (
    ReportAmendRequest,
    ReportDetailResponse,
    ReportRejectRequest,
    ReportReleaseRequest,
    ReportResponse,
    ReportSubmitRequest,
    ReportVerifierSummary,
    ReportVerifyRequest,
)
from app.services.audit_service import AuditService
from app.services.clinical_engine import ClinicalEngine, store_value
from app.services.pdf_service import PDFReportService


def _trim_decimal(val: Decimal | float | int | str | None) -> str | None:
    """Format decimal without scientific notation or redundant trailing zeroes."""
    if val is None:
        return None
    try:
        d = Decimal(str(val))
        return f"{d.normalize():f}" if "." in f"{d:f}" else f"{d:f}"
    except Exception:
        return str(val)


class ReportService:
    """Handles business logic for diagnostic reports and clinical sign-off workflows."""

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

    async def _audit_decision(
        self,
        db: AsyncSession,
        report: Report,
        action: str,
        description: str,
        user: User,
        severity: str = "INFO",
    ) -> None:
        """Immutable log of every review decision (submit/verify/reject/amend/release)."""
        lab_id = report.order.lab_id if report.order else user.lab_id
        await self.audit.record_event(
            db,
            action=action,
            description=description,
            category="CLINICAL",
            severity=severity,
            actor_name=user.name,
            actor_role=str(user.role.value if hasattr(user.role, "value") else user.role),
            actor_id=user.id,
            target_type="report",
            target_id=str(report.id),
            details={
                "order_id": str(report.order_id),
                "version": report.version,
                "status": report.status.value,
            },
            lab_id=lab_id,
        )

    def _build_verifier_summary(self, verifier: User | None) -> ReportVerifierSummary | None:
        if not verifier:
            return None
        return ReportVerifierSummary(
            id=verifier.id,
            name=verifier.name,
            role=str(verifier.role.value if hasattr(verifier.role, "value") else verifier.role),
        )

    @staticmethod
    def _report_modality(report: Report) -> str:
        """Stored discipline side of a report (pathology or imaging)."""
        modality = report.modality
        return modality.value if hasattr(modality, "value") else str(modality or "pathology")

    @staticmethod
    def _imaging_test_ids(report: Report) -> set[UUID]:
        """Non-cancelled imaging test ids on the report's order."""
        from app.models.test import CategoryModality as _Modality

        ids: set[UUID] = set()
        order = report.order
        if not order:
            return ids
        for ot in order.order_tests or []:
            if ot.status == OrderTestStatus.CANCELLED or not ot.test:
                continue
            category = getattr(ot.test, "category", None)
            modality = getattr(category, "modality", None) if category else None
            value = getattr(modality, "value", modality)
            if value == _Modality.IMAGING.value:
                ids.add(ot.test_id)
        return ids

    def _store_imaging_sections(self, report: Report, data: ReportVerifyRequest) -> dict[str, Any]:
        """Validate per-study coverage and persist narrative sections + composed notes."""
        sections = data.imaging_sections or []
        by_test = {s.test_id: s for s in sections}
        missing = self._imaging_test_ids(report) - set(by_test)
        if missing:
            raise ValidationException(
                message="Each ordered imaging study needs its own findings and impression."
            )
        stored: dict[str, dict[str, object]] = {}
        composed: list[str] = []
        order = report.order
        tests_by_id = {ot.test_id: ot.test for ot in (order.order_tests if order else []) if ot.test}
        for test_id in sorted(by_test, key=lambda t: str(t)):
            section = by_test[test_id]
            findings = section.findings.strip()
            impression = section.impression.strip()
            if not findings or not impression:
                raise ValidationException(
                    message="Each ordered imaging study needs its own findings and impression."
                )
            test = tests_by_id.get(test_id)
            params = sorted(getattr(test, "parameters", []) or [], key=lambda p: p.sort_order)
            views = [
                p.name
                for p in params
                if (p.name or "").strip().lower() != "impression" and not getattr(p, "deleted_at", None)
            ]
            stored[str(test_id)] = {
                "test_id": str(test_id),
                "test_name": test.name if test else "Imaging study",
                "test_code": test.code if test else "",
                "views": views,
                "findings": findings,
                "impression": impression,
            }
            label = test.code if test and test.code else "Imaging"
            composed.append(f"[{label}] {findings}")
        base = report.result_data if isinstance(report.result_data, dict) else {}
        payload = dict(base)
        payload["imaging_sections"] = stored
        if not (report.notes or "").strip():
            report.notes = "\n".join(composed)
        return payload

    def _build_response(self, report: Report) -> ReportResponse:
        order = report.order
        patient = order.patient if order else None
        return ReportResponse(
            id=report.id,
            order_id=report.order_id,
            order_number=order.order_number if order else "UNKNOWN",
            patient_id=patient.id if patient else (order.patient_id if order else report.id),
            patient_name=patient.name if patient else "Unknown",
            version=report.version,
            status=report.status.value,
            verified_by=self._build_verifier_summary(report.verifier),
            verification_reason=report.verification_reason,
            verified_at=report.verified_at,
            released_at=report.released_at,
            notes=report.notes,
            modality=self._report_modality(report),
            created_at=report.created_at,
            updated_at=report.updated_at,
        )

    def _build_detail_response(self, report: Report) -> ReportDetailResponse:
        base = self._build_response(report)
        return ReportDetailResponse(
            **base.model_dump(),
            previous_report_id=report.previous_report_id,
            file_url=report.file_url,
            result_data=report.result_data,
            public_token=report.public_token,
        )

    async def list_reports(
        self,
        db: AsyncSession,
        current_user: User,
        pagination: PaginationParams,
        status: ReportStatus | None = None,
        statuses: list[ReportStatus] | None = None,
        order_id: UUID | None = None,
        patient_id: UUID | None = None,
        activity_from: datetime | None = None,
        activity_to: datetime | None = None,
        critical_only: bool = False,
        modality: str | None = None,
    ) -> PaginatedResponse[ReportResponse]:
        """List reports with role-based branch scoping, status/modality filters, and pagination."""
        lab_id = None if current_user.is_superadmin else current_user.lab_id
        items = await self.report_repo.list(
            db=db,
            lab_id=lab_id,
            status=status,
            statuses=statuses,
            order_id=order_id,
            patient_id=patient_id,
            activity_from=activity_from,
            activity_to=activity_to,
            critical_only=critical_only,
            modality=modality,
            offset=pagination.offset,
            limit=pagination.limit,
        )
        total = await self.report_repo.count(
            db=db,
            lab_id=lab_id,
            status=status,
            statuses=statuses,
            order_id=order_id,
            patient_id=patient_id,
            activity_from=activity_from,
            activity_to=activity_to,
            critical_only=critical_only,
            modality=modality,
        )
        data = [self._build_response(r) for r in items]
        total_pages = math.ceil(total / pagination.limit) if total > 0 else 0
        meta = PaginationMeta(
            total=total,
            page=pagination.page,
            limit=pagination.limit,
            pages=total_pages,
        )
        return PaginatedResponse(data=data, meta=meta)

    async def get_report(
        self,
        db: AsyncSession,
        report_id: UUID,
        current_user: User,
    ) -> ReportDetailResponse:
        """Fetch full report details with branch isolation check."""
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        return self._build_detail_response(report)

    async def get_order_reports(
        self,
        db: AsyncSession,
        order_id: UUID,
        current_user: User,
        modality: str | None = None,
    ) -> list[ReportResponse]:
        """Fetch all versions of reports for a given visit order (optionally one side)."""
        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 order.lab_id != current_user.lab_id:
            raise NotFoundException(message=f"Order {order_id} not found in this branch")

        reports = await self.report_repo.get_all_versions_by_order_id(db, order_id, modality)
        return [self._build_response(r) for r in reports]

    async def submit_for_verification(
        self,
        db: AsyncSession,
        report_id: UUID,
        data: ReportSubmitRequest,
        current_user: User,
    ) -> ReportDetailResponse:
        """
        Submit a draft report for pathologist review and electronic sign-off.
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        if report.status not in (ReportStatus.DRAFT, ReportStatus.REJECTED):
            raise ValidationException(
                message=(
                    f"Cannot submit report with status '{report.status.value}'. "
                    "Must be 'draft' or 'rejected'."
                )
            )

        if not report.result_data or not isinstance(report.result_data, dict):
            raise ValidationException(message="Cannot submit report without entered test observations.")

        params = report.result_data.get("parameters", {})
        if not params or not isinstance(params, dict):
            raise ValidationException(message="Cannot submit report without entered test observations.")

        report.status = ReportStatus.PENDING_VERIFICATION
        submission_note = data.notes or ""
        if data.verifier_id:
            from app.models.user import User as UserModel

            verifier = await db.get(UserModel, data.verifier_id)
            if verifier:
                verifier_role = verifier.role.value if hasattr(verifier.role, "value") else verifier.role
                assigned_msg = f"Assigned to {verifier.name} ({verifier_role})"
                submission_note = f"{assigned_msg}. {submission_note}".strip()

        if submission_note:
            report.notes = (
                f"{report.notes}\nSubmission Note: {submission_note}"
                if report.notes
                else f"Submission Note: {submission_note}"
            )

        await self._audit_decision(
            db,
            report,
            "REPORT_SUBMITTED",
            (
                f"Order {report.order.order_number if report.order else report.order_id}"
                f" v{report.version} submitted for review"
            ),
            current_user,
        )
        await db.commit()
        await db.refresh(report)
        return self._build_detail_response(report)

    async def verify_report(
        self,
        db: AsyncSession,
        report_id: UUID,
        data: ReportVerifyRequest,
        current_user: User,
    ) -> ReportDetailResponse:
        """
        Authorize and electronically sign off on a diagnostic report.
        Advances order and order tests to COMPLETED.

        Only pathologists/doctors/radiologists may verify — superadmins and
        everyone else must forward to a pathologist instead. Enforced here,
        not just in UI.
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        actor_role = str(
            current_user.role.value if hasattr(current_user.role, "value") else current_user.role
        )
        allowed_verifiers = (
            UserRole.PATHOLOGIST.value,
            UserRole.DOCTOR.value,
            UserRole.RADIOLOGIST.value,
        )
        if actor_role not in allowed_verifiers:
            raise ForbiddenException(
                message=(
                    "Only a pathologist, doctor, or radiologist can verify reports."
                    " Please forward to a pathologist."
                )
            )

        if report.status in (ReportStatus.VERIFIED, ReportStatus.RELEASED):
            raise ValidationException(message=f"Report is already '{report.status.value}'.")

        # Imaging (narrative) reports carry per-study sections, not analytes.
        is_imaging = self._report_modality(report) == "imaging"
        if is_imaging:
            if not (data.verification_reason or "").strip():
                raise ValidationException(
                    message="Cannot verify an imaging report without a diagnostic impression."
                )
            report.result_data = self._store_imaging_sections(report, data)
        else:
            # Validate findings presence
            if not report.result_data or not isinstance(report.result_data, dict):
                raise ValidationException(message="Cannot verify report without clinical findings.")

            params = report.result_data.get("parameters", {})
            if not params or not isinstance(params, dict):
                raise ValidationException(message="Cannot verify report without clinical findings.")

        now = datetime.now(UTC)
        report.status = ReportStatus.VERIFIED
        report.verified_by = current_user.id
        report.verified_at = now
        report.verification_reason = data.verification_reason or "Clinically verified and authorized"

        if data.clinical_notes:
            report.notes = (
                f"{report.notes}\nClinical Note: {data.clinical_notes}"
                if report.notes
                else data.clinical_notes
            )

        # Cascade order status to COMPLETED
        if report.order:
            report.order.status = OrderStatus.COMPLETED
            for ot in report.order.order_tests:
                if ot.status != OrderTestStatus.CANCELLED:
                    ot.status = OrderTestStatus.VERIFIED

        await self._audit_decision(
            db,
            report,
            "REPORT_VERIFIED",
            (
                f"Order {report.order.order_number if report.order else report.order_id}"
                f" v{report.version} verified & signed: {report.verification_reason}"
            ),
            current_user,
        )
        await db.commit()
        await db.refresh(report)
        return self._build_detail_response(report)

    async def reject_report(
        self,
        db: AsyncSession,
        report_id: UUID,
        data: ReportRejectRequest,
        current_user: User,
    ) -> ReportDetailResponse:
        """
        Pathologist rejection of report draft with documented clinical rationale.
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        if report.status in (ReportStatus.VERIFIED, ReportStatus.RELEASED):
            raise ValidationException(
                message=(
                    f"Cannot reject a report that is already '{report.status.value}'. "
                    "Must create an amendment."
                )
            )

        now = datetime.now(UTC)
        report.status = ReportStatus.REJECTED
        report.verification_reason = data.reason
        report.verified_by = current_user.id
        report.verified_at = now

        # Per-analyte rework instructions ride on the stored observations so the
        # bench worksheet surfaces them on the same order after rejection.
        if data.parameter_remarks:
            stored = dict(report.result_data) if isinstance(report.result_data, dict) else {}
            raw_params = stored.get("parameters")
            params = dict(raw_params) if isinstance(raw_params, dict) else {}
            for remark in data.parameter_remarks:
                entry = dict(params.get(str(remark.parameter_id), {}))
                entry["pathologist_remark"] = remark.remark
                params[str(remark.parameter_id)] = entry
            stored["parameters"] = params
            report.result_data = stored

        # Keep order processing so technician can re-run/re-enter
        if report.order and report.order.status != OrderStatus.CANCELLED:
            report.order.status = OrderStatus.PROCESSING

        await self._audit_decision(
            db,
            report,
            "REPORT_REJECTED",
            (
                f"Order {report.order.order_number if report.order else report.order_id}"
                f" v{report.version} rejected: {data.reason}"
            ),
            current_user,
            severity="WARNING",
        )
        await db.commit()
        await db.refresh(report)
        return self._build_detail_response(report)

    async def amend_report(
        self,
        db: AsyncSession,
        report_id: UUID,
        data: ReportAmendRequest,
        current_user: User,
    ) -> ReportDetailResponse:
        """
        Create a new versioned amendment from a verified or released report.
        The original report remains completely immutable.
        """
        original = await self.report_repo.get_by_id(db, report_id)
        if not original:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        if original.status not in (ReportStatus.VERIFIED, ReportStatus.RELEASED):
            raise ValidationException(
                message=(
                    "Only verified or released reports can be amended. "
                    f"Current status is '{original.status.value}'."
                )
            )

        # Check that this is indeed the latest version on its own side
        latest = await self.report_repo.get_latest_by_order_id(
            db, original.order_id, self._report_modality(original)
        )
        if latest and latest.version > original.version:
            raise ValidationException(
                message=(
                    f"Version {original.version} has already been amended by version {latest.version}. "
                    f"Amend version {latest.version} instead."
                )
            )

        # Deep copy findings from original report
        new_result_data = deepcopy(original.result_data) if original.result_data else {}
        if isinstance(new_result_data, dict):
            new_result_data["amended_from_version"] = original.version
            new_result_data["amendment_reason"] = data.reason
            new_result_data["amended_at"] = datetime.now(UTC).isoformat()

        # Update specific parameter observations if provided
        if data.results and isinstance(new_result_data, dict):
            params_dict = new_result_data.get("parameters", {})
            if isinstance(params_dict, dict) and original.order:
                # Resolve parameters
                order = original.order
                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

                for entry in data.results:
                    # Look up parameter across order tests
                    matched_param = None
                    for ot in order.order_tests:
                        if ot.test:
                            for p in ot.test.parameters:
                                if p.id == entry.parameter_id:
                                    matched_param = p
                                    break
                        if matched_param:
                            break

                    if not matched_param:
                        raise ValidationException(
                            message=f"Parameter '{entry.parameter_id}' does not belong to this order."
                        )

                    ref_range = self.engine.select_reference_range(
                        matched_param.ranges,
                        patient_gender=patient_gender,
                        patient_age=patient_age,
                    )
                    flag, is_abnormal = self.engine.evaluate_value(entry.value, matched_param, ref_range)

                    params_dict[str(matched_param.id)] = {
                        "parameter_id": str(matched_param.id),
                        "parameter_name": matched_param.name,
                        "unit": matched_param.unit,
                        "value": store_value(entry.value),
                        "reference_low": (
                            _trim_decimal(ref_range.numeric_low)
                            if ref_range and ref_range.numeric_low is not None
                            else None
                        ),
                        "reference_high": (
                            _trim_decimal(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,
                        "amended_at": datetime.now(UTC).isoformat(),
                    }

        new_version = (latest.version if latest else original.version) + 1
        amended_report = Report(
            order_id=original.order_id,
            modality=original.modality,
            previous_report_id=original.id,
            version=new_version,
            status=ReportStatus.DRAFT,
            verification_reason=f"Amended: {data.reason}",
            result_data=new_result_data,
            notes=data.notes or original.notes,
        )
        amended_report = await self.report_repo.create(db, amended_report)

        # Order returns to PROCESSING during amendment review
        if original.order and original.order.status != OrderStatus.CANCELLED:
            original.order.status = OrderStatus.PROCESSING

        amended_report.order = original.order
        await self._audit_decision(
            db,
            amended_report,
            "REPORT_AMENDED",
            (
                f"Order {original.order.order_number if original.order else original.order_id}"
                f" v{amended_report.version} amended from v{original.version}: {data.reason}"
            ),
            current_user,
        )
        await db.commit()
        # Reload with relations
        reloaded = await self.report_repo.get_by_id(db, amended_report.id)
        final_report = reloaded or amended_report
        return self._build_detail_response(final_report)

    async def release_report(
        self,
        db: AsyncSession,
        report_id: UUID,
        data: ReportReleaseRequest,
        current_user: User,
    ) -> ReportDetailResponse:
        """
        Mark a verified report as released to the patient / ordering physician.
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        if report.status != ReportStatus.VERIFIED:
            raise ValidationException(
                message=(
                    "Report must be in 'verified' status before release. "
                    f"Current status is '{report.status.value}'."
                )
            )

        now = datetime.now(UTC)
        report.status = ReportStatus.RELEASED
        report.released_at = now
        if not report.public_token:
            report.public_token = secrets.token_urlsafe(32)
        if data.notes:
            report.notes = (
                f"{report.notes}\nRelease Note: {data.notes}"
                if report.notes
                else f"Release Note: {data.notes}"
            )

        if report.order:
            report.order.status = OrderStatus.COMPLETED
            for ot in report.order.order_tests:
                if ot.status == OrderTestStatus.VERIFIED:
                    ot.status = OrderTestStatus.RELEASED

        await self._audit_decision(
            db,
            report,
            "REPORT_RELEASED",
            (
                f"Order {report.order.order_number if report.order else report.order_id}"
                f" v{report.version} released to patient"
            ),
            current_user,
        )
        await db.commit()
        await db.refresh(report)

        # Trigger outbound patient notification (generates 72h client link & emails patient)
        try:
            from app.services.notification_service import NotificationService

            await NotificationService().send_report_released_notification(db, report)
        except Exception as exc:
            import logging

            logging.getLogger(__name__).warning(
                "Failed to auto-send release notification for report %s: %s", report.id, exc
            )

        # Accrue radiologist commission when the verifier is a commissioned radiologist
        try:
            from app.services.radiologist_commission_service import RadiologistCommissionService

            await RadiologistCommissionService().accrue_for_report(db, report.id)
        except Exception as exc:
            import logging

            logging.getLogger(__name__).warning(
                "Failed to accrue radiologist commission for report %s: %s", report.id, exc
            )

        return self._build_detail_response(report)

    async def render_report_pdf(
        self,
        db: AsyncSession,
        report_id: UUID,
        current_user: User,
    ) -> tuple[bytes, str]:
        """
        Generate dynamic binary PDF for a report.
        Returns tuple of (pdf_bytes, filename).
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        pdf_bytes = await asyncio.to_thread(
            self.pdf_service.render_pdf, report, self._report_modality(report)
        )
        order_num = report.order.order_number if report.order else "report"
        filename = f"report_{order_num}_v{report.version}.pdf"
        return pdf_bytes, filename

    async def render_report_html(
        self,
        db: AsyncSession,
        report_id: UUID,
        current_user: User,
    ) -> str:
        """
        Generate dynamic HTML preview for a report.
        """
        report = await self.report_repo.get_by_id(db, report_id)
        if not report:
            raise NotFoundException(message=f"Report {report_id} not found")

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

        return self.pdf_service.render_html(report, self._report_modality(report))
