"""HTTP client for the BASF Fastlane "Asset API for carriers" (v2.0.0).

Implements only the endpoints used by our integration:
    - getPublishedVersion  (look up a tank by licence_number + nationality)
    - newAsset             (create a vehicle component)
    - updateAsset          (update master-data fields on an existing vehicle component)
    - commitAsset          (start approval process)

Auth: "Standard" mode  → publicKey inside body.
      (JWT support is stubbed; enable KAFKA/FASTLANE_JWT once the ERP key is issued.)
"""
from __future__ import annotations

from typing import Any, Optional

import httpx

from ..config import get_settings
from ..logger import get_logger

log = get_logger(__name__)


class FastlaneApiError(Exception):
    def __init__(self, code: str, message: str, details: str | None = None):
        self.code = code
        self.message = message
        self.details = details
        super().__init__(f"[{code}] {message} — {details or ''}")


class FastlaneNotFound(FastlaneApiError):
    """Raised when getPublishedVersion returns 'Asset not found'."""


class FastlaneClient:
    def __init__(self) -> None:
        s = get_settings()
        # e.g.  https://prod.fastlane.now/backend/asset/api/v1/restAPIs
        self._base_url = s.fastlane_base_url.rstrip("/")
        self._public_key = s.fastlane_public_key
        self._timeout = s.fastlane_timeout
        self._mock = bool(s.fastlane_mock_mode)
        self._headers = {
            "Content-Type": "application/json;charset=UTF-8",
            "Accept": "application/json",
        }
        if self._mock:
            log.warning(
                "FastlaneClient running in MOCK MODE — no HTTP calls to BASF "
                "will be made. Set FASTLANE_MOCK_MODE=false in .env to disable."
            )

    # ---------------------------------------------------------------- internals
    async def _call(self, method: str, data: dict[str, Any]) -> dict[str, Any]:
        # ---- Mock-mode short-circuit -------------------------------------
        if self._mock:
            return self._mock_response(method, data)

        url = f"{self._base_url}/{method}"
        body = {"publicKey": self._public_key, "data": data}
        log.info("→ Fastlane POST %s data=%s", method, _redact(data))
        async with httpx.AsyncClient(timeout=self._timeout) as c:
            r = await c.post(url, json=body, headers=self._headers)
        # Fastlane returns 200 for both success and business errors; parse error_code.
        try:
            js = r.json()
        except Exception:
            r.raise_for_status()
            raise
        err = (js.get("error_code") or {})
        code = str(err.get("code", ""))
        if code != "0":
            msg = err.get("message", "Unknown")
            details = err.get("details")
            log.warning("← Fastlane %s error code=%s msg=%s details=%s",
                        method, code, msg, details)
            if details and "not found" in str(details).lower():
                raise FastlaneNotFound(code, msg, details)
            raise FastlaneApiError(code, msg, details)
        log.info("← Fastlane %s OK", method)
        return js.get("payload") or {}

    # ---------------------------------------------------------------- helpers
    @staticmethod
    def default_location_list(is_dangerous_goods: bool,
                              extra: Optional[list[str]] = None,
                              site_code: Optional[str] = None) -> list[str]:
        """Build the `locationList` per client policy (A4 / A6).

        Rules:
          - First entry is the hazard flag:
              "1" = asset usable for NON-Hazardous jobs
              "2" = asset usable for Hazardous (DG) jobs
          - Then the BASF site code (e.g. "102" for Ludwigshafen).
          - Additional codes may be appended by the caller.
        """
        base = ["2"] if is_dangerous_goods else ["1"]
        if site_code is None:
            site_code = get_settings().fastlane_site_code_default
        if site_code and site_code not in base:
            base.append(site_code)
        return base + [x for x in (extra or []) if x not in base]

    @staticmethod
    def vehicle_type_from_length(length_ft: Optional[int]) -> int:
        """Pick a Fastlane `type` code from tank length in feet (client A11).

        20ft → 311 (default), 32ft → 312. Anything else falls back to 311 so
        we never send an unsupported value.
        """
        if length_ft is None:
            return 311
        try:
            n = int(length_ft)
        except (TypeError, ValueError):
            return 311
        if n <= 22:
            return 311
        if n <= 34:
            return 312
        if n <= 42:
            return 313
        return 314

    # ---------------------------------------------------------------- API
    async def get_published_version(
        self,
        *,
        asset_id: Optional[int] = None,
        licence_number: Optional[str] = None,
        nationality: Optional[str] = None,
    ) -> dict[str, Any]:
        """Return the most-recent published version of a vehicle component.

        Per spec §3.4 + §1.2: identify by `id` OR by licence_number+nationality.
        Raises FastlaneNotFound when the asset does not exist yet.
        """
        data: dict[str, Any] = {}
        if asset_id is not None:
            data["id"] = asset_id
        else:
            if not licence_number:
                raise ValueError("licence_number or asset_id required")
            data["licence_number"] = licence_number
            if nationality:
                data["nationality"] = nationality
        return await self._call("getPublishedVersion", data)

    async def new_asset(
        self,
        *,
        vehicle_type: int,
        licence_number: str,
        nationality: Optional[str],
        location_list: list[str],
        owner: Optional[dict[str, str]] = None,
        fields: Optional[dict[str, Any]] = None,
    ) -> dict[str, Any]:
        """Create a new vehicle component (spec §2.1)."""
        data: dict[str, Any] = {
            "type": vehicle_type,
            "licence_number": licence_number,
            "locationList": location_list,
        }
        # Client A12: nationality is NOT required for tanks. Only include it
        # when explicitly enabled AND supplied.
        if nationality and get_settings().fastlane_send_nationality:
            data["nationality"] = nationality
        if owner:
            data["owner"] = owner
        payload = await self._call("newAsset", data)
        # If any fields were passed, apply them via updateAsset immediately.
        if fields:
            vehicle = payload.get("vehicle") or {}
            asset_id = vehicle.get("id")
            inc = vehicle.get("incrementalVersion")
            if asset_id and inc is not None:
                payload = await self.update_asset(
                    asset_id=asset_id,
                    incremental_version=inc,
                    fields=fields,
                )
        return payload

    async def update_asset(
        self,
        *,
        asset_id: int,
        incremental_version: int,
        fields: dict[str, Any],
        location_list: Optional[list[str]] = None,
    ) -> dict[str, Any]:
        """Update master-data fields of an existing vehicle component (spec §2.2)."""
        data: dict[str, Any] = {
            "id": asset_id,
            "incrementalVersion": incremental_version,
            "fields": fields,
        }
        if location_list is not None:
            data["locationList"] = location_list
        return await self._call("updateAsset", data)

    async def commit_asset(
        self,
        *,
        asset_id: int,
        incremental_version: int,
        location_list: list[str],
    ) -> dict[str, Any]:
        """Start the approval process (spec §2.4)."""
        return await self._call("commitAsset", {
            "id": asset_id,
            "incrementalVersion": incremental_version,
            "locationList": location_list,
        })

    async def revoke_commit(
        self,
        *,
        asset_id: int,
        incremental_version: int,
    ) -> dict[str, Any]:
        """Revoke a submitted commit before BASF support reviews it (spec §2.5).

        Moves the asset back to 'editing' state so fields/documents can be
        changed again before re-committing.
        """
        return await self._call("revokeCommit", {
            "id": asset_id,
            "incrementalVersion": incremental_version,
        })

    async def revert_asset(
        self,
        *,
        asset_id: int,
        incremental_version: int,
    ) -> dict[str, Any]:
        """Revert a published asset back to editing (spec §2.6).

        Used when changes must be made to an already-approved/published asset.
        A new editing version is created; incrementalVersion increases.
        """
        return await self._call("revertAsset", {
            "id": asset_id,
            "incrementalVersion": incremental_version,
        })

    async def disable_asset(
        self,
        *,
        asset_id: int,
        incremental_version: int,
    ) -> dict[str, Any]:
        """Permanently disable an asset (spec §2.7).

        The asset is removed from all location lists and can no longer be
        allocated. This action is irreversible.
        """
        return await self._call("disableAsset", {
            "id": asset_id,
            "incrementalVersion": incremental_version,
        })

    async def get_my_assets(
        self,
        *,
        page: Optional[int] = None,
        page_size: Optional[int] = None,
    ) -> dict[str, Any]:
        """List all assets owned by this carrier (spec §3.1).

        Returns a paginated list. `page` is 1-based; `page_size` defaults to
        the BASF server default (typically 20).
        """
        data: dict[str, Any] = {}
        if page is not None:
            data["page"] = page
        if page_size is not None:
            data["pageSize"] = page_size
        return await self._call("getMyAssets", data)

    async def get_editing_version(
        self,
        *,
        asset_id: Optional[int] = None,
        licence_number: Optional[str] = None,
        nationality: Optional[str] = None,
    ) -> dict[str, Any]:
        """Return the current editing (draft) version of an asset (spec §3.2).

        Use after `revertAsset` to retrieve the new editing version before
        calling `updateAsset` or `uploadFile`.
        Raises FastlaneNotFound if no editing version exists.
        """
        data: dict[str, Any] = {}
        if asset_id is not None:
            data["id"] = asset_id
        else:
            if not licence_number:
                raise ValueError("licence_number or asset_id required")
            data["licence_number"] = licence_number
            if nationality:
                data["nationality"] = nationality
        return await self._call("getEditingVersion", data)

    async def upload_file(
        self,
        *,
        asset_id: int,
        incremental_version: int,
        fields: list[str],
        name: str,
        content_base64: str,
    ) -> dict[str, Any]:
        """Upload a supporting document for an asset (spec §2.3).

        Per spec, the payload keys are: id, incrementalVersion, fields, name, content.
        `fields` is an array of field names the document validates
        (e.g. ["general_inspection", "vehicle_owner"]).
        `content_base64` is the file bytes encoded as base64. Max ~5 MB.
        Allowed types: PDF, JPG, PNG.
        """
        data: dict[str, Any] = {
            "id": asset_id,
            "incrementalVersion": incremental_version,
            "fields": fields,
            "name": name,
            "content": content_base64,
        }
        return await self._call("uploadFile", data)

    def default_owner(self) -> dict[str, str]:
        """Return the fallback owner block (client policy: use ITT / Head Office)."""
        s = get_settings()
        return {
            "name": s.fastlane_default_owner_name,
            "street": s.fastlane_default_owner_street,
            "zipCode": s.fastlane_default_owner_zip,
            "city": s.fastlane_default_owner_city,
            "country": s.fastlane_default_owner_country,
        }


