"""Minimal Hubject compatible REST API layer.

This module exposes a small Flask application which provides the Hubject
OICP 2.3 endpoints that are relevant for the Pipelet deployment.  Incoming
requests are validated against the deployment configuration stored in
``hubject-config.json`` and the payloads are then forwarded to the internal OCPP
HTTP API.

The service persists the raw requests for auditing purposes and enforces
mutual TLS by default.  Certificates from ``certs/`` are used to set up the
TLS context when the module is executed directly.
"""

from __future__ import annotations

import json
import logging
import os
import re
import ssl
import threading
from datetime import datetime, timezone
from pathlib import Path
from collections.abc import Mapping, Sequence
from typing import Any
from urllib.parse import urljoin

import pymysql
import requests
from flask import Flask, Response, abort, jsonify, request


CONFIG_FILE = "hubject-config.json"
CONFIG_PATH = Path(__file__).resolve().parent / CONFIG_FILE
CONFIG_FOUND = CONFIG_PATH.is_file()

try:
    _raw_config = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
except FileNotFoundError:
    _raw_config = {}

CONFIG_DIR = CONFIG_PATH.resolve().parent

MYSQL_CONFIG: dict[str, Any] = {
    "host": "127.0.0.1",
    "user": "root",
    "password": "",
    "db": "op",
    "charset": "utf8mb4",
}
MYSQL_CONFIG.update(_raw_config.get("mysql", {}))

hubject_api_cfg = _raw_config.get("hubject_api", {})
hubject_client_cfg = _raw_config.get("hubject", {})

DEFAULT_HUBJECT_PARTNER_ID = "DE*WLE"

log_cfg = _raw_config.get("log_levels", {})
level_name = log_cfg.get("hubject_api", "DEBUG").upper()
LOG_LEVEL = getattr(logging, level_name, logging.DEBUG)
logging.basicConfig(
    level=LOG_LEVEL,
    format="%(asctime)s %(levelname)s:%(name)s:%(message)s",
)

logger = logging.getLogger(__name__)
if CONFIG_FOUND:
    logger.info("Hubject config found at %s", CONFIG_PATH)
else:
    logger.warning("Hubject config not found at %s", CONFIG_PATH)
logger.info("MySQL host configured as %s", MYSQL_CONFIG.get("host"))


def _merge_operator_evse_entries(
    entries: list[Any],
) -> dict[str, Any]:
    """Merge a list of OperatorEvseData mappings into a single mapping."""

    merged: dict[str, Any] = {}
    evse_records: list[Any] = []

    for entry in entries:
        if not isinstance(entry, Mapping):
            continue

        for key, value in entry.items():
            if key in {"EvseData", "EvseDataRecord"}:
                if isinstance(value, list):
                    evse_records.extend(value)
                continue

            merged.setdefault(key, value)

    if evse_records:
        merged.setdefault("EvseDataRecord", []).extend(evse_records)

    return merged


def _merge_operator_evse_status_entries(
    entries: list[Any],
) -> dict[str, Any]:
    """Merge OperatorEvseStatus entries into a single mapping."""

    merged: dict[str, Any] = {}
    status_records: list[Any] = []

    for entry in entries:
        if not isinstance(entry, Mapping):
            continue

        for key, value in entry.items():
            if key in {"EvseStatusRecords", "EvseStatusRecord"}:
                if isinstance(value, list):
                    status_records.extend(value)
                elif isinstance(value, Mapping):
                    status_records.append(dict(value))
                continue

            merged.setdefault(key, value)

    if status_records:
        merged.setdefault("EvseStatusRecords", []).extend(status_records)

    return merged


# Canonical string representations expected by Hubject for the
# ``CalibrationLawDataAvailability`` attribute.  Hubject is strict about the
# literal values which means that inputs such as ``"NotAvailable"`` (without a
# space) are rejected with a ``400`` response.  Normalize common variations to
# the canonical schema spelling to improve compatibility.
_CALIBRATION_LAW_DATA_AVAILABILITY: dict[str, str] = {
    "local": "Local",
    "external": "External",
    "not available": "Not Available",
    "notavailable": "Not Available",
    "not_available": "Not Available",
}


def _normalize_calibration_law_data(record: Mapping[str, Any]) -> dict[str, Any]:
    """Return an EVSE record with normalized calibration law metadata."""

    normalized = dict(record)
    value = normalized.get("CalibrationLawDataAvailability")
    if isinstance(value, str):
        candidate = " ".join(value.replace("-", " ").split()).strip()
        canonical = _CALIBRATION_LAW_DATA_AVAILABILITY.get(candidate.lower())
        if canonical:
            normalized["CalibrationLawDataAvailability"] = canonical
    return normalized


def _normalize_operator_evse_data(payload: dict[str, Any]) -> None:
    """Ensure the OperatorEvseData payload matches Hubject's expectations."""

    operator_id = payload.get("OperatorID")
    operator_entries = payload.get("OperatorEvseData")

    if isinstance(operator_entries, list):
        normalized = _merge_operator_evse_entries(list(operator_entries))
    elif isinstance(operator_entries, Mapping):
        normalized = dict(operator_entries)
    else:
        return

    if operator_id and not normalized.get("OperatorID"):
        normalized["OperatorID"] = operator_id

    if "EvseData" in normalized and "EvseDataRecord" not in normalized:
        evse_data = normalized.pop("EvseData")
        if isinstance(evse_data, list):
            normalized["EvseDataRecord"] = list(evse_data)

    operator_name = normalized.get("OperatorName")
    if operator_name:
        normalized["OperatorName"] = _normalize_operator_name(operator_name)

    records = normalized.get("EvseDataRecord")
    if isinstance(records, list):
        normalized_records = []
        for record in records:
            if isinstance(record, Mapping):
                normalized_records.append(_normalize_calibration_law_data(record))
            else:
                normalized_records.append(record)
        normalized["EvseDataRecord"] = normalized_records

    payload["OperatorEvseData"] = normalized


def _normalize_operator_evse_status(payload: dict[str, Any]) -> None:
    """Normalize the OperatorEvseStatus payload for Hubject compatibility."""

    operator_id = payload.get("OperatorID")
    operator_entries = payload.get("OperatorEvseStatus")

    if isinstance(operator_entries, list):
        normalized = _merge_operator_evse_status_entries(list(operator_entries))
    elif isinstance(operator_entries, Mapping):
        normalized = dict(operator_entries)
    else:
        return

    if operator_id and not normalized.get("OperatorID"):
        normalized["OperatorID"] = operator_id

    operator_name = normalized.get("OperatorName")
    if operator_name:
        normalized["OperatorName"] = _normalize_operator_name(operator_name)

    records = normalized.get("EvseStatusRecords")
    if isinstance(records, Mapping):
        normalized["EvseStatusRecords"] = [dict(records)]
    elif isinstance(records, list):
        normalized_records = []
        for record in records:
            if isinstance(record, Mapping):
                normalized_records.append(dict(record))
            else:
                normalized_records.append(record)
        normalized["EvseStatusRecords"] = normalized_records

    payload["OperatorEvseStatus"] = normalized


def _coerce_float(value: Any) -> float | None:
    """Best-effort conversion of ``value`` to ``float``."""

    if value in (None, ""):
        return None
    if isinstance(value, (int, float)):
        return float(value)
    if isinstance(value, str):
        candidate = value.strip()
        if not candidate:
            return None
        try:
            return float(candidate)
        except ValueError:
            return None
    return None


def _first_non_empty_str(*candidates: Any) -> str | None:
    """Return the first non-empty string in ``candidates``."""

    for candidate in candidates:
        if isinstance(candidate, str):
            stripped = candidate.strip()
            if stripped:
                return stripped
    return None


def _first_mapping(*candidates: Any) -> dict[str, Any] | None:
    """Return the first mapping found in ``candidates``."""

    for candidate in candidates:
        if isinstance(candidate, Mapping):
            return dict(candidate)
    return None


def _operator_id_for_path(operator_id: str) -> str:
    """Format an OperatorID for usage inside Hubject REST paths."""

    return operator_id.replace(" ", "")


_EVSE_ID_PATTERN = re.compile(
    r"^(([A-Za-z]{2}\*?[A-Za-z0-9]{3}\*?E[A-Za-z0-9\*]{1,30})|(\+?[0-9]{1,3}\*[0-9]{3}\*[0-9\*]{1,32}))$"
)


def _normalize_evse_identifier(evse_id: str, operator_id: str | None = None) -> str:
    """Return an EVSE ID formatted according to Hubject's expectations."""

    normalized = str(evse_id or "").strip()
    if not normalized:
        return ""

    candidate = normalized.replace(" ", "")
    if candidate.upper().startswith("EVSE-"):
        candidate = candidate[5:]
    candidate = candidate.replace("#", "*")
    candidate = candidate.replace("−", "*").replace("–", "*")
    candidate = candidate.replace("-", "*")
    candidate = candidate.upper()

    # Collapse duplicate separators that might have been introduced when replacing characters.
    while "**" in candidate:
        candidate = candidate.replace("**", "*")

    operator_candidate = str(operator_id or "").strip().replace(" ", "").upper()

    if _EVSE_ID_PATTERN.fullmatch(candidate):
        return candidate

    parts = [part for part in candidate.split("*") if part]

    def _build_candidate(country: str, operator: str, rest_parts: list[str]) -> str | None:
        filtered = [part for part in rest_parts if part]
        if not filtered:
            return None
        rest = "*".join(filtered)
        if not rest.startswith("E"):
            rest = f"E{rest}"
        proposal = f"{country}*{operator}*{rest}" if country and operator else None
        if proposal and _EVSE_ID_PATTERN.fullmatch(proposal):
            return proposal
        return None

    if operator_candidate and "*" in operator_candidate:
        country, _, operator = operator_candidate.partition("*")
        rest_options: list[list[str]] = []
        if parts:
            rest_options.append(parts)
            if parts[0] == "EVSE":
                rest_options.append(parts[1:])
            if len(parts) >= 3:
                rest_options.append(parts[2:])
        for rest_parts in rest_options:
            proposal = _build_candidate(country, operator, rest_parts)
            if proposal:
                return proposal

    if len(parts) >= 3:
        proposal = _build_candidate(parts[0], parts[1], parts[2:])
        if proposal:
            return proposal

    return normalized


