"""
Map a 1Rx consultation result to Amazon's ConsumeConsultation v2 payload.

This is the body POSTed to the Shuttle API. It differs from the legacy SFTP
response (json_mapper.rx_to_amazon_response) in shape and strictness:
messageIdentifiers/data wrapper, a per-delivery trackingId, providerName,
prescriber details are mandatory, and the prescription travels as a ZIP
behind a pre-signed URL instead of a JPG inside the PGP bundle.

Amazon rejects payloads missing MANDATORY fields with status ERRORED, so
this mapper raises ConsumeMappingError instead of inventing defaults.
"""

import logging
import time
from typing import Any, Dict, List, Optional

from .json_mapper import JSONMapper

logger = logging.getLogger(__name__)

MESSAGE_TYPE = "CONSULTATION_COMPLETED"

# consultationResponse.failureReason allowed values (spec §4.1 3c)
FAILURE_REASONS = (
    "USER_NOT_REACHABLE",
    "USER_NOT_ANSWERING_CALL",
    "NETWORK_CONNECTIVITY_ISSUES",
    "INCORRECT_NUMBER",
    "USER_ASKED_TO_CALL_LATER",
    "UNABLE_TO_UNDERSTAND_USER",
    "USER_ASKED_TO_CANCEL_ORDER",
)
DEFAULT_FAILURE_REASON = "USER_NOT_REACHABLE"

# Substring match on the 1Rx orderCancelReason, first hit wins.
FAILURE_REASON_MAPPING = {
    "call not answered": "USER_NOT_ANSWERING_CALL",
    "not answering": "USER_NOT_ANSWERING_CALL",
    "call later": "USER_ASKED_TO_CALL_LATER",
    "out of network": "NETWORK_CONNECTIVITY_ISSUES",
    "network": "NETWORK_CONNECTIVITY_ISSUES",
    "language barrier": "UNABLE_TO_UNDERSTAND_USER",
    "cancel the order": "USER_ASKED_TO_CANCEL_ORDER",
    "incorrect number": "INCORRECT_NUMBER",
    "wrong number": "INCORRECT_NUMBER",
    "not reachable": "USER_NOT_REACHABLE",
}


class ConsumeMappingError(Exception):
    """A MANDATORY field for the Amazon payload could not be populated."""

    def __init__(self, field: str, detail: str = ""):
        self.field = field
        message = f"Cannot build Amazon consultation response: missing {field}"
        if detail:
            message = f"{message} ({detail})"
        super().__init__(message)


def rx_to_amazon_consume_response(
    rx_response: Dict[str, Any],
    original_request: Dict[str, Any],
    *,
    tracking_id: str,
    provider_name: str,
    file_url: Optional[str] = None,
    file_name: Optional[str] = None,
    call_attempt_number: int = 1,
    sent_time_millis: Optional[int] = None,
) -> Dict[str, Any]:
    """Build the ConsumeConsultation request body.

    file_url is the pre-signed URL of the prescription ZIP; it is required
    whenever 1Rx approved the order because Amazon mandates it on SUCCESS.
    """
    if not tracking_id:
        raise ConsumeMappingError("messageIdentifiers.trackingId")
    if not provider_name:
        raise ConsumeMappingError("messageIdentifiers.providerName")

    now_millis = sent_time_millis or int(time.time() * 1000)
    ids = _extract_identifiers(original_request)
    message_identifiers = {
        "primaryReferenceId": ids["primary_reference_id"],
        "trackingId": tracking_id,
        "providerName": provider_name,
    }
    if ids["secondary_reference_id"]:
        message_identifiers["secondaryReferenceId"] = ids["secondary_reference_id"]
    if ids["is_test_data"] is not None:
        message_identifiers["isTestData"] = ids["is_test_data"]

    rx_status = (rx_response.get("status") or "").upper()
    cancel_reason = rx_response.get("orderCancelReason") or ""

    if rx_status in ("APPROVED", "REJECTED"):
        consultation_response = {
            "status": "SUCCESS",
            "customerCallAttemptNumber": call_attempt_number,
        }
    else:
        consultation_response = {
            "status": "FAILED",
            "customerCallAttemptNumber": call_attempt_number,
            "failureReason": _map_failure_reason(cancel_reason),
        }

    data = {
        "responseMetadata": {
            "messageSentTime": now_millis,
            "messageType": MESSAGE_TYPE,
        },
        "consultationResponse": consultation_response,
    }

    if consultation_response["status"] == "SUCCESS":
        if rx_status == "APPROVED" and not file_url:
            raise ConsumeMappingError(
                "rxImageDetails.fileUrl", "pre-signed prescription URL required on SUCCESS"
            )
        data["rxDetailFromConsultation"] = [
            _build_rx_detail(
                rx_response,
                ids,
                rx_status=rx_status,
                cancel_reason=cancel_reason,
                issue_time_millis=rx_response.get("approveTime") or now_millis,
                file_url=file_url,
                file_name=file_name,
            )
        ]

    return {"messageIdentifiers": message_identifiers, "data": data}