def _redact(data: dict[str, Any]) -> dict[str, Any]:
    """Truncate large fields like base64 file content in logs."""
    out: dict[str, Any] = {}
    for k, v in data.items():
        if k == "content" and isinstance(v, str) and len(v) > 40:
            out[k] = f"<{len(v)} bytes base64>"
        else:
            out[k] = v
    return out


# ---------------------------------------------------------------------------
# Mock-mode implementation
# ---------------------------------------------------------------------------
# Simple in-process store: {"PLATE|NAT": {"id": int, "incrementalVersion": int}}
_MOCK_STORE: dict[str, dict[str, Any]] = {}
_MOCK_NEXT_ID: dict[str, int] = {"n": 900000}


def _mock_key(licence_number: str | None, nationality: str | None) -> str:
    return f"{(licence_number or '').upper()}|{(nationality or '').upper()}"


def _mock_call(method: str, data: dict[str, Any]) -> dict[str, Any]:
    """Return canned BASF-Fastlane-shaped responses so the whole pipeline
    (registration minion → status listener → ERP callback) exercises without
    needing real BASF credentials.

    Contract kept identical to the real API: response is the `payload` dict
    that would sit under `js["payload"]` (i.e. `{"vehicle": {...}}`).
    """
    licence = data.get("licence_number")
    nat = data.get("nationality")
    key = _mock_key(licence, nat)

    if method == "getPublishedVersion":
        # If the caller specified an id, always return it as "found".
        if data.get("id"):
            return {
                "vehicle": {
                    "id": int(data["id"]),
                    "incrementalVersion": 1,
                    "licence_number": licence,
                    "nationality": nat,
                    "status": "approved",
                }
            }
        # Otherwise consult the in-memory store.
        if key in _MOCK_STORE:
            v = _MOCK_STORE[key]
            log.info("[MOCK] getPublishedVersion HIT %s → id=%s", key, v["id"])
            return {
                "vehicle": {
                    "id": v["id"],
                    "incrementalVersion": v["incrementalVersion"],
                    "licence_number": licence,
                    "nationality": nat,
                    "status": "approved",
                }
            }
        log.info("[MOCK] getPublishedVersion MISS %s → NotFound", key)
        raise FastlaneNotFound("404", "Asset not found", "asset not found")

    if method == "newAsset":
        _MOCK_NEXT_ID["n"] += 1
        new_id = _MOCK_NEXT_ID["n"]
        _MOCK_STORE[key] = {"id": new_id, "incrementalVersion": 1}
        log.info("[MOCK] newAsset %s → id=%s", key, new_id)
        return {
            "vehicle": {
                "id": new_id,
                "incrementalVersion": 1,
                "licence_number": licence,
                "nationality": nat,
                "status": "pending",
            }
        }

    if method == "updateAsset":
        aid = int(data.get("id", 0))
        new_ver = int(data.get("incrementalVersion", 1)) + 1
        # keep store in sync
        for k, v in _MOCK_STORE.items():
            if v["id"] == aid:
                v["incrementalVersion"] = new_ver
                break
        log.info("[MOCK] updateAsset id=%s → v=%s", aid, new_ver)
        return {
            "vehicle": {
                "id": aid,
                "incrementalVersion": new_ver,
                "status": "pending",
            }
        }

    if method == "commitAsset":
        aid = int(data.get("id", 0))
        log.info("[MOCK] commitAsset id=%s → PUBLISHED", aid)
        return {
            "vehicle": {
                "id": aid,
                "incrementalVersion": int(data.get("incrementalVersion", 1)),
                "status": "approved",
            }
        }

    if method == "revokeCommit":
        aid = int(data.get("id", 0))
        log.info("[MOCK] revokeCommit id=%s → editing", aid)
        return {"vehicle": {"id": aid,
                            "incrementalVersion": int(data.get("incrementalVersion", 1)),
                            "status": "editing"}}

    if method == "revertAsset":
        aid = int(data.get("id", 0))
        new_ver = int(data.get("incrementalVersion", 1)) + 1
        for v in _MOCK_STORE.values():
            if v["id"] == aid:
                v["incrementalVersion"] = new_ver
                break
        log.info("[MOCK] revertAsset id=%s → v=%s editing", aid, new_ver)
        return {"vehicle": {"id": aid, "incrementalVersion": new_ver, "status": "editing"}}

    if method == "disableAsset":
        aid = int(data.get("id", 0))
        log.info("[MOCK] disableAsset id=%s → disabled", aid)
        return {"vehicle": {"id": aid,
                            "incrementalVersion": int(data.get("incrementalVersion", 1)),
                            "status": "disabled"}}

    if method == "getMyAssets":
        assets = [
            {"id": v["id"], "incrementalVersion": v["incrementalVersion"], "status": "approved"}
            for v in _MOCK_STORE.values()
        ]
        log.info("[MOCK] getMyAssets → %d assets", len(assets))
        return {"vehicles": assets, "total": len(assets)}

    if method == "getEditingVersion":
        if data.get("id"):
            aid = int(data["id"])
            for v in _MOCK_STORE.values():
                if v["id"] == aid:
                    return {"vehicle": {"id": aid,
                                        "incrementalVersion": v["incrementalVersion"],
                                        "status": "editing"}}
        if key in _MOCK_STORE:
            v = _MOCK_STORE[key]
            return {"vehicle": {"id": v["id"],
                                "incrementalVersion": v["incrementalVersion"],
                                "status": "editing"}}
        raise FastlaneNotFound("404", "Asset not found", "no editing version")

    if method == "uploadFile":
        aid = int(data.get("id", 0))
        log.info("[MOCK] uploadFile id=%s name=%s fields=%s",
                 aid, data.get("name"), data.get("fields"))
        new_ver = int(data.get("incrementalVersion", 1)) + 1
        return {
            "vehicle": {
                "id": aid,
                "incrementalVersion": new_ver,
                "status": "pending",
            }
        }

    log.warning("[MOCK] Unhandled method %s — returning empty payload", method)
    return {}


# Bind the free function as a bound method for readability in _call().
FastlaneClient._mock_response = staticmethod(_mock_call)  # type: ignore[attr-defined]