def _normalise_charge_detail_record(
    source: Mapping[str, Any],
    default_operator_id: str | None,
) -> dict[str, Any]:
    """Convert internal CDR payloads into Hubject's CDR schema."""

    start_info = source.get("StartInfo") if isinstance(source.get("StartInfo"), Mapping) else {}
    stop_info = source.get("StopInfo") if isinstance(source.get("StopInfo"), Mapping) else {}
    remote_request = (
        source.get("RemoteRequest")
        if isinstance(source.get("RemoteRequest"), Mapping)
        else {}
    )

    session_id = _first_non_empty_str(source.get("SessionID"), source.get("session_id"))
    if not session_id:
        raise HubjectApiError("CDR payload missing SessionID")

    evse_id = _first_non_empty_str(
        source.get("EvseID"),
        source.get("EVSEID"),
        start_info.get("evseId") if isinstance(start_info, Mapping) else None,
        stop_info.get("evseId") if isinstance(stop_info, Mapping) else None,
    )
    if not evse_id:
        raise HubjectApiError("CDR payload missing EvseID")

    hub_operator_id = _first_non_empty_str(
        source.get("HubOperatorID"),
        source.get("OperatorID"),
        default_operator_id,
    )

    normalized_evse_id = _normalize_evse_identifier(evse_id, hub_operator_id or default_operator_id)
    if not _EVSE_ID_PATTERN.fullmatch(normalized_evse_id):
        raise HubjectApiError(
            f"CDR payload contains invalid EvseID '{evse_id}'"
        )

    identification = _first_mapping(
        source.get("Identification"),
        remote_request.get("Identification") if isinstance(remote_request, Mapping) else None,
    )
    if identification is None:
        token_value = _first_non_empty_str(
            source.get("Token"),
            start_info.get("idTag") if isinstance(start_info, Mapping) else None,
            stop_info.get("idTag") if isinstance(stop_info, Mapping) else None,
        )
        if token_value:
            identification = {"RemoteIdentification": {"EvcoID": token_value}}
        else:
            raise HubjectApiError("CDR payload missing Identification")

    charging_start = _first_non_empty_str(
        source.get("ChargingStart"),
        source.get("StartTimestamp"),
        start_info.get("sessionStartTimestamp") if isinstance(start_info, Mapping) else None,
        start_info.get("startTimestamp") if isinstance(start_info, Mapping) else None,
        start_info.get("timestamp") if isinstance(start_info, Mapping) else None,
    )
    if not charging_start:
        raise HubjectApiError("CDR payload missing ChargingStart")

    charging_end = _first_non_empty_str(
        source.get("ChargingEnd"),
        source.get("StopTimestamp"),
        stop_info.get("stopTimestamp") if isinstance(stop_info, Mapping) else None,
        stop_info.get("timestamp") if isinstance(stop_info, Mapping) else None,
    )
    if not charging_end:
        raise HubjectApiError("CDR payload missing ChargingEnd")

    session_start = _first_non_empty_str(
        source.get("SessionStart"),
        start_info.get("sessionStartTimestamp") if isinstance(start_info, Mapping) else None,
        start_info.get("timestamp") if isinstance(start_info, Mapping) else None,
        charging_start,
    )
    session_end = _first_non_empty_str(
        source.get("SessionEnd"),
        stop_info.get("sessionEndTimestamp") if isinstance(stop_info, Mapping) else None,
        stop_info.get("timestamp") if isinstance(stop_info, Mapping) else None,
        charging_end,
    )

    meter_start = _coerce_float(
        source.get("MeterValueStart")
        or source.get("MeterStart")
        or (start_info.get("meterStart") if isinstance(start_info, Mapping) else None)
        or (start_info.get("meterStartWh") if isinstance(start_info, Mapping) else None)
    )
    meter_end = _coerce_float(
        source.get("MeterValueEnd")
        or source.get("MeterStop")
        or (stop_info.get("meterStop") if isinstance(stop_info, Mapping) else None)
        or (stop_info.get("meterStopWh") if isinstance(stop_info, Mapping) else None)
    )

    consumed_energy = _coerce_float(source.get("ConsumedEnergy"))
    if consumed_energy is None and meter_start is not None and meter_end is not None:
        consumed_energy = max(meter_end - meter_start, 0.0)
    if consumed_energy is None:
        consumed_energy = 0.0

    record: dict[str, Any] = {
        "SessionID": session_id,
        "EvseID": normalized_evse_id,
        "Identification": identification,
        "ChargingStart": charging_start,
        "ChargingEnd": charging_end,
        "SessionStart": session_start or charging_start,
        "SessionEnd": session_end or charging_end,
        "ConsumedEnergy": consumed_energy,
    }

    cpo_session_id = _first_non_empty_str(source.get("CPOPartnerSessionID"))
    if cpo_session_id:
        record["CPOPartnerSessionID"] = cpo_session_id

    emp_session_id = _first_non_empty_str(source.get("EMPPartnerSessionID"))
    if emp_session_id:
        record["EMPPartnerSessionID"] = emp_session_id

    partner_product_id = _first_non_empty_str(
        source.get("PartnerProductID"),
        remote_request.get("PartnerProductID") if isinstance(remote_request, Mapping) else None,
    )
    if partner_product_id:
        record["PartnerProductID"] = partner_product_id

    if meter_start is not None:
        record["MeterValueStart"] = meter_start
    if meter_end is not None:
        record["MeterValueEnd"] = meter_end

    meter_values = source.get("MeterValueInBetween")
    if isinstance(meter_values, Mapping):
        record["MeterValueInBetween"] = dict(meter_values)
    elif isinstance(meter_values, list):
        record["MeterValueInBetween"] = {"meterValues": list(meter_values)}

    signed_values = source.get("SignedMeteringValues")
    if isinstance(signed_values, list):
        record["SignedMeteringValues"] = list(signed_values)

    calibration_info = source.get("CalibrationLawVerificationInfo")
    if isinstance(calibration_info, Mapping):
        record["CalibrationLawVerificationInfo"] = dict(calibration_info)

    if hub_operator_id:
        record["HubOperatorID"] = hub_operator_id

    hub_provider_id = _first_non_empty_str(
        source.get("HubProviderID"),
        source.get("ProviderID"),
    )
    if hub_provider_id:
        record["HubProviderID"] = hub_provider_id

    return record


def _normalize_operator_name(value: Any) -> str:
    """Coerce localized operator name payloads to a single string value."""

    if isinstance(value, Mapping):
        # Hubject expects a plain string. Prefer English translations but fall back
        # to the first non-empty entry if necessary.
        for key in ("en", "En", "EN"):
            candidate = value.get(key)
            if candidate:
                return str(candidate)
        for candidate in value.values():
            if candidate:
                return str(candidate)
        return ""

    if isinstance(value, (list, tuple)):
        for candidate in value:
            normalized = _normalize_operator_name(candidate)
            if normalized:
                return normalized
        return ""

    if value is None:
        return ""

    return str(value)


def _extract_evse_id(payload: Mapping[str, Any] | None) -> str:
    """Return a normalized EVSE ID from a Hubject request payload."""

    if not isinstance(payload, Mapping):
        return ""

    candidates = (
        "EVSEId",
        "EVSEID",
        "EvseID",
        "evse_id",
        "evseId",
        "evseID",
        "evseid",
    )

    for key in candidates:
        value = payload.get(key)
        if value not in (None, ""):
            return str(value).strip()

    return ""


def _resolve_path(value: str | None) -> Path | None:
    if not value:
        return None
    candidate = Path(value)
    if not candidate.is_absolute():
        candidate = CONFIG_DIR / candidate
    return candidate


def _resolve_paths(value: str | Sequence[str] | None) -> list[Path]:
    if not value:
        return []
    if isinstance(value, (list, tuple, set)):
        items = value
    else:
        items = [value]
    resolved: list[Path] = []
    for item in items:
        if not item:
            continue
        resolved_path = _resolve_path(str(item))
        if resolved_path:
            resolved.append(resolved_path)
    return resolved


def _build_ca_bundle(paths: Sequence[Path], bundle_name: str) -> Path | None:
    if not paths:
        return None
    if len(paths) == 1:
        return paths[0]
    target_dir = CONFIG_DIR / "certs"
    try:
        target_dir.mkdir(parents=True, exist_ok=True)
        bundle_path = target_dir / f"{bundle_name}_bundle.pem"
        with bundle_path.open("wb") as handle:
            for path in paths:
                data = path.read_bytes()
                handle.write(data)
                if not data.endswith(b"\n"):
                    handle.write(b"\n")
        return bundle_path
    except OSError as exc:
        logger.error("Failed to build CA bundle %s: %s", bundle_name, exc)
        return None


class OCPPApiError(RuntimeError):
    """Raised when the OCPP REST API returns an error."""