def _extract_identifiers(original_request: Dict[str, Any]) -> Dict[str, Any]:
    """Pull the identifiers Amazon expects echoed back, from either the API
    (messageIdentifiers/data) or legacy SFTP (flat requestMetadata) request."""
    if "messageIdentifiers" in original_request:
        message_ids = original_request.get("messageIdentifiers") or {}
        data = original_request.get("data") or {}
        primary = message_ids.get("primaryReferenceId")
        secondary = message_ids.get("secondaryReferenceId")
        is_test_data = message_ids.get("isTestData")
        rx_details = data.get("rxDetailForConsultation") or []
        patient_name = _first_patient_name(rx_details)
    else:
        request_metadata = original_request.get("requestMetadata") or {}
        primary = request_metadata.get("referenceId")
        secondary = request_metadata.get("consultationId")
        is_test_data = None
        rx_details = original_request.get("rxDetailForConsultation") or []
        patient_name = _first_patient_name(rx_details)

    if not primary:
        raise ConsumeMappingError("messageIdentifiers.primaryReferenceId")

    rx_ids = [
        header.get("rxId")
        for rx in rx_details
        for header in [rx.get("rxHeaderDetailsForConsultation") or {}]
        if header.get("rxId")
    ]
    ordered_medicine_urls = {
        item.get("rxItemId"): item.get("orderedMedicine")
        for rx in rx_details
        for item in rx.get("rxItemDetailForConsultation") or []
        if item.get("rxItemId")
    }

    return {
        "primary_reference_id": primary,
        "secondary_reference_id": secondary,
        "is_test_data": is_test_data,
        "rx_ids": rx_ids,
        "ordered_medicine_urls": ordered_medicine_urls,
        "request_patient_name": patient_name,
    }


def _first_patient_name(rx_details: List[Dict[str, Any]]) -> Optional[str]:
    for rx in rx_details:
        header = rx.get("rxHeaderDetailsForConsultation") or {}
        name = (header.get("patientContactDetails") or {}).get("name")
        if name:
            return name
    return None


def _build_rx_detail(
    rx_response: Dict[str, Any],
    ids: Dict[str, Any],
    *,
    rx_status: str,
    cancel_reason: str,
    issue_time_millis: int,
    file_url: Optional[str],
    file_name: Optional[str],
) -> Dict[str, Any]:
    if not ids["rx_ids"]:
        raise ConsumeMappingError(
            "rxDetailFromConsultation.rxIds", "no rxId in the original request"
        )

    doctor = rx_response.get("doctor") or {}
    prescriber = {
        "name": _require(doctor.get("name"), "prescriberDetails.name"),
        "contactNumber": _require(doctor.get("phone"), "prescriberDetails.contactNumber"),
        "registrationNumber": _require(
            doctor.get("registrationNumber"), "prescriberDetails.registrationNumber"
        ),
        "address": _require(doctor.get("address"), "prescriberDetails.address"),
    }

    patient = rx_response.get("patient") or {}
    patient_details = {
        "name": _require(
            patient.get("name") or ids["request_patient_name"], "patientDetails.name"
        )
    }
    if patient.get("gender"):
        patient_details["gender"] = patient["gender"]
    if patient.get("age") is not None:
        patient_details["age"] = patient["age"]

    rx_items = _build_rx_items(
        rx_response,
        ids["ordered_medicine_urls"],
        rx_status=rx_status,
        cancel_reason=cancel_reason,
        issue_time_millis=issue_time_millis,
    )

    rx_images = []
    if file_url:
        rx_images.append(
            {
                # Name of the image inside the ZIP, never the ZIP itself.
                "fileName": file_name or f"{ids['secondary_reference_id'] or ids['primary_reference_id']}.jpg",
                "formatType": "ZIP",
                "rxIndex": "1",
                "fileUrl": file_url,
            }
        )

    return {
        "rxIds": ids["rx_ids"],
        "rxHeaderDetailsFromConsultation": {
            "rxIssueDate": issue_time_millis,
            "prescriberDetails": prescriber,
            "patientDetails": patient_details,
        },
        "rxItemDetailFromConsultation": rx_items,
        "rxImageDetails": rx_images,
    }


