"""保单服务申请与多模态材料识别。

用户从自己的有效保单发起服务，上传的图片先经过格式校验和标准化，再交给
百炼多模态模型提取字段。模型结果必须由用户确认后才能生成正式服务申请；
识别可信度和原始图片只在管理端审核视图中展示。
"""

from collections.abc import Callable
from dataclasses import replace
from datetime import datetime
from typing import Any

from zbt.core.errors import AppError
from zbt.core.identifiers import new_ulid
from zbt.domains.enrollment.repository import EnrollmentRepository
from zbt.domains.identity.models import H5User
from zbt.domains.service_request.models import MaterialRecognition, ServiceRequest
from zbt.domains.service_request.preprocessing import (
    MaterialPreprocessor,
    PassthroughMaterialPreprocessor,
)
from zbt.domains.service_request.repository import ServiceRequestRepository
from zbt.domains.service_request.storage import (
    InMemoryMaterialStorage,
    MaterialStorage,
    StoredMaterial,
)
from zbt.domains.service_request.vision import MaterialVisionGateway


class ServiceRequestService:
    """协调保单校验、图片处理、模型识别、用户确认和运营审核。"""

    def __init__(
        self,
        repository: ServiceRequestRepository,
        enrollment_repository: EnrollmentRepository,
        vision_gateway: MaterialVisionGateway,
        clock: Callable[[], datetime],
        material_storage: MaterialStorage | None = None,
        material_preprocessor: MaterialPreprocessor | None = None,
    ) -> None:
        self._repository = repository
        self._enrollments = enrollment_repository
        self._vision = vision_gateway
        self._clock = clock
        self._materials = material_storage or InMemoryMaterialStorage()
        self._preprocessor = (
            material_preprocessor or PassthroughMaterialPreprocessor()
        )

    def recognize_material(
        self,
        user: H5User,
        *,
        policy_id: str,
        material_type: str,
        source_name: str,
        media_type: str,
        content: bytes,
    ) -> dict[str, Any]:
        """识别一张属于当前用户有效保单的服务材料。

        返回的是“待确认结果”，并不直接创建服务申请，避免模型识别错误未经
        用户核对就进入后台业务流程。
        """

        self._require_user_policy(user.id, policy_id)
        if media_type not in {"image/jpeg", "image/png", "image/webp"}:
            raise AppError("MATERIAL_TYPE_UNSUPPORTED", "仅支持JPG、PNG和WebP图片", 422)
        if not content:
            raise AppError("MATERIAL_EMPTY", "上传材料不能为空", 422)
        # 预处理器会验证图片真实性、尺寸和格式，并输出适合模型读取的标准图片。
        processed = self._preprocessor.process(
            content=content,
            media_type=media_type,
        )
        extraction = self._vision.recognize(
            content=processed.content,
            media_type=processed.media_type,
            material_type=material_type,
        )
        recognition_id = new_ulid()
        recognition = MaterialRecognition(
            id=recognition_id,
            user_id=user.id,
            policy_id=policy_id,
            material_type=material_type,
            source_name=source_name,
            media_type=processed.media_type,
            fields=dict(extraction.fields),
            confidence=extraction.confidence,
            warnings=extraction.warnings,
            status="PENDING_CONFIRMATION",
            created_at=self._clock(),
        )
        # 原图/标准图保存在材料存储中，结构化识别结果保存在业务数据库中。
        self._materials.save(recognition_id, processed.content)
        try:
            self._repository.save_recognition(recognition)
        except Exception:
            self._materials.delete(recognition_id)
            raise
        return self._recognition_data(recognition)

    def submit_request(
        self,
        user: H5User,
        *,
        policy_id: str,
        request_type: str,
        recognition_id: str,
        confirmed_fields: dict[str, str],
    ) -> dict[str, Any]:
        """使用用户核对后的字段创建正式服务申请。"""

        self._require_user_policy(user.id, policy_id)
        recognition = self._repository.get_recognition(recognition_id)
        if (
            recognition is None
            or recognition.user_id != user.id
            or recognition.policy_id != policy_id
        ):
            raise AppError("MATERIAL_RECOGNITION_NOT_FOUND", "未找到材料识别结果", 404)
        if recognition.status != "PENDING_CONFIRMATION":
            raise AppError("MATERIAL_ALREADY_CONFIRMED", "材料识别结果已确认", 409)
        required = {"invoice_no", "hospital_name", "amount_yuan", "contact_mobile"}
        missing = sorted(
            field for field in required if not confirmed_fields.get(field, "").strip()
        )
        if missing:
            raise AppError(
                "SERVICE_REQUEST_FIELDS_INCOMPLETE",
                "服务申请信息不完整",
                422,
                details={"missing_fields": missing},
            )
        now = self._clock()
        self._repository.save_recognition(
            replace(recognition, status="CONFIRMED", confirmed_at=now)
        )
        request = ServiceRequest(
            id=new_ulid(),
            request_no=f"ZBT-SVC-{now:%Y%m%d}-{new_ulid()[-4:]}",
            user_id=user.id,
            policy_id=policy_id,
            request_type=request_type,
            recognition_id=recognition_id,
            confirmed_fields={
                key: str(value).strip() for key, value in confirmed_fields.items()
            },
            status="SUBMITTED",
            submitted_at=now,
        )
        self._repository.save_request(request)
        return self._request_data(request)

    def list_user_requests(self, user: H5User) -> dict[str, Any]:
        return self._list_requests(user.id)

    def list_all_requests(self) -> dict[str, Any]:
        return self._list_requests(include_audit=True)

    def get_request_material(self, request_id: str) -> tuple[StoredMaterial, str, str]:
        """读取申请对应的材料，供有权限的管理端审核人员查看。"""

        request = self._repository.get_request(request_id)
        if request is None:
            raise AppError("SERVICE_REQUEST_NOT_FOUND", "未找到服务申请", 404)
        recognition = self._repository.get_recognition(request.recognition_id)
        if recognition is None:
            raise AppError("MATERIAL_RECOGNITION_NOT_FOUND", "未找到材料识别记录", 404)
        material = self._materials.load(recognition.id)
        if material is None:
            raise AppError("SERVICE_MATERIAL_NOT_FOUND", "该申请未留存原始材料", 404)
        return material, recognition.media_type, recognition.source_name

    def review_request(
        self,
        request_id: str,
        *,
        decision: str,
        reviewer_id: str,
    ) -> dict[str, Any]:
        """把待处理申请更新为受理或不受理；已处理申请不能重复审核。"""

        request = self._repository.get_request(request_id)
        if request is None:
            raise AppError("SERVICE_REQUEST_NOT_FOUND", "未找到服务申请", 404)
        if request.status != "SUBMITTED":
            raise AppError("SERVICE_REQUEST_ALREADY_REVIEWED", "服务申请已完成审核", 409)
        if decision not in {"ACCEPTED", "REJECTED"}:
            raise AppError("SERVICE_REQUEST_DECISION_INVALID", "审核结论无效", 422)
        reviewed = replace(
            request,
            status=decision,
            reviewed_by=reviewer_id,
            review_note=None if decision == "ACCEPTED" else "不满足赔付条件",
            reviewed_at=self._clock(),
        )
        self._repository.save_request(reviewed)
        return self._request_data(reviewed, include_audit=True)

    def _list_requests(
        self,
        user_id: str | None = None,
        *,
        include_audit: bool = False,
    ) -> dict[str, Any]:
        requests = sorted(
            self._repository.list_requests(user_id),
            key=lambda item: item.submitted_at,
            reverse=True,
        )
        return {
            "items": [
                self._request_data(request, include_audit=include_audit)
                for request in requests
            ],
            "total": len(requests),
        }

    def _require_user_policy(self, user_id: str, policy_id: str) -> None:
        policy = next(
            (
                item
                for item in self._enrollments.list_policies(user_id)
                if item.id == policy_id and item.status == "ACTIVE"
            ),
            None,
        )
        if policy is None:
            raise AppError("POLICY_NOT_FOUND", "未找到可申请服务的有效保单", 404)

    @staticmethod
    def _recognition_data(recognition: MaterialRecognition) -> dict[str, Any]:
        return {
            "recognition_id": recognition.id,
            "policy_id": recognition.policy_id,
            "material_type": recognition.material_type,
            "source_name": recognition.source_name,
            "fields": recognition.fields,
            "warnings": list(recognition.warnings),
            "status": recognition.status,
            "created_at": recognition.created_at,
            "confirmed_at": recognition.confirmed_at,
        }

    def _request_data(
        self,
        request: ServiceRequest,
        *,
        include_audit: bool = False,
    ) -> dict[str, Any]:
        data: dict[str, Any] = {
            "request_id": request.id,
            "request_no": request.request_no,
            "user_id": request.user_id,
            "policy_id": request.policy_id,
            "request_type": request.request_type,
            "recognition_id": request.recognition_id,
            "confirmed_fields": request.confirmed_fields,
            "status": request.status,
            "submitted_at": request.submitted_at,
            "reviewed_by": request.reviewed_by,
            "review_note": request.review_note,
            "reviewed_at": request.reviewed_at,
        }
        if include_audit:
            recognition = self._repository.get_recognition(request.recognition_id)
            data["recognition"] = (
                {
                    "source_name": recognition.source_name,
                    "media_type": recognition.media_type,
                    "confidence": recognition.confidence,
                    "warnings": list(recognition.warnings),
                    "material_available": self._materials.exists(recognition.id),
                }
                if recognition is not None
                else None
            )
        return data