class OCPPApiClient:
    """Simple wrapper around the OCPP REST API used by Hubject routes."""

    def __init__(
        self,
        base_url: str,
        *,
        timeout: float = 10.0,
        verify: bool | str = True,
        hubject_base_url: str | None = None,
    ) -> None:
        normalized_base = self._normalize_base(base_url)
        hubject_base = self._normalize_base(hubject_base_url) if hubject_base_url is not None else normalized_base
        self.base_url = normalized_base
        self._hubject_base_url = hubject_base
        self.timeout = timeout
        self.verify = verify
        self._log = logging.getLogger(self.__class__.__name__)

    @staticmethod
    def _normalize_base(base_url: str | None) -> str:
        if not base_url:
            return ""
        return base_url.rstrip("/") + "/"

    def _post(
        self,
        path: str,
        payload: Mapping[str, Any],
        *,
        base_url: str | None = None,
    ) -> Mapping[str, Any]:
        target_base = base_url or self.base_url
        if not target_base:
            raise OCPPApiError("OCPP base URL not configured")

        normalized_base = target_base.rstrip("/") + "/"
        normalized_path = path.lstrip("/")
        url = urljoin(normalized_base, normalized_path)
        self._log.info("POST %s", url)
        logger.info("Forwarding Hubject request to OCPP API: %s", url)
        try:
            serialized_payload = json.dumps(payload, ensure_ascii=False)
        except (TypeError, ValueError):
            serialized_payload = str(payload)
        self._log.debug("payload=%s", serialized_payload)
        logger.debug("OCPP API payload: %s", serialized_payload)
        try:
            response = requests.post(
                url,
                json=dict(payload),
                timeout=self.timeout,
                verify=self.verify,
            )
        except requests.RequestException as exc:  # pragma: no cover - network
            raise OCPPApiError(f"OCPP API request failed: {exc}") from exc

        response_text = response.text
        self._log.debug(
            "response_status=%s, body=%s", response.status_code, response_text
        )
        logger.debug(
            "OCPP API response (%s) for %s: %s",
            response.status_code,
            url,
            response_text,
        )

        if response.status_code >= 400:
            raise OCPPApiError(
                f"OCPP API returned {response.status_code} for {url}: {response_text}"
            )

        try:
            data = response.json()
        except ValueError as exc:
            raise OCPPApiError("OCPP API did not return JSON") from exc

        return data

    def authorize_start(
        self,
        evse_id: str,
        identification: Mapping[str, Any],
        session_id: str,
    ) -> Mapping[str, Any]:
        payload = {
            "evse_id": evse_id,
            "identification": identification,
            "session_id": session_id,
            "phase": "start",
        }
        return self._post("hubject/authorize-start", payload, base_url=self._hubject_base_url)

    def authorize_stop(
        self,
        evse_id: str,
        identification: Mapping[str, Any],
        session_id: str,
    ) -> Mapping[str, Any]:
        payload = {
            "evse_id": evse_id,
            "identification": identification,
            "session_id": session_id,
            "phase": "stop",
        }
        return self._post("hubject/authorize-stop", payload, base_url=self._hubject_base_url)

    def upload_cdr(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("hubject/cdr", payload, base_url=self._hubject_base_url)

    def push_evse_data(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("hubject/evse-data", payload, base_url=self._hubject_base_url)

    def push_evse_status(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("hubject/evse-status", payload, base_url=self._hubject_base_url)

    def remote_start_transaction(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("hubject/remote-start", payload, base_url=self._hubject_base_url)

    def remote_stop_transaction(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("hubject/remote-stop", payload, base_url=self._hubject_base_url)

    def authorize_remote_start_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("2.3/authorize-remote-start-evse", payload, base_url=self.base_url)

    def authorize_remote_stop_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        return self._post("2.3/authorize-remote-stop-evse", payload, base_url=self.base_url)


OCPP_API_BASE_URL = str(hubject_api_cfg.get("ocpp_base_url", "http://127.0.0.1:9751"))
HUBJECT_API_BASE_URL = str(hubject_api_cfg.get("hubject_base_url", OCPP_API_BASE_URL))
OCPP_API_TIMEOUT = float(hubject_api_cfg.get("ocpp_timeout", 10.0))
OCPP_API_VERIFY = hubject_api_cfg.get("ocpp_verify_tls", False)
if isinstance(OCPP_API_VERIFY, str):
    OCPP_API_VERIFY = _resolve_path(OCPP_API_VERIFY) or OCPP_API_VERIFY

OCPP_CLIENT = OCPPApiClient(
    OCPP_API_BASE_URL,
    timeout=OCPP_API_TIMEOUT,
    verify=OCPP_API_VERIFY,
    hubject_base_url=HUBJECT_API_BASE_URL,
)


class HubjectApiError(RuntimeError):
    """Raised when the Hubject REST API returns an error."""


class HubjectApiClient:
    """Minimal synchronous client for calling the Hubject OICP endpoints."""

    def __init__(
        self,
        base_url: str,
        *,
        timeout: float = 30.0,
        authorization: str | None = None,
        verify: bool | str = True,
        cert: tuple[str, str] | None = None,
        operator_id: str | None = None,
        partner_id: str | None = None,
    ) -> None:
        normalized_base = base_url.rstrip("/") + "/" if base_url else ""
        self.base_url = normalized_base
        self.timeout = timeout
        self.authorization = authorization.strip() if isinstance(authorization, str) else None
        self.verify = verify
        self.cert = cert
        self.operator_id = _first_non_empty_str(operator_id)
        self.partner_id = _first_non_empty_str(partner_id, DEFAULT_HUBJECT_PARTNER_ID)
        self._log = logging.getLogger(self.__class__.__name__)

    def _post(self, path: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        if not self.base_url:
            raise HubjectApiError("Hubject base URL not configured")

        if isinstance(self.verify, str) and self.verify.strip():
            if not os.path.exists(self.verify) or not os.access(self.verify, os.R_OK):
                self._log.error(
                    "Hubject CA bundle is missing or not readable: %s", self.verify
                )
                raise HubjectApiError(
                    f"Hubject CA bundle not found or not readable: {self.verify}"
                )

        normalized_base = self.base_url.rstrip("/") + "/"
        normalized_path = path.lstrip("/")
        url = urljoin(normalized_base, normalized_path)
        headers = {"Content-Type": "application/json"}
        if self.authorization:
            headers["Authorization"] = self.authorization

        self._log.info("POST %s", url)
        try:
            serialized_payload = json.dumps(payload, ensure_ascii=False)
        except (TypeError, ValueError):
            serialized_payload = str(payload)
        self._log.debug("payload=%s", serialized_payload)

        try:
            response = requests.post(
                url,
                json=dict(payload),
                timeout=self.timeout,
                verify=self.verify,
                cert=self.cert,
                headers=headers,
            )
        except requests.RequestException as exc:  # pragma: no cover - network
            raise HubjectApiError(f"Hubject API request failed: {exc}") from exc

        response_text = response.text
        self._log.debug(
            "response_status=%s, body=%s", response.status_code, response_text
        )

        if response.status_code >= 400:
            raise HubjectApiError(
                f"Hubject API returned {response.status_code} for {url}: {response_text}"
            )

        if not response.content:
            return {}

        try:
            data = response.json()
        except ValueError:
            return {"raw": response_text}

        return data

    def send_cdr(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        """Upload a charge detail record via the Hubject CDR endpoint."""

        record = _normalise_charge_detail_record(payload, self.operator_id)
        operator_id = _first_non_empty_str(record.get("HubOperatorID"), self.operator_id)
        if not operator_id:
            raise HubjectApiError("Operator ID is required for CDR upload")

        path_partner = _first_non_empty_str(self.partner_id, self.operator_id, operator_id)
        path_operator = _operator_id_for_path(path_partner)

        path = f"cdrmgmt/v22/operators/{path_operator}/charge-detail-record"
        return self._post(path, record)

    def _normalise_remote_payload(self, payload: Mapping[str, Any]) -> dict[str, Any]:
        request_payload: dict[str, Any] = dict(payload)

        operator_candidate = _first_non_empty_str(
            request_payload.get("OperatorID"),
            request_payload.get("operator_id"),
            self.operator_id,
            self.partner_id,
        )
        if operator_candidate:
            request_payload["OperatorID"] = operator_candidate
        request_payload.pop("operator_id", None)

        provider_candidate = _first_non_empty_str(
            request_payload.get("ProviderID"),
            request_payload.get("provider_id"),
            request_payload.get("ExternalID"),
            request_payload.get("externalID"),
            self.partner_id,
        )
        if provider_candidate:
            request_payload["ProviderID"] = provider_candidate
        request_payload.pop("provider_id", None)

        evse_candidate = (
            request_payload.get("EVSEId")
            or request_payload.get("EvseID")
            or request_payload.get("EVSEID")
        )
        if evse_candidate and "EVSEId" not in request_payload:
            request_payload["EVSEId"] = evse_candidate

        return request_payload

    def authorize_remote_start_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        request_payload = self._normalise_remote_payload(payload)
        provider_id = _first_non_empty_str(
            request_payload.get("ProviderID"),
            self.partner_id,
        )
        if not provider_id:
            raise HubjectApiError("Provider ID is required for remote start authorization")

        path_provider = _operator_id_for_path(provider_id)
        path = f"charging/v21/providers/{path_provider}/authorize-remote/start"
        return self._post(path, request_payload)

    def authorize_remote_stop_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        request_payload = self._normalise_remote_payload(payload)
        provider_id = _first_non_empty_str(
            request_payload.get("ProviderID"),
            self.partner_id,
        )
        if not provider_id:
            raise HubjectApiError("Provider ID is required for remote stop authorization")

        path_provider = _operator_id_for_path(provider_id)
        path = f"charging/v21/providers/{path_provider}/authorize-remote/stop"
        return self._post(path, request_payload)

    def authorize_start_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        request_payload = self._normalise_remote_payload(payload)
        operator_id = _first_non_empty_str(
            request_payload.get("OperatorID"),
            self.operator_id,
            self.partner_id,
        )
        if not operator_id:
            raise HubjectApiError("Operator ID is required for start authorization")

        request_payload["OperatorID"] = operator_id
        path_operator = _operator_id_for_path(operator_id)
        path = f"charging/v21/operators/{path_operator}/authorize/start"
        return self._post(path, request_payload)

    def authorize_stop_evse(self, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        request_payload = self._normalise_remote_payload(payload)
        operator_id = _first_non_empty_str(
            request_payload.get("OperatorID"),
            self.operator_id,
            self.partner_id,
        )
        if not operator_id:
            raise HubjectApiError("Operator ID is required for stop authorization")

        request_payload["OperatorID"] = operator_id
        path_operator = _operator_id_for_path(operator_id)
        path = f"charging/v21/operators/{path_operator}/authorize/stop"
        return self._post(path, request_payload)

    def push_evse_data(self, operator_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        operator = str(operator_id or "").strip()
        if not operator:
            raise HubjectApiError("Operator ID is required for EVSE data push")

        request_payload: dict[str, Any] = dict(payload)
        request_payload["OperatorID"] = operator
        request_payload.pop("operator_id", None)

        path = f"evsepush/v23/operators/{operator}/data-records"
        return self._post(path, request_payload)

    def push_evse_status(self, operator_id: str, payload: Mapping[str, Any]) -> Mapping[str, Any]:
        operator = str(operator_id or "").strip()
        if not operator:
            raise HubjectApiError("Operator ID is required for EVSE status push")

        request_payload: dict[str, Any] = dict(payload)
        request_payload["OperatorID"] = operator
        request_payload.pop("operator_id", None)

        path = f"evsepush/v21/operators/{operator}/status-records"
        return self._post(path, request_payload)


def _prepare_hubject_cert_pair(
    cert_path: Path | None, key_path: Path | None
) -> tuple[str, str] | None:
    if cert_path and key_path:
        return str(cert_path), str(key_path)
    return None


HUBJECT_API_BASE_URL = str(hubject_client_cfg.get("api_base_url", ""))
HUBJECT_API_TIMEOUT = float(hubject_client_cfg.get("timeout", 30.0) or 30.0)
HUBJECT_API_AUTH = hubject_client_cfg.get("authorization")
HUBJECT_API_CERT = _resolve_path(hubject_client_cfg.get("client_cert"))
HUBJECT_API_KEY = _resolve_path(hubject_client_cfg.get("client_key"))
HUBJECT_API_CA_BUNDLE_PATHS = _resolve_paths(hubject_client_cfg.get("ca_bundle"))
HUBJECT_API_CA_BUNDLE = _build_ca_bundle(HUBJECT_API_CA_BUNDLE_PATHS, "hubject_api")
HUBJECT_OPERATOR_ID = _first_non_empty_str(
    hubject_client_cfg.get("operator_id"),
    hubject_api_cfg.get("operator_id"),
    hubject_client_cfg.get("partner_id"),
    DEFAULT_HUBJECT_PARTNER_ID,
)
HUBJECT_PARTNER_ID = _first_non_empty_str(
    hubject_client_cfg.get("partner_id"),
    DEFAULT_HUBJECT_PARTNER_ID,
)

HUBJECT_API_VERIFY: bool | str = True
if HUBJECT_API_CA_BUNDLE:
    HUBJECT_API_VERIFY = str(HUBJECT_API_CA_BUNDLE)
elif hubject_client_cfg.get("ca_bundle"):
    logger.warning(
        "Hubject CA bundle configured but could not be built; falling back to system CAs"
    )

HUBJECT_API_CERT_PAIR = _prepare_hubject_cert_pair(HUBJECT_API_CERT, HUBJECT_API_KEY)

if HUBJECT_API_BASE_URL:
    HUBJECT_API_CLIENT: HubjectApiClient | None = HubjectApiClient(
        HUBJECT_API_BASE_URL,
        timeout=HUBJECT_API_TIMEOUT,
        authorization=HUBJECT_API_AUTH,
        verify=HUBJECT_API_VERIFY,
        cert=HUBJECT_API_CERT_PAIR,
        operator_id=HUBJECT_OPERATOR_ID,
        partner_id=HUBJECT_PARTNER_ID,
    )
else:
    HUBJECT_API_CLIENT = None


# Optional fallback to the asynchronous helper from the OCPP server.
try:  # pragma: no cover - import failure only occurs in unusual deployments
    from pipelet_ocpp_server import check_rfid_authorization as _check_rfid_authorization
except Exception:  # pragma: no cover - optional dependency at import time
    _check_rfid_authorization = None


REQUIRE_CLIENT_CERT = bool(hubject_api_cfg.get("require_client_certificate", True))
CLIENT_VERIFY_HEADER = str(hubject_api_cfg.get("client_verify_header", "X-SSL-Client-Verify"))

SSL_CERTFILE = _resolve_path(hubject_api_cfg.get("certfile"))
SSL_KEYFILE = _resolve_path(hubject_api_cfg.get("keyfile"))
SSL_CA_BUNDLE = _resolve_path(hubject_api_cfg.get("ca_bundle"))

LISTEN_HOST = str(hubject_api_cfg.get("host", "0.0.0.0"))
LISTEN_PORT = int(hubject_api_cfg.get("port", 9443))


def _describe_tls_material(path: Path | None) -> str:
    if path is None:
        return "not configured"
    try:
        if path.exists():
            return f"{path} (exists)"
    except OSError as exc:
        return f"{path} (error checking existence: {exc})"
    return f"{path} (missing)"


def _log_startup_configuration() -> None:
    logger.info(
        "TLS material - cert: %s, key: %s, client CA: %s",
        _describe_tls_material(SSL_CERTFILE),
        _describe_tls_material(SSL_KEYFILE),
        _describe_tls_material(SSL_CA_BUNDLE),
    )
    hubject_url = str(hubject_client_cfg.get("api_base_url", ""))
    if hubject_url:
        logger.info("Configured Hubject API base URL: %s", hubject_url)
    else:
        logger.info("No Hubject API base URL configured")
    logger.info(
        "OCPP API base URL: %s (timeout=%.1fs, verify_tls=%s)",
        OCPP_API_BASE_URL,
        OCPP_API_TIMEOUT,
        OCPP_API_VERIFY,
    )

_EVENT_TABLE_INITIALISED = False
_EVENT_TABLE_LOCK = threading.Lock()
_API_LOG_TABLE_INITIALISED = False
_API_LOG_TABLE_LOCK = threading.Lock()


def get_db_connection():
    return pymysql.connect(
        host=MYSQL_CONFIG.get("host"),
        user=MYSQL_CONFIG.get("user"),
        password=MYSQL_CONFIG.get("password"),
        db=MYSQL_CONFIG.get("db"),
        charset=MYSQL_CONFIG.get("charset", "utf8mb4"),
    )


def _ensure_event_table(conn) -> None:
    global _EVENT_TABLE_INITIALISED
    if _EVENT_TABLE_INITIALISED:
        return
    with _EVENT_TABLE_LOCK:
        if _EVENT_TABLE_INITIALISED:
            return
        try:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    CREATE TABLE IF NOT EXISTS op_hubject_audit (
                        id INT AUTO_INCREMENT PRIMARY KEY,
                        event_type VARCHAR(64) NOT NULL,
                        payload JSON,
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                    )
                    """
                )
            conn.commit()
        except Exception:  # pragma: no cover - database optional in tests
            logging.getLogger(__name__).exception(
                "Failed to ensure Hubject audit table"
            )
        else:
            _EVENT_TABLE_INITIALISED = True


def _ensure_api_log_table(conn) -> None:
    global _API_LOG_TABLE_INITIALISED
    if _API_LOG_TABLE_INITIALISED:
        return
    with _API_LOG_TABLE_LOCK:
        if _API_LOG_TABLE_INITIALISED:
            return
        try:
            with conn.cursor() as cur:
                cur.execute(
                    """
                    CREATE TABLE IF NOT EXISTS op_hubject_api_log (
                        id INT AUTO_INCREMENT PRIMARY KEY,
                        endpoint VARCHAR(255) NOT NULL,
                        remote_ip VARCHAR(45),
                        payload LONGTEXT,
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                    )
                    """
                )
            conn.commit()
        except Exception:  # pragma: no cover - database optional in tests
            logging.getLogger(__name__).exception(
                "Failed to ensure Hubject API log table"
            )
        else:
            _API_LOG_TABLE_INITIALISED = True


def persist_event(event_type: str, payload: Mapping[str, Any]) -> None:
    try:
        conn = get_db_connection()
    except Exception:  # pragma: no cover - database optional in tests
        logging.getLogger(__name__).debug(
            "MySQL unavailable - skipping audit entry", exc_info=True
        )
        return

    try:
        _ensure_event_table(conn)
        with conn.cursor() as cur:
            cur.execute(
                "INSERT INTO op_hubject_audit (event_type, payload) VALUES (%s, %s)",
                (event_type, json.dumps(payload, ensure_ascii=False)),
            )
        conn.commit()
    except Exception:  # pragma: no cover - database optional in tests
        logging.getLogger(__name__).exception(
            "Failed to persist Hubject audit entry"
        )
    finally:
        try:
            conn.close()
        except Exception:
            pass


def _serialize_payload(payload: Any) -> str:
    if isinstance(payload, Mapping):
        try:
            return json.dumps(payload, ensure_ascii=False)
        except (TypeError, ValueError):
            pass
    if isinstance(payload, (list, tuple)):
        try:
            return json.dumps(payload, ensure_ascii=False)
        except (TypeError, ValueError):
            pass
    if isinstance(payload, (bytes, bytearray)):
        return payload.decode("utf-8", errors="replace")
    if payload is None:
        return ""
    return str(payload)


def log_api_call(endpoint: str, remote_ip: str | None, payload: Any) -> None:
    try:
        conn = get_db_connection()
    except Exception:  # pragma: no cover - database optional in tests
        logging.getLogger(__name__).debug(
            "MySQL unavailable - skipping API call log", exc_info=True
        )
        return

    payload_str = _serialize_payload(payload)
    try:
        _ensure_api_log_table(conn)
        with conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO op_hubject_api_log (endpoint, remote_ip, payload)
                VALUES (%s, %s, %s)
                """,
                (endpoint, remote_ip, payload_str),
            )
        conn.commit()
    except Exception:  # pragma: no cover - database optional in tests
        logging.getLogger(__name__).exception("Failed to persist Hubject API log entry")
    finally:
        try:
            conn.close()
        except Exception:
            pass


def _is_chargepoint_oicp_enabled(chargepoint_id: str | None) -> bool | None:
    if not chargepoint_id:
        return None

    try:
        conn = get_db_connection()
    except Exception:
        logger.debug(
            "MySQL unavailable - cannot load OICP flag for %s", chargepoint_id, exc_info=True
        )
        return None

    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                SELECT enabled
                FROM op_server_oicp_enable
                WHERE chargepoint_id=%s
                LIMIT 1
                """,
                (chargepoint_id,),
            )
            row = cur.fetchone()
        if isinstance(row, Mapping):
            value = row.get("enabled")
        else:
            value = row[0] if row else None
        return bool(value)
    except Exception:
        logger.warning(
            "Failed to read OICP enable flag for %s", chargepoint_id, exc_info=True
        )
        return None
    finally:
        try:
            conn.close()
        except Exception:
            pass


app = Flask(__name__)


def _now() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def _get_remote_ip() -> str | None:
    forwarded_for = request.headers.get("X-Forwarded-For")
    if forwarded_for:
        candidate = forwarded_for.split(",")[0].strip()
        if candidate:
            return candidate
    remote_addr = request.remote_addr
    return remote_addr.strip() if isinstance(remote_addr, str) else remote_addr


def _build_status_code(code: str, description: str, *, additional: str | None = None) -> dict[str, Any]:
    return {
        "Code": code,
        "Description": description,
        "AdditionalInfo": additional,
    }


def _map_authorization_status(status: str | None) -> tuple[str, dict[str, Any]]:
    if not status:
        return "NotAuthorized", _build_status_code("100", "No status returned")
    normalized = status.strip().lower()
    if normalized in {"accepted", "approved", "authorized", "authorised", "success"}:
        return "Authorized", _build_status_code("000", "Authorization granted")
    if normalized in {"blocked", "denied", "invalid", "rejected"}:
        return "NotAuthorized", _build_status_code("101", "Authorization denied")
    return "NotAuthorized", _build_status_code(
        "102", f"Unknown status '{status}'"
    )


def _extract_identification(data: Mapping[str, Any]) -> tuple[dict[str, Any], str | None]:
    identification = data.get("Identification")
    if isinstance(identification, Mapping):
        if "RFIDMifareFamilyIdentification" in identification:
            mifare = identification["RFIDMifareFamilyIdentification"]
            if isinstance(mifare, Mapping):
                uid = mifare.get("UID")
                if uid:
                    return dict(identification), str(uid)
        elif "PlugAndChargeIdentification" in identification:
            plug = identification["PlugAndChargeIdentification"]
            if isinstance(plug, Mapping):
                evcc_id = plug.get("EvccID") or plug.get("EVCCID")
                if evcc_id:
                    return dict(identification), str(evcc_id)
        elif "RemoteIdentification" in identification:
            remote = identification["RemoteIdentification"]
            if isinstance(remote, Mapping):
                evco_id = remote.get("EvcoID") or remote.get("EVCOID")
                if evco_id:
                    return dict(identification), str(evco_id)
        else:
            for key, value in identification.items():
                if isinstance(value, Mapping) and "UID" in value:
                    uid = value.get("UID")
                    if uid:
                        return dict(identification), str(uid)
    rfid = data.get("RFID") or data.get("rfid")
    if rfid:
        return {"RFIDMifareFamilyIdentification": {"UID": str(rfid)}}, str(rfid)
    return {}, None


def _run_check_rfid(evse_id: str, token: str) -> str | None:
    if _check_rfid_authorization is None:
        return None
    try:
        import asyncio

        return asyncio.run(_check_rfid_authorization(evse_id, token))
    except RuntimeError:
        loop = asyncio.new_event_loop()
        try:
            return loop.run_until_complete(_check_rfid_authorization(evse_id, token))
        finally:
            loop.close()
    except Exception:
        logging.getLogger(__name__).exception("RFID authorization check failed")
        return None


def _require_client_certificate() -> None:
    if not REQUIRE_CLIENT_CERT:
        return

    if request.path in {"/robots.txt", "/alive"}:
        return

    environ = request.environ
    header_value = request.headers.get(CLIENT_VERIFY_HEADER)
    candidates = [
        environ.get("SSL_CLIENT_VERIFY"),
        environ.get("HTTP_SSL_CLIENT_VERIFY"),
        header_value,
    ]
    for candidate in candidates:
        if not candidate:
            continue
        normalized = str(candidate).strip().lower()
        if normalized in {"success", "1", "true", "ok", "verified"}:
            return
    logging.getLogger(__name__).warning(
        "Client certificate validation failed: %s", candidates
    )
    abort(Response("Client certificate required", status=403))


@app.before_request
def _log_incoming_request() -> None:
    try:
        payload = request.get_json(force=False, silent=True)
    except Exception:
        payload = None
    if payload is None:
        try:
            payload = request.get_data(cache=True, as_text=True)
        except Exception:
            payload = ""
    log_api_call(request.path, _get_remote_ip(), payload)


@app.before_request
def _enforce_client_cert() -> None:
    _require_client_certificate()


def _base_authorization_response(
    *,
    status: str,
    status_code: Mapping[str, Any],
    evse_id: str,
    cpo_session_id: str,
    emp_session_id: str,
    identification: Mapping[str, Any],
) -> dict[str, Any]:
    return {
        "AuthorizationStatus": status,
        "StatusCode": dict(status_code),
        "CPOPartnerSessionID": cpo_session_id,
        "EMPPartnerSessionID": emp_session_id,
        "EVSEId": evse_id,
        "Identification": identification,
        "Timestamp": _now(),
    }


@app.post("/Authorization/AuthorizeStart")
def authorize_start():
    body = request.get_json(force=True, silent=True) or {}
    evse_id = _extract_evse_id(body)
    if not evse_id:
        return jsonify({"StatusCode": _build_status_code("200", "EVSEId missing")}), 400

    identification, token = _extract_identification(body)
    if token is None:
        return jsonify({"StatusCode": _build_status_code("201", "Identification missing")}), 400

    operator_id = (
        str(
            body.get("OperatorID")
            or body.get("operator_id")
            or HUBJECT_OPERATOR_ID
            or ""
        ).strip()
    )
    provider_id = (
        str(
            body.get("ProviderID")
            or body.get("provider_id")
            or HUBJECT_PARTNER_ID
            or ""
        ).strip()
    )
    cpo_session_id = str(body.get("CPOPartnerSessionID") or body.get("SessionID") or token)
    emp_session_id = str(body.get("EMPPartnerSessionID") or token)

    persist_event("authorize_start_request", body)

    hubject_request: dict[str, Any] = dict(body)
    hubject_request.setdefault("EVSEId", evse_id)
    hubject_request.setdefault("Identification", identification)
    hubject_request.setdefault("CPOPartnerSessionID", cpo_session_id)
    if emp_session_id:
        hubject_request.setdefault("EMPPartnerSessionID", emp_session_id)
    if token:
        hubject_request.setdefault("RFID", token)

    resolved_operator_id = operator_id or HUBJECT_OPERATOR_ID or HUBJECT_PARTNER_ID
    if resolved_operator_id:
        hubject_request["OperatorID"] = resolved_operator_id

    resolved_provider_id = provider_id or HUBJECT_PARTNER_ID or resolved_operator_id
    if resolved_provider_id:
        hubject_request.setdefault("ProviderID", resolved_provider_id)

    logger.debug(
        "Prepared Hubject authorize-start request (OperatorID=%s, ProviderID=%s): %s",
        hubject_request.get("OperatorID"),
        hubject_request.get("ProviderID"),
        _serialize_payload(hubject_request),
    )

    hubject_response: Mapping[str, Any] | None = None
    hubject_status: str | None = None
    hubject_status_code: Mapping[str, Any] | None = None
    hubject_error: str | None = None

    if HUBJECT_API_CLIENT is not None:
        try:
            hubject_response = HUBJECT_API_CLIENT.authorize_start_evse(hubject_request)
        except HubjectApiError as exc:
            logger.warning(
                "Hubject authorize-start failed for %s: %s",
                evse_id,
                exc,
            )
            hubject_error = str(exc)
        else:
            logger.debug(
                "Hubject authorize-start response for %s: %s",
                evse_id,
                _serialize_payload(hubject_response),
            )
    else:
        logger.warning("Hubject API client unavailable for authorize-start requests")
        hubject_error = "Hubject API client unavailable"

    if isinstance(hubject_response, Mapping):
        for key in ("AuthorizationStatus", "authorizationStatus", "Status", "status"):
            value = hubject_response.get(key)
            if isinstance(value, str) and value.strip():
                hubject_status = value.strip()
                break
        status_code_payload = hubject_response.get("StatusCode") or hubject_response.get("statusCode")
        if isinstance(status_code_payload, Mapping):
            hubject_status_code = dict(status_code_payload)
        cpo_session_id = str(
            hubject_response.get("CPOPartnerSessionID")
            or hubject_response.get("SessionID")
            or cpo_session_id
        )
        emp_session_id = str(
            hubject_response.get("EMPPartnerSessionID") or emp_session_id
        )
        identification = (
            hubject_response.get("Identification")
            if isinstance(hubject_response.get("Identification"), Mapping)
            else identification
        )
    else:
        hubject_response = None

    if hubject_status is None:
        fallback = _run_check_rfid(evse_id, token)
        if fallback:
            hubject_status = fallback

    status, status_code = _map_authorization_status(hubject_status)
    if hubject_status_code:
        status_code = dict(hubject_status_code)
    elif hubject_error and status != "Authorized":
        status_code = _build_status_code(
            "500",
            "Authorization failed",
            additional=hubject_error,
        )

    response_payload = _base_authorization_response(
        status=status,
        status_code=status_code,
        evse_id=evse_id,
        cpo_session_id=cpo_session_id,
        emp_session_id=emp_session_id,
        identification=identification,
    )

    auth_start = None
    if isinstance(hubject_response, Mapping):
        auth_start_candidate = hubject_response.get("AuthorizationStart")
        if isinstance(auth_start_candidate, Mapping):
            auth_start = dict(auth_start_candidate)
        operator_id_value = hubject_response.get("OperatorID")
        if operator_id_value and "OperatorID" not in response_payload:
            response_payload["OperatorID"] = operator_id_value
        provider_id_value = hubject_response.get("ProviderID")
        if provider_id_value and "ProviderID" not in response_payload:
            response_payload["ProviderID"] = provider_id_value
        session_id_value = hubject_response.get("SessionID")
        if session_id_value and "SessionID" not in response_payload:
            response_payload["SessionID"] = session_id_value

    if not isinstance(auth_start, Mapping):
        auth_start = {
            "SessionID": cpo_session_id,
            "Timestamp": response_payload["Timestamp"],
        }
    else:
        auth_start.setdefault("SessionID", cpo_session_id)
        auth_start.setdefault("Timestamp", response_payload["Timestamp"])

    response_payload["AuthorizationStart"] = auth_start
    if hubject_response is not None:
        response_payload["HubjectResponse"] = hubject_response
    if hubject_error and status != "Authorized":
        response_payload.setdefault("Error", hubject_error)

    persist_event("authorize_start_response", response_payload)

    log_status = "Authorized" if status == "Authorized" else "un-authorized"
    logger.debug("Return to OCPP Server: %s", log_status)

    http_status = 200 if status == "Authorized" else 401
    return jsonify(response_payload), http_status


@app.post("/api/oicp/charging/v21/providers/<operator_id>/authorize-remote/start")
@app.post("/api/oicp/charging/v21/providers/<operator_id>/authorize-remote/Start")
def authorize_remote_start(
    operator_id: str, body: Mapping[str, Any] | None = None
):
    if body is None:
        body = request.get_json(force=True, silent=True) or {}
    if not isinstance(body, dict):
        body = dict(body)

    operator_id = str(operator_id or "").strip()

    serialized_payload = _serialize_payload(body)
    logger.debug(
        "authorize_remote_start payload for %s: %s",
        operator_id,
        serialized_payload,
    )

    evse_id = _extract_evse_id(body)
    if not evse_id:
        status_code = _build_status_code("200", "EVSEId missing")
        response_payload = {
            "StatusCode": status_code,
            "Timestamp": _now(),
        }
        logger.warning(
            "Remote start rejected - EVSEId missing (operator=%s, payload=%s)",
            operator_id,
            serialized_payload,
        )
        return jsonify(response_payload), 400

    identification, token = _extract_identification(body)
    provider_id = str(body.get("ProviderID") or body.get("provider_id") or "").strip()
    cpo_session_id = str(
        body.get("CPOPartnerSessionID")
        or body.get("SessionID")
        or token
        or f"remote-{_now()}"
    )
    emp_session_id = str(body.get("EMPPartnerSessionID") or token or "")

    connector_value = None
    for key in ("ConnectorID", "ConnectorId", "connector_id", "connectorId"):
        if key in body:
            connector_value = body.get(key)
            break

    connector_id: int | None = None
    if connector_value not in (None, ""):
        try:
            connector_id = int(str(connector_value).strip())
        except (TypeError, ValueError):
            status_code = _build_status_code(
                "202",
                f"Invalid ConnectorID '{connector_value}'",
                additional="ConnectorID must be an integer",
            )
            response_payload = _base_authorization_response(
                status="NotAuthorized",
                status_code=status_code,
                evse_id=evse_id,
                cpo_session_id=cpo_session_id,
                emp_session_id=emp_session_id,
                identification=identification,
            )
            response_payload["ProviderID"] = provider_id
            response_payload["OperatorID"] = operator_id
            persist_event("authorize_remote_start_response", response_payload)
            logger.warning(
                "Remote start rejected - invalid ConnectorID '%s' for %s (operator=%s)",
                connector_value,
                evse_id,
                operator_id,
            )
            return jsonify(response_payload), 400

    logger.info(
        "Remote start request for %s (operator=%s, provider=%s)",
        evse_id,
        operator_id,
        provider_id or "-",
    )

    persist_event("authorize_remote_start_request", body)

    chargepoint_hint = body.get("chargepoint_id") or body.get("station_id")
    enabled_flag = (
        _is_chargepoint_oicp_enabled(str(chargepoint_hint)) if chargepoint_hint else None
    )
    if chargepoint_hint and enabled_flag is False:
        status_code = _build_status_code(
            "300", f"Chargepoint {chargepoint_hint} is not enabled for OICP"
        )
        response_payload = _base_authorization_response(
            status="NotAuthorized",
            status_code=status_code,
            evse_id=evse_id,
            cpo_session_id=cpo_session_id,
            emp_session_id=emp_session_id,
            identification=identification,
        )
        response_payload["ProviderID"] = provider_id
        response_payload["OperatorID"] = operator_id
        persist_event("authorize_remote_start_response", response_payload)
        logger.info(
            "Remote start rejected - OICP disabled for chargepoint %s (operator=%s)",
            chargepoint_hint,
            operator_id,
        )
        return jsonify(response_payload), 401
    if chargepoint_hint and enabled_flag is None:
        logger.debug(
            "Skipping OICP enablement check for chargepoint %s due to database unavailability",
            chargepoint_hint,
        )

    charging_profile = body.get("ChargingProfile") or body.get("charging_profile")
    ocpp_payload: dict[str, Any] = {
        "evse_id": evse_id,
        "session_id": cpo_session_id,
        "operator_id": operator_id,
        "identification": identification,
        "raw_request": body,
    }
    if provider_id:
        ocpp_payload["provider_id"] = provider_id
    if emp_session_id:
        ocpp_payload["emp_session_id"] = emp_session_id
    if token:
        ocpp_payload["token"] = token
    if connector_id is not None:
        ocpp_payload["connector_id"] = connector_id
    if isinstance(charging_profile, Mapping):
        ocpp_payload["charging_profile"] = charging_profile

    try:
        ocpp_response = OCPP_CLIENT.remote_start_transaction(ocpp_payload)
        ocpp_status = ocpp_response.get("status") or ocpp_response.get("AuthorizationStatus")
    except OCPPApiError as exc:
        logging.getLogger(__name__).warning("OCPP remote-start failed: %s", exc)
        status_code = _build_status_code(
            "500", "Remote start failed", additional=str(exc)
        )
        response_payload = _base_authorization_response(
            status="NotAuthorized",
            status_code=status_code,
            evse_id=evse_id,
            cpo_session_id=cpo_session_id,
            emp_session_id=emp_session_id,
            identification=identification,
        )
        response_payload["ProviderID"] = provider_id
        response_payload["OperatorID"] = operator_id
        response_payload["OcppResponse"] = {"error": str(exc)}
        persist_event("authorize_remote_start_response", response_payload)
        return jsonify(response_payload), 502

    status, status_code = _map_authorization_status(ocpp_status)

    response_payload = _base_authorization_response(
        status=status,
        status_code=status_code,
        evse_id=evse_id,
        cpo_session_id=cpo_session_id,
        emp_session_id=emp_session_id,
        identification=identification,
    )
    response_payload["ProviderID"] = provider_id
    response_payload["OperatorID"] = operator_id
    response_payload["OcppResponse"] = ocpp_response
    if connector_id is not None:
        response_payload["ConnectorID"] = connector_id

    http_status = 200 if status == "Authorized" else 401

    persist_event("authorize_remote_start_response", response_payload)
    logger.info(
        "Remote start response for %s (status=%s): %s",
        evse_id,
        status,
        _serialize_payload(response_payload),
    )

    return jsonify(response_payload), http_status


@app.post("/api/oicp/2.3/authorize-remote-start-evse")
def authorize_remote_start_v23():
    body = request.get_json(force=True, silent=True) or {}
    if not isinstance(body, dict):
        body = dict(body)
    operator_id = str(
        body.get("OperatorID")
        or body.get("operator_id")
        or HUBJECT_OPERATOR_ID
        or ""
    ).strip()
    return authorize_remote_start(operator_id, body=body)


@app.post("/api/oicp/charging/v21/providers/<operator_id>/authorize-remote/stop")
@app.post("/api/oicp/charging/v21/providers/<operator_id>/authorize-remote/Stop")
def authorize_remote_stop(
    operator_id: str, body: Mapping[str, Any] | None = None
):
    if body is None:
        body = request.get_json(force=True, silent=True) or {}
    if not isinstance(body, dict):
        body = dict(body)

    operator_id = str(operator_id or "").strip()

    serialized_payload = _serialize_payload(body)
    logger.debug(
        "authorize_remote_stop payload for %s: %s",
        operator_id,
        serialized_payload,
    )

    evse_id = _extract_evse_id(body)
    if not evse_id:
        status_code = _build_status_code("200", "EVSEId missing")
        response_payload = {
            "StatusCode": status_code,
            "Timestamp": _now(),
        }
        logger.warning(
            "Remote stop rejected - EVSEId missing (operator=%s, payload=%s)",
            operator_id,
            serialized_payload,
        )
        return jsonify(response_payload), 400

    identification, token = _extract_identification(body)
    provider_id = str(body.get("ProviderID") or body.get("provider_id") or "").strip()
    cpo_session_id = str(
        body.get("CPOPartnerSessionID")
        or body.get("SessionID")
        or body.get("SessionId")
        or token
        or f"remote-{_now()}"
    )
    emp_session_id = str(body.get("EMPPartnerSessionID") or token or "")

    connector_value = None
    for key in ("ConnectorID", "ConnectorId", "connector_id", "connectorId"):
        if key in body:
            connector_value = body.get(key)
            break

    connector_id: int | None = None
    if connector_value not in (None, ""):
        try:
            connector_id = int(str(connector_value).strip())
        except (TypeError, ValueError):
            status_code = _build_status_code(
                "202",
                f"Invalid ConnectorID '{connector_value}'",
                additional="ConnectorID must be an integer",
            )
            response_payload = _base_authorization_response(
                status="NotAuthorized",
                status_code=status_code,
                evse_id=evse_id,
                cpo_session_id=cpo_session_id,
                emp_session_id=emp_session_id,
                identification=identification,
            )
            response_payload["ProviderID"] = provider_id
            response_payload["OperatorID"] = operator_id
            persist_event("authorize_remote_stop_response", response_payload)
            logger.warning(
                "Remote stop rejected - invalid ConnectorID '%s' for %s (operator=%s)",
                connector_value,
                evse_id,
                operator_id,
            )
            return jsonify(response_payload), 400

    transaction_value = None
    for key in ("TransactionID", "TransactionId", "transaction_id"):
        if key in body:
            transaction_value = body.get(key)
            break

    transaction_id: int | None = None
    if transaction_value not in (None, ""):
        try:
            transaction_id = int(str(transaction_value).strip())
        except (TypeError, ValueError):
            status_code = _build_status_code(
                "202",
                f"Invalid TransactionID '{transaction_value}'",
                additional="TransactionID must be an integer",
            )
            response_payload = _base_authorization_response(
                status="NotAuthorized",
                status_code=status_code,
                evse_id=evse_id,
                cpo_session_id=cpo_session_id,
                emp_session_id=emp_session_id,
                identification=identification,
            )
            response_payload["ProviderID"] = provider_id
            response_payload["OperatorID"] = operator_id
            persist_event("authorize_remote_stop_response", response_payload)
            logger.warning(
                "Remote stop rejected - invalid TransactionID '%s' for %s (operator=%s)",
                transaction_value,
                evse_id,
                operator_id,
            )
            return jsonify(response_payload), 400

    logger.info(
        "Remote stop request for %s (operator=%s, provider=%s, transaction=%s, connector=%s): payload=%s",
        evse_id,
        operator_id,
        provider_id or "-",
        transaction_id if transaction_id is not None else "-",
        connector_id if connector_id is not None else "-",
        serialized_payload,
    )

    persist_event("authorize_remote_stop_request", body)

    hubject_request: dict[str, Any] = dict(body)
    hubject_request.setdefault("EVSEId", evse_id)
    hubject_request.setdefault("Identification", identification)
    hubject_request.setdefault("CPOPartnerSessionID", cpo_session_id)
    if emp_session_id:
        hubject_request.setdefault("EMPPartnerSessionID", emp_session_id)
    if token:
        hubject_request.setdefault("RFID", token)
    if operator_id and not hubject_request.get("OperatorID"):
        hubject_request["OperatorID"] = operator_id
    elif HUBJECT_OPERATOR_ID and not hubject_request.get("OperatorID"):
        hubject_request["OperatorID"] = HUBJECT_OPERATOR_ID
    if provider_id and not hubject_request.get("ProviderID"):
        hubject_request["ProviderID"] = provider_id
    if connector_id is not None:
        hubject_request.setdefault("ConnectorID", connector_id)
    if transaction_id is not None:
        hubject_request.setdefault("TransactionID", transaction_id)

    hubject_response: Mapping[str, Any] | None = None
    hubject_status: str | None = None
    hubject_status_code: Mapping[str, Any] | None = None
    hubject_error: str | None = None

    if HUBJECT_API_CLIENT is not None:
        try:
            hubject_response = HUBJECT_API_CLIENT.authorize_stop_evse(hubject_request)
        except HubjectApiError as exc:
            logger.warning(
                "Hubject authorize-stop failed for %s: %s",
                evse_id,
                exc,
            )
            hubject_error = str(exc)
    else:
        logger.warning("Hubject API client unavailable for authorize-stop requests")
        hubject_error = "Hubject API client unavailable"

    if isinstance(hubject_response, Mapping):
        for key in ("AuthorizationStatus", "authorizationStatus", "Status", "status"):
            value = hubject_response.get(key)
            if isinstance(value, str) and value.strip():
                hubject_status = value.strip()
                break
        status_code_payload = hubject_response.get("StatusCode") or hubject_response.get("statusCode")
        if isinstance(status_code_payload, Mapping):
            hubject_status_code = dict(status_code_payload)
        cpo_session_id = str(
            hubject_response.get("CPOPartnerSessionID")
            or hubject_response.get("SessionID")
            or cpo_session_id
        )
        emp_session_id = str(
            hubject_response.get("EMPPartnerSessionID") or emp_session_id
        )
        identification = (
            hubject_response.get("Identification")
            if isinstance(hubject_response.get("Identification"), Mapping)
            else identification
        )
        operator_id = (
            hubject_response.get("OperatorID") or operator_id
        )
        provider_id = (
            hubject_response.get("ProviderID") or provider_id
        )
        transaction_response = hubject_response.get("TransactionID")
        if transaction_response is not None and transaction_id is None:
            try:
                transaction_id = int(transaction_response)
            except (TypeError, ValueError):
                transaction_id = transaction_response
    else:
        hubject_response = None

    ocpp_payload: dict[str, Any] = {
        "evse_id": evse_id,
        "session_id": cpo_session_id,
        "operator_id": operator_id,
        "raw_request": body,
    }
    if provider_id:
        ocpp_payload["provider_id"] = provider_id
    if emp_session_id:
        ocpp_payload["emp_session_id"] = emp_session_id
    if token:
        ocpp_payload["token"] = token
    if connector_id is not None:
        ocpp_payload["connector_id"] = connector_id
    if transaction_id is not None:
        ocpp_payload["transaction_id"] = transaction_id

    ocpp_response: Mapping[str, Any] | None = None
    ocpp_status: str | None = None

    try:
        ocpp_result = OCPP_CLIENT.remote_stop_transaction(ocpp_payload)
    except OCPPApiError as exc:
        logging.getLogger(__name__).warning("OCPP remote-stop failed: %s", exc)
        status_code = _build_status_code(
            "500", "Remote stop failed", additional=str(exc)
        )
        response_payload = _base_authorization_response(
            status="NotAuthorized",
            status_code=status_code,
            evse_id=evse_id,
            cpo_session_id=cpo_session_id,
            emp_session_id=emp_session_id,
            identification=identification,
        )
        if provider_id:
            response_payload["ProviderID"] = provider_id
        if operator_id:
            response_payload["OperatorID"] = operator_id
        if connector_id is not None:
            response_payload["ConnectorID"] = connector_id
        if transaction_id is not None:
            response_payload["TransactionID"] = transaction_id
        response_payload["OcppResponse"] = {"error": str(exc)}
        if hubject_response is not None:
            response_payload["HubjectResponse"] = hubject_response
        persist_event("authorize_remote_stop_response", response_payload)
        logger.info(
            "Remote stop response for %s (status=%s): %s",
            evse_id,
            "NotAuthorized",
            _serialize_payload(response_payload),
        )
        return jsonify(response_payload), 502

    if isinstance(ocpp_result, Mapping):
        ocpp_response = dict(ocpp_result)
        ocpp_status = (
            ocpp_response.get("status")
            or ocpp_response.get("AuthorizationStatus")
            or ocpp_response.get("authorizationStatus")
        )
        if transaction_id is None:
            transaction_candidate = (
                ocpp_response.get("transaction_id")
                or ocpp_response.get("TransactionID")
                or ocpp_response.get("transactionId")
            )
            if transaction_candidate is not None:
                try:
                    transaction_id = int(transaction_candidate)
                except (TypeError, ValueError):
                    transaction_id = transaction_candidate
        if connector_id is None:
            connector_candidate = (
                ocpp_response.get("connector_id")
                or ocpp_response.get("ConnectorID")
                or ocpp_response.get("connectorId")
            )
            if connector_candidate not in (None, ""):
                try:
                    connector_id = int(connector_candidate)
                except (TypeError, ValueError):
                    connector_id = connector_candidate  # type: ignore[assignment]
    else:
        ocpp_status = str(ocpp_result)
        ocpp_response = {"raw": ocpp_result}

    status_source = ocpp_status or hubject_status
    status, status_code = _map_authorization_status(status_source)
    if hubject_status_code:
        status_code = dict(hubject_status_code)
    elif hubject_error and status != "Authorized":
        status_code = _build_status_code(
            "500",
            "Authorization failed",
            additional=hubject_error,
        )

    response_payload = _base_authorization_response(
        status=status,
        status_code=status_code,
        evse_id=evse_id,
        cpo_session_id=cpo_session_id,
        emp_session_id=emp_session_id,
        identification=identification,
    )
    if provider_id:
        response_payload["ProviderID"] = provider_id
    if operator_id:
        response_payload["OperatorID"] = operator_id
    if connector_id is not None:
        response_payload["ConnectorID"] = connector_id
    if transaction_id is not None:
        response_payload["TransactionID"] = transaction_id

    if ocpp_response is not None:
        response_payload["OcppResponse"] = ocpp_response
    auth_stop = None
    if isinstance(hubject_response, Mapping):
        auth_stop_candidate = hubject_response.get("AuthorizationStop")
        if isinstance(auth_stop_candidate, Mapping):
            auth_stop = dict(auth_stop_candidate)

    if not isinstance(auth_stop, Mapping):
        auth_stop = {
            "SessionID": cpo_session_id,
            "Timestamp": response_payload["Timestamp"],
        }
    else:
        auth_stop.setdefault("SessionID", cpo_session_id)
        auth_stop.setdefault("Timestamp", response_payload["Timestamp"])

    response_payload["AuthorizationStop"] = auth_stop
    if hubject_response is not None:
        response_payload["HubjectResponse"] = hubject_response
    if hubject_error and status != "Authorized":
        response_payload.setdefault("Error", hubject_error)

    http_status = 200 if status == "Authorized" else 401

    persist_event("authorize_remote_stop_response", response_payload)
    logger.info(
        "Remote stop response for %s (status=%s): %s",
        evse_id,
        status,
        _serialize_payload(response_payload),
    )

    return jsonify(response_payload), http_status


@app.post("/api/oicp/2.3/authorize-remote-stop-evse")
def authorize_remote_stop_v23():
    body = request.get_json(force=True, silent=True) or {}
    if not isinstance(body, dict):
        body = dict(body)
    operator_id = str(
        body.get("OperatorID")
        or body.get("operator_id")
        or HUBJECT_OPERATOR_ID
        or ""
    ).strip()
    return authorize_remote_stop(operator_id, body=body)


@app.post("/Authorization/AuthorizeStop")
def authorize_stop():
    body = request.get_json(force=True, silent=True) or {}
    evse_id = _extract_evse_id(body)
    if not evse_id:
        return jsonify({"StatusCode": _build_status_code("200", "EVSEId missing")}), 400

    identification, token = _extract_identification(body)
    if token is None:
        return jsonify({"StatusCode": _build_status_code("201", "Identification missing")}), 400

    cpo_session_id = str(body.get("CPOPartnerSessionID") or body.get("SessionID") or token)
    emp_session_id = str(body.get("EMPPartnerSessionID") or token)

    persist_event("authorize_stop_request", body)

    operator_id = str(body.get("OperatorID") or body.get("operator_id") or "").strip()
    provider_id = (
        str(
            body.get("ProviderID")
            or body.get("provider_id")
            or HUBJECT_PARTNER_ID
            or ""
        ).strip()
    )

    hubject_request: dict[str, Any] = dict(body)
    hubject_request.setdefault("EVSEId", evse_id)
    hubject_request.setdefault("Identification", identification)
    hubject_request.setdefault("CPOPartnerSessionID", cpo_session_id)
    if emp_session_id:
        hubject_request.setdefault("EMPPartnerSessionID", emp_session_id)
    if token:
        hubject_request.setdefault("RFID", token)
    if operator_id:
        hubject_request.setdefault("OperatorID", operator_id)
    elif HUBJECT_OPERATOR_ID and not hubject_request.get("OperatorID"):
        hubject_request["OperatorID"] = HUBJECT_OPERATOR_ID
    resolved_provider_id = provider_id or HUBJECT_PARTNER_ID or operator_id
    if resolved_provider_id:
        hubject_request.setdefault("ProviderID", resolved_provider_id)

    logger.debug(
        "Prepared Hubject authorize-stop request (OperatorID=%s, ProviderID=%s): %s",
        hubject_request.get("OperatorID"),
        hubject_request.get("ProviderID"),
        _serialize_payload(hubject_request),
    )

    hubject_response: Mapping[str, Any] | None = None
    hubject_status: str | None = None
    hubject_status_code: Mapping[str, Any] | None = None
    hubject_error: str | None = None

    if HUBJECT_API_CLIENT is not None:
        try:
            hubject_response = HUBJECT_API_CLIENT.authorize_stop_evse(hubject_request)
        except HubjectApiError as exc:
            logger.warning(
                "Hubject authorize-stop failed for %s: %s",
                evse_id,
                exc,
            )
            hubject_error = str(exc)
        else:
            logger.debug(
                "Hubject authorize-stop response for %s: %s",
                evse_id,
                _serialize_payload(hubject_response),
            )
    else:
        logger.warning("Hubject API client unavailable for authorize-stop requests")
        hubject_error = "Hubject API client unavailable"

    if isinstance(hubject_response, Mapping):
        for key in ("AuthorizationStatus", "authorizationStatus", "Status", "status"):
            value = hubject_response.get(key)
            if isinstance(value, str) and value.strip():
                hubject_status = value.strip()
                break
        status_code_payload = hubject_response.get("StatusCode") or hubject_response.get("statusCode")
        if isinstance(status_code_payload, Mapping):
            hubject_status_code = dict(status_code_payload)
        cpo_session_id = str(
            hubject_response.get("CPOPartnerSessionID")
            or hubject_response.get("SessionID")
            or cpo_session_id
        )
        emp_session_id = str(
            hubject_response.get("EMPPartnerSessionID") or emp_session_id
        )
        identification = (
            hubject_response.get("Identification")
            if isinstance(hubject_response.get("Identification"), Mapping)
            else identification
        )
        if not operator_id:
            operator_id = str(hubject_response.get("OperatorID") or "").strip()
        if not provider_id:
            provider_id = str(hubject_response.get("ProviderID") or "").strip()
    else:
        hubject_response = None

    if hubject_status is None:
        fallback = _run_check_rfid(evse_id, token)
        if fallback:
            hubject_status = fallback

    status, status_code = _map_authorization_status(hubject_status)
    if hubject_status_code:
        status_code = dict(hubject_status_code)
    elif hubject_error and status != "Authorized":
        status_code = _build_status_code(
            "500",
            "Authorization failed",
            additional=hubject_error,
        )

    response_payload = _base_authorization_response(
        status=status,
        status_code=status_code,
        evse_id=evse_id,
        cpo_session_id=cpo_session_id,
        emp_session_id=emp_session_id,
        identification=identification,
    )
    if operator_id:
        response_payload["OperatorID"] = operator_id
    if provider_id:
        response_payload["ProviderID"] = provider_id

    auth_stop = None
    if isinstance(hubject_response, Mapping):
        auth_stop_candidate = hubject_response.get("AuthorizationStop")
        if isinstance(auth_stop_candidate, Mapping):
            auth_stop = dict(auth_stop_candidate)

    if not isinstance(auth_stop, Mapping):
        auth_stop = {
            "SessionID": cpo_session_id,
            "Timestamp": response_payload["Timestamp"],
        }
    else:
        auth_stop.setdefault("SessionID", cpo_session_id)
        auth_stop.setdefault("Timestamp", response_payload["Timestamp"])

    response_payload["AuthorizationStop"] = auth_stop
    if hubject_response is not None:
        response_payload["HubjectResponse"] = hubject_response
    if hubject_error and status != "Authorized":
        response_payload.setdefault("Error", hubject_error)

    persist_event("authorize_stop_response", response_payload)

    http_status = 200 if status == "Authorized" else 401
    return jsonify(response_payload), http_status


@app.post("/Cdrmgmt/Send")
def send_cdr():
    body = request.get_json(force=True, silent=True) or {}
    cdr = body.get("Cdr") or body.get("CDR")
    if not isinstance(cdr, Mapping):
        return jsonify({"StatusCode": _build_status_code("300", "Cdr missing")}), 400

    persist_event("cdr_send_request", body)

    if HUBJECT_API_CLIENT is None:
        status_code = _build_status_code("301", "Hubject client unavailable")
        response_payload = {
            "Result": False,
            "StatusCode": status_code,
            "Timestamp": _now(),
        }
        persist_event("cdr_send_response", response_payload)
        return jsonify(response_payload), 503

    try:
        hubject_response = HUBJECT_API_CLIENT.send_cdr(cdr)
    except HubjectApiError as exc:
        logging.getLogger(__name__).warning("Hubject CDR upload failed: %s", exc)
        status_code = _build_status_code("301", "CDR upload failed", additional=str(exc))
        response_payload = {
            "Result": False,
            "StatusCode": status_code,
            "Timestamp": _now(),
            "CdrResponse": {"error": str(exc)},
        }
        persist_event("cdr_send_response", response_payload)
        return jsonify(response_payload), 502

    status_code = _build_status_code("000", "CDR accepted")
    response_payload = {
        "Result": True,
        "StatusCode": status_code,
        "Timestamp": _now(),
        "CdrResponse": hubject_response,
    }
    persist_event("cdr_send_response", response_payload)
    return jsonify(response_payload)


@app.post("/api/oicp/evsepush/v23/operators/<operator_id>/data-records")
def push_evse_data(operator_id: str):
    body = request.get_json(force=True, silent=True) or {}

    allowed_actions = {"insert", "update", "delete", "fullLoad"}
    raw_action = body.get("ActionType") or body.get("action_type") or "update"
    action_type = str(raw_action or "").strip()
    if not action_type:
        action_type = "update"
    normalized_action = next(
        (candidate for candidate in allowed_actions if candidate.lower() == action_type.lower()),
        None,
    )
    if normalized_action is None:
        status_code = _build_status_code(
            "400",
            f"Unsupported ActionType '{raw_action}'",
            additional=f"Allowed: {', '.join(sorted(allowed_actions))}",
        )
        response_payload = {
            "Result": False,
            "StatusCode": status_code,
            "Timestamp": _now(),
        }
        return jsonify(response_payload), 400

    payload: dict[str, Any] = dict(body)
    payload["OperatorID"] = operator_id
    payload["ActionType"] = normalized_action
    payload.pop("operator_id", None)

    _normalize_operator_evse_data(payload)

    persist_event("evse_data_push_request", payload)
    try:
        serialized_payload = json.dumps(payload, ensure_ascii=False)
    except (TypeError, ValueError):
        serialized_payload = str(payload)
    logger.debug("Prepared EVSE data payload: %s", serialized_payload)

    hubject_response: Mapping[str, Any] | None = None
    ocpp_response: Mapping[str, Any] | None = None

    if HUBJECT_API_CLIENT is not None:
        try:
            hubject_response = HUBJECT_API_CLIENT.push_evse_data(operator_id, payload)
        except HubjectApiError as exc:
            logging.getLogger(__name__).warning("Hubject EVSE data push failed: %s", exc)
            status_code = _build_status_code(
                "500",
                "EVSE data push failed",
                additional=str(exc),
            )
            response_payload = {
                "Result": False,
                "StatusCode": status_code,
                "OperatorID": operator_id,
                "ActionType": normalized_action,
                "Timestamp": _now(),
            }
            persist_event("evse_data_push_response", response_payload)
            return jsonify(response_payload), 502
    else:
        try:
            ocpp_response = OCPP_CLIENT.push_evse_data(payload)
        except OCPPApiError as exc:
            logging.getLogger(__name__).warning("OCPP EVSE data push failed: %s", exc)
            status_code = _build_status_code(
                "500",
                "EVSE data push failed",
                additional=str(exc),
            )
            response_payload = {
                "Result": False,
                "StatusCode": status_code,
                "OperatorID": operator_id,
                "ActionType": normalized_action,
                "Timestamp": _now(),
            }
            persist_event("evse_data_push_response", response_payload)
            return jsonify(response_payload), 502

    status_code = _build_status_code("000", "EVSE data accepted")
    response_payload: dict[str, Any] = {
        "Result": True,
        "StatusCode": status_code,
        "OperatorID": operator_id,
        "ActionType": normalized_action,
        "Timestamp": _now(),
    }
    if hubject_response is not None:
        response_payload["HubjectResponse"] = hubject_response
    if ocpp_response is not None:
        response_payload["OcppResponse"] = ocpp_response

    persist_event("evse_data_push_response", response_payload)
    return jsonify(response_payload)


@app.post("/api/oicp/evsepush/v21/operators/<operator_id>/status-records")
def push_evse_status(operator_id: str):
    body = request.get_json(force=True, silent=True) or {}

    allowed_actions = {"insert", "update", "delete", "fullLoad"}
    raw_action = body.get("ActionType") or body.get("action_type") or "update"
    action_type = str(raw_action or "").strip()
    if not action_type:
        action_type = "update"
    normalized_action = next(
        (candidate for candidate in allowed_actions if candidate.lower() == action_type.lower()),
        None,
    )
    if normalized_action is None:
        status_code = _build_status_code(
            "400",
            f"Unsupported ActionType '{raw_action}'",
            additional=f"Allowed: {', '.join(sorted(allowed_actions))}",
        )
        response_payload = {
            "Result": False,
            "StatusCode": status_code,
            "Timestamp": _now(),
        }
        return jsonify(response_payload), 400

    payload: dict[str, Any] = dict(body)
    payload["OperatorID"] = operator_id
    payload["ActionType"] = normalized_action
    payload.pop("operator_id", None)

    _normalize_operator_evse_status(payload)

    persist_event("evse_status_push_request", payload)
    try:
        serialized_payload = json.dumps(payload, ensure_ascii=False)
    except (TypeError, ValueError):
        serialized_payload = str(payload)
    logger.debug("Prepared EVSE status payload: %s", serialized_payload)

    hubject_response: Mapping[str, Any] | None = None
    ocpp_response: Mapping[str, Any] | None = None

    if HUBJECT_API_CLIENT is not None:
        try:
            hubject_response = HUBJECT_API_CLIENT.push_evse_status(operator_id, payload)
        except HubjectApiError as exc:
            logging.getLogger(__name__).warning("Hubject EVSE status push failed: %s", exc)
            status_code = _build_status_code(
                "500",
                "EVSE status push failed",
                additional=str(exc),
            )
            response_payload = {
                "Result": False,
                "StatusCode": status_code,
                "OperatorID": operator_id,
                "ActionType": normalized_action,
                "Timestamp": _now(),
            }
            persist_event("evse_status_push_response", response_payload)
            return jsonify(response_payload), 502
    else:
        try:
            ocpp_response = OCPP_CLIENT.push_evse_status(payload)
        except OCPPApiError as exc:
            logging.getLogger(__name__).warning("OCPP EVSE status push failed: %s", exc)
            status_code = _build_status_code(
                "500",
                "EVSE status push failed",
                additional=str(exc),
            )
            response_payload = {
                "Result": False,
                "StatusCode": status_code,
                "OperatorID": operator_id,
                "ActionType": normalized_action,
                "Timestamp": _now(),
            }
            persist_event("evse_status_push_response", response_payload)
            return jsonify(response_payload), 502

    status_code = _build_status_code("000", "EVSE status accepted")
    response_payload: dict[str, Any] = {
        "Result": True,
        "StatusCode": status_code,
        "OperatorID": operator_id,
        "ActionType": normalized_action,
        "Timestamp": _now(),
    }
    if hubject_response is not None:
        response_payload["HubjectResponse"] = hubject_response
    if ocpp_response is not None:
        response_payload["OcppResponse"] = ocpp_response

    persist_event("evse_status_push_response", response_payload)
    return jsonify(response_payload)


@app.get("/robots.txt")
def robots_txt() -> Response:
    return Response("User-agent: *\nDisallow: /\n", mimetype="text/plain")


@app.get("/alive")
def alive() -> Response:
    return Response("ok", mimetype="text/plain")

def create_ssl_context() -> ssl.SSLContext | None:
    if not (SSL_CERTFILE and SSL_KEYFILE):
        return None
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    context.load_cert_chain(str(SSL_CERTFILE), str(SSL_KEYFILE))
    if SSL_CA_BUNDLE:
        context.load_verify_locations(cafile=str(SSL_CA_BUNDLE))
    if REQUIRE_CLIENT_CERT:
        context.verify_mode = ssl.CERT_REQUIRED
    return context


if __name__ == "__main__":  # pragma: no cover - manual start helper
    _log_startup_configuration()
    ssl_context = create_ssl_context()
    logger.info(
        "Starting Hubject API on %s:%s (SSL=%s)", LISTEN_HOST, LISTEN_PORT, bool(ssl_context)
    )
    app.run(host=LISTEN_HOST, port=LISTEN_PORT, ssl_context=ssl_context)
