"""``/api/eligibility/request`` and ``/api/eligibility/parse-271`` — API-only eligibility pair.

Builds a 270 inquiry from a small JSON body and parses a 271 response.
Nothing is persisted to the DB — these are operator-driven, ephemeral
operations per SP3 (P4 T23–T24). The 270 serializer pulls X12 from a
``ParseResult270`` Pydantic; the 271 parser builds the same structure
in reverse from the wire format.

Why these are not ``GET /api/eligibility/...``: the 270 build is
operator-initiated (pay-portal paste-back), so the inbound surface is
a JSON ``POST``. The 271 inbound is a multipart file upload — same
shape as ``/api/parse-999`` — so the file can be the actual 271 text
saved from the payer portal.

SP36 Task 5: this block moved here from ``api.py:2832`` (``270 / 271
eligibility`` divider).
"""
from __future__ import annotations

import json
import logging
from datetime import date as _date
from typing import Any

from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import JSONResponse

from cyclone.auth.deps import matrix_gate
from cyclone.parsers.exceptions import CycloneParseError
from cyclone.parsers.models import BatchSummary, Envelope
from cyclone.parsers.models_270 import (
    EligibilityBenefitInquiry,
    InformationReceiver270,
    InformationSource270,
    ParseResult270,
    Subscriber270,
)
from cyclone.parsers.parse_271 import parse as parse_271_text
from cyclone.parsers.serialize_270 import serialize_270

log = logging.getLogger(__name__)

router = APIRouter(dependencies=[Depends(matrix_gate)])


def _validate_eligibility_request(body: dict) -> tuple[ParseResult270, str]:
    """Build a :class:`ParseResult270` from a request body dict.

    The body shape is the minimum surface needed to build a valid 270
    inquiry (per spec section 3.4 — operator-driven, ephemeral):

    ::

        {
            "subscriber": {first_name, last_name, member_id, dob},
            "provider":   {npi, name},
            "payer":      {id, name},
            "service_type_code": "1"
        }

    Returns ``(ParseResult270, service_type_code)``. Raises
    :class:`HTTPException` (400) when the body is missing required
    fields.
    """
    subscriber_in = body.get("subscriber") or {}
    provider_in = body.get("provider") or {}
    payer_in = body.get("payer") or {}
    service_type_code = (body.get("service_type_code") or "").strip()

    # Required-field checks. We surface a single 400 with the first
    # missing field name to match the rest of the API's error contract.
    if not service_type_code:
        raise HTTPException(
            status_code=400,
            detail={"error": "Bad request", "detail": "service_type_code is required"},
        )
    if not subscriber_in.get("member_id"):
        raise HTTPException(
            status_code=400,
            detail={"error": "Bad request", "detail": "subscriber.member_id is required"},
        )
    if not provider_in.get("npi"):
        raise HTTPException(
            status_code=400,
            detail={"error": "Bad request", "detail": "provider.npi is required"},
        )
    if not payer_in.get("name"):
        raise HTTPException(
            status_code=400,
            detail={"error": "Bad request", "detail": "payer.name is required"},
        )

    # Build the Pydantic models. The serializer handles all envelope
    # generation (sender_id/receiver_id/control_number/transaction_date
    # are filled in by the serializer with sensible defaults).
    subscriber_dob_raw = subscriber_in.get("dob")
    subscriber_dob: _date | None = None
    if subscriber_dob_raw:
        try:
            subscriber_dob = _date.fromisoformat(subscriber_dob_raw)
        except (TypeError, ValueError) as exc:
            raise HTTPException(
                status_code=400,
                detail={
                    "error": "Bad request",
                    "detail": f"subscriber.dob must be YYYY-MM-DD: {exc}",
                },
            ) from exc

    result = ParseResult270(
        envelope=Envelope(
            sender_id="SUBMITTERID",
            receiver_id=str(payer_in.get("id") or "RECEIVERID"),
            control_number="000000001",
            transaction_date=_date.today(),
            implementation_guide="005010X279A1",
        ),
        information_source=InformationSource270(
            name=str(payer_in["name"]),
            id=str(payer_in.get("id") or "") or None,
        ),
        information_receiver=InformationReceiver270(
            name=str(provider_in.get("name") or ""),
            npi=str(provider_in["npi"]),
        ),
        subscriber=Subscriber270(
            member_id=str(subscriber_in["member_id"]),
            first_name=str(subscriber_in.get("first_name") or "") or None,
            last_name=str(subscriber_in.get("last_name") or "") or None,
            dob=subscriber_dob,
        ),
        inquiries=[EligibilityBenefitInquiry(service_type_code=service_type_code)],
        summary=BatchSummary(
            input_file="eligibility_request",
            control_number="000000001",
            transaction_date=_date.today(),
            total_claims=1,
            passed=1,
            failed=0,
        ),
    )
    return result, service_type_code


@router.post("/api/eligibility/request")
def post_eligibility_request(body: dict) -> Any:
    """Build a 270 eligibility inquiry from a small JSON body.

    Returns ``{"raw_270_text": <X12>, "parsed": <ParseResult270>}``
    so the operator can either download the raw text (paste into a
    payer portal) or render the parsed fields directly. Per spec
    section 3.4, nothing is persisted to the DB.
    """
    try:
        result, _ = _validate_eligibility_request(body)
    except HTTPException:
        raise
    except (KeyError, TypeError, ValueError) as exc:
        raise HTTPException(
            status_code=400,
            detail={"error": "Bad request", "detail": f"Malformed body: {exc}"},
        ) from exc

    raw_270_text = serialize_270(result)
    return {
        "raw_270_text": raw_270_text,
        "parsed": json.loads(result.model_dump_json()),
    }


@router.post("/api/eligibility/parse-271")
async def post_eligibility_parse_271(
    file: UploadFile = File(...),
) -> Any:
    """Parse a 271 eligibility response and return the structured summary.

    Accepts the raw 271 text as a file upload (multipart/form-data),
    mirrors the ``/api/parse-999`` contract. Per spec section 3.4 the
    result is NOT persisted — the operator re-pastes the 271 each
    time they need a fresh read.

    The response body is a JSON object with three top-level keys:
    ``coverage_benefits``, ``subscriber``, and ``summary``. 400 is
    returned on empty / undecodable / malformed EDI; 200 on success.
    """
    raw = await file.read()
    if not raw:
        return JSONResponse(
            status_code=400,
            content={"error": "Empty file", "detail": "Uploaded file contained no bytes."},
        )
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError as exc:
        return JSONResponse(
            status_code=400,
            content={"error": "Encoding error", "detail": str(exc)},
        )

    try:
        result = parse_271_text(text, input_file=file.filename or "")
    except CycloneParseError as exc:
        return JSONResponse(
            status_code=400,
            content={"error": "Parse error", "detail": str(exc)},
        )
    except Exception as exc:  # pragma: no cover - safety net
        log.exception("Unexpected parser failure on 271")
        return JSONResponse(
            status_code=500,
            content={"error": "Internal server error", "detail": str(exc)},
        )

    return {
        "coverage_benefits": [
            json.loads(cb.model_dump_json()) for cb in result.coverage_benefits
        ],
        "subscriber": json.loads(result.subscriber.model_dump_json()),
        "summary": json.loads(result.summary.model_dump_json()),
        "envelope": json.loads(result.envelope.model_dump_json()),
        "information_source": json.loads(result.information_source.model_dump_json()),
        "information_receiver": json.loads(result.information_receiver.model_dump_json()),
    }
