"""Tank Data Minion — one job: fetch tank cert data + certificate document
from ERP's own REST APIs (called IN PARALLEL), then either continue the
allocate pipeline on success or report failure back to ERP.

Consumes: fastlane.tank.data.requested   (raw TankAllocationCommand)
Produces (success): fastlane.tank.allocate.requested
Produces (failure): fastlane.tank.allocate.failed
DLQ    : fastlane.tank.data.dlq
"""
from __future__ import annotations

import asyncio
from datetime import datetime, timezone

from app.config import get_settings
from app.logger import get_logger
from app.schemas import AllocationStatus, TankAllocationCommand, TankAllocationStatusEvent
from app.services.tank_data_client import (
    TankDataIncompleteError,
    build_allocation_event,
    fetch_tank_data,
    fetch_tank_document,
)
from minions._runner import emit, envelope_key, run_minion

log = get_logger("tank-data-minion")


def _reason(audit: dict) -> str:
    if (audit.get("response") or {}).get("timeout"):
        return "tank_data_timeout"
    return "no_tank_data"


def _build_tank_data_log(
    cmd: TankAllocationCommand,
    data_audit: dict,
    doc_audit: dict,
    *,
    ok: bool,
    message: str | None = None,
) -> dict:
    """Shape an ERP audit entry for the two ERP API calls made by this stage.

    This stage runs BEFORE anything is sent to BASF, so it produces no
    FastLane exchanges of its own. Previously that meant a tank rejected
    here (e.g. missing next_tank_inspection) produced NO audit entry at all
    and the ERP's Log viewer appeared empty — indistinguishable from a bug.

    The entry is shaped exactly like the ones FastlaneClient produces
    (`exchanges` = ordered list of {method, url, request, response, ok}) so
    the ERP renders it with the same viewer, no special-casing needed. The
    tank-document audit deliberately carries no PDF bytes (omitted upstream
    in fetch_tank_document) to keep the stored log small.
    """
    def _exchange(name: str, audit: dict) -> dict:
        req = audit.get("request") or {}
        resp = audit.get("response")
        return {
            "method": name,
            "url": req.get("url"),
            "request": req,
            "response": resp,
            "ok": bool(resp) and not (resp or {}).get("error"),
        }

    return {
        "operation": "ERP_TANK_DATA",
        "operation_label": "ERP Tank Data Lookup",
        "occurred_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "job_id": cmd.job_id,
        "tank_id": cmd.tank_id,
        "licence_number": cmd.licence_number,
        "fastlane_asset_id": None,
        "ok": ok,
        "message": message,
        "exchanges": [
            _exchange("ERP getTankData", data_audit),
            _exchange("ERP getTankDocument", doc_audit),
        ],
    }


async def _publish_failure(
    cmd: TankAllocationCommand,
    reason: str,
    audit: dict,
    tank_data_log: dict | None = None,
) -> None:
    s = get_settings()
    raw = dict(audit)
    if tank_data_log is not None:
        # `fastlane_log` is the key the ERP callback controller reads to
        # append an audit row, so the pre-BASF failure now shows up in the
        # dashboard's Log viewer instead of silently recording nothing.
        raw["fastlane_log"] = tank_data_log
    event = TankAllocationStatusEvent(
        event_type=s.topic_allocate_failed,
        status=AllocationStatus.FAILED,
        job_id=cmd.job_id,
        tank_id=cmd.tank_id,
        licence_number=cmd.licence_number,
        message=reason,
        raw=raw,
    )
    value = event.model_dump(mode="json")
    await emit(s.topic_allocate_failed, envelope_key(value), value)


async def handle(event: dict) -> None:
    cmd = TankAllocationCommand.model_validate(event)
    s = get_settings()

    # Call both ERP APIs at the same time — the data-fetch alone can take
    # 30-90s, so waiting for them one after another would double the delay.
    (tank_data, data_audit), (doc, doc_audit) = await asyncio.gather(
        fetch_tank_data(cmd.licence_number),
        fetch_tank_document(cmd.licence_number),
    )

    if not tank_data:
        reason = _reason(data_audit)
        log.warning("No tank data for plate=%s reason=%s", cmd.licence_number, reason)
        await _publish_failure(
            cmd, reason,
            {"tank_data": data_audit, "tank_document": doc_audit},
            _build_tank_data_log(cmd, data_audit, doc_audit, ok=False, message=reason),
        )
        return

    if not doc:
        reason = _reason(doc_audit)
        log.warning("No tank document for plate=%s reason=%s", cmd.licence_number, reason)
        await _publish_failure(
            cmd, reason,
            {"tank_data": data_audit, "tank_document": doc_audit},
            _build_tank_data_log(cmd, data_audit, doc_audit, ok=False, message=reason),
        )
        return

    out_event = None
    try:
        out_event = build_allocation_event(cmd, tank_data, doc)
    except TankDataIncompleteError as exc:
        log.warning(
            "Tank data incomplete for plate=%s field_issue=%s — reporting back to ERP",
            cmd.licence_number, exc.reason,
        )
        await _publish_failure(
            cmd, exc.reason,
            {"tank_data": data_audit, "tank_document": doc_audit},
            _build_tank_data_log(cmd, data_audit, doc_audit, ok=False, message=exc.reason),
        )
        return

    value = out_event.model_dump(mode="json")
    # Carry the ERP lookup audit forward on the SUCCESS path too, so the
    # dashboard shows what data FastLane's payload was actually built from —
    # not just the BASF calls. `metadata` is a free-form dict that survives
    # being copied through lookup-/upsert-minion as the event travels.
    value.setdefault("metadata", {})["erp_tank_data_log"] = _build_tank_data_log(
        cmd, data_audit, doc_audit, ok=True,
    )
    await emit(s.topic_allocate_requested, envelope_key(value), value)
    log.info(
        "Tank data+doc OK job=%s tank=%s plate=%s → allocate pipeline started",
        cmd.job_id, cmd.tank_id, cmd.licence_number,
    )


async def main() -> None:
    s = get_settings()
    await run_minion(
        name="tank-data-minion",
        topic=s.topic_tank_data_requested,
        group_id=s.consumer_group_tank_data,
        handler=handle,
        dlq_topic=s.topic_tank_data_dlq,
    )


if __name__ == "__main__":
    asyncio.run(main())