def _build_rx_items(
    rx_response: Dict[str, Any],
    ordered_medicine_urls: Dict[str, str],
    *,
    rx_status: str,
    cancel_reason: str,
    issue_time_millis: int,
) -> List[Dict[str, Any]]:
    uuid_to_url = {
        med.get("uuid"): med.get("url")
        for med in rx_response.get("orderedMedicines") or []
        if med.get("uuid")
    }

    approved = rx_response.get("approvedMedicines") or []
    rejected = rx_response.get("rejectedMedicines") or []

    # A whole-order rejection may arrive without per-medicine entries; every
    # ordered medicine is then rejected for the order-level reason.
    if rx_status == "REJECTED" and not approved and not rejected:
        rejected = [
            {**med, "rejectionReason": cancel_reason}
            for med in rx_response.get("orderedMedicines") or []
        ]

    items = []
    for medicine in approved:
        items.append(
            _build_rx_item(
                medicine,
                uuid_to_url,
                ordered_medicine_urls,
                issue_time_millis=issue_time_millis,
                item_status="APPROVED",
                rejection_reasons=[],
            )
        )
    for medicine in rejected:
        reason = _map_medicine_rejection_reason(
            medicine.get("rejectionReason") or cancel_reason
        )
        items.append(
            _build_rx_item(
                medicine,
                uuid_to_url,
                ordered_medicine_urls,
                issue_time_millis=issue_time_millis,
                item_status="REJECTED",
                rejection_reasons=[reason],
            )
        )

    if not items:
        raise ConsumeMappingError(
            "rxItemDetailFromConsultation", "1Rx response has no medicines"
        )
    return items


def _build_rx_item(
    medicine: Dict[str, Any],
    uuid_to_url: Dict[str, str],
    ordered_medicine_urls: Dict[str, str],
    *,
    issue_time_millis: int,
    item_status: str,
    rejection_reasons: List[str],
) -> Dict[str, Any]:
    rx_item_id = _require(medicine.get("id"), "rxItemDetailFromConsultation.rxItemId")
    ordered_medicine = (
        uuid_to_url.get(medicine.get("uuid"))
        or medicine.get("url")
        or ordered_medicine_urls.get(rx_item_id)
    )
    _require(ordered_medicine, f"rxItemDetailFromConsultation[{rx_item_id}].orderedMedicine")

    quantity = medicine.get("quantity")
    if quantity is None:
        raise ConsumeMappingError(
            f"rxItemDetailFromConsultation[{rx_item_id}].totalPrescribedQty"
        )

    dosage_str = medicine.get("dosage") or "1-0-0"
    item = {
        "rxItemId": rx_item_id,
        "rxItemIssueDate": issue_time_millis,
        "orderedMedicine": ordered_medicine,
        "totalPrescribedQty": int(quantity),
        "dosage": {
            "dose": JSONMapper._extract_dose_from_dosage(dosage_str),
            "form": medicine.get("type") or "TABLETS",
            "frequency": JSONMapper._extract_frequency_from_dosage(dosage_str),
            "frequencyUnit": "DAILY",
            "duration": str(medicine.get("days") or 1),
            "durationUnit": "DAYS",
        },
        "itemStatus": item_status,
        "isEligibleForFutureConsultations": _is_eligible_for_future(rejection_reasons),
    }
    if medicine.get("name"):
        item["medicineName"] = medicine["name"]
    if medicine.get("comments"):
        item["instructions"] = medicine["comments"]
    if rejection_reasons:
        item["rejectionReason"] = rejection_reasons
    return item


def _require(value: Any, field: str) -> Any:
    if value is None or (isinstance(value, str) and not value.strip()):
        raise ConsumeMappingError(field)
    return value


def _map_failure_reason(reason: Optional[str]) -> str:
    lowered = (reason or "").lower()
    for needle, amazon_reason in FAILURE_REASON_MAPPING.items():
        if needle in lowered:
            return amazon_reason
    if reason:
        logger.warning(
            "No Amazon failureReason mapping for 1Rx cancel reason %r; using %s",
            reason,
            DEFAULT_FAILURE_REASON,
        )
    return DEFAULT_FAILURE_REASON


def _map_medicine_rejection_reason(reason: Optional[str]) -> str:
    lowered = (reason or "").lower()
    for rx_reason, amazon_reason in JSONMapper.MEDICINE_REJECTION_ERROR_MAPPING.items():
        if rx_reason.lower() in lowered:
            return amazon_reason
    return "OTHERS"


def _is_eligible_for_future(rejection_reasons: List[str]) -> bool:
    return not any(
        reason in JSONMapper.NO_FUTURE_CONSULTATION_REASONS for reason in rejection_reasons
    )
