"""
Onboarding application state machine — PRD-HWONB-001 §7.2.

    draft -> in_progress -> ready_to_transmit -> transmitted -> {in_process|pending}
          -> {approved -> provisioned | rejected}

Also handles the error terminal state (`error`), used when HubWallet's own
bootstrap/config is broken (PRD-HWONB-002 §3.2) rather than a merchant or GP
underwriting problem.

IMPORTANT — read before trusting this blindly (PRD-HWONB-001 §13.1 C-24):
GP's exact terminal-state strings for `GET /status`'s `applicationStatus`/
`approvalStatus` fields are NOT fully enumerated in any source material
(PDF Developer Guide examples are sparse, and CERT probing hadn't reached a
real approval/rejection decision as of this writing — see C-24/C-25 in the
PRD). `_map_gp_status_to_local()` below is therefore intentionally
defensive: it logs the full raw GP payload to `tsys_decision_payload` on
every call (so a real CERT-observed decision can be diffed against this
mapping later), and falls back to using **`mid` presence** as an interim
approval heuristic when the status string itself doesn't match a known
value. Do not ship this mapping as authoritative without confirming it
against a real CERT-observed approval/rejection first (PRD-HWONB-013 §2.1).
"""

from __future__ import annotations

import logging
from typing import Any, Dict

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Local status transition table
# ---------------------------------------------------------------------------

# Local statuses, in the order the application state machine allows.
STATUSES = (
    "draft",
    "in_progress",
    "ready_to_transmit",
    "transmitted",
    "in_process",
    "pending",
    "approved",
    "provisioned",
    "rejected",
    "error",
)

# Allowed forward transitions. `error` and `rejected` are reachable from any
# non-terminal state (a GP call can fail / a rejection can be observed at any
# point after transmit). `restart_after_rejection` (services.py) is the only
# path back from `rejected`, and it does so by creating a brand-new
# application row, not by transitioning this one.
_TRANSITIONS: Dict[str, set] = {
    "draft": {"in_progress", "error"},
    "in_progress": {"in_progress", "ready_to_transmit", "error"},
    "ready_to_transmit": {"ready_to_transmit", "transmitted", "in_progress", "error"},
    "transmitted": {"in_process", "pending", "approved", "rejected", "error"},
    "in_process": {"in_process", "pending", "approved", "rejected", "error"},
    "pending": {"pending", "approved", "rejected", "error"},
    "approved": {"provisioned", "error"},
    "provisioned": set(),  # terminal — success
    "rejected": set(),  # terminal — only escaped via restart (new application row)
    "error": {"in_progress", "error"},  # a config/bootstrap error can be retried
}

# GP status string -> local status. Populate/correct this from real
# CERT-observed values (PRD-HWONB-001 §13.1 C-24) — these are best-effort
# readings from the sparse PDF examples, not confirmed exhaustive.
_GP_STATUS_MAP: Dict[str, str] = {
    "IN_PROCESS": "in_process",
    "PENDING": "pending",
    "APPROVED": "approved",
    "REJECTED": "rejected",
}


def can_transition(current: str, target: str) -> bool:
    """Return True if `current -> target` is an allowed transition."""
    return target in _TRANSITIONS.get(current, set())


def assert_transition(current: str, target: str) -> None:
    """Raise ValueError if `current -> target` is not an allowed transition."""
    if not can_transition(current, target):
        raise ValueError(f"Invalid onboarding status transition: {current!r} -> {target!r}")


def _map_gp_status_to_local(
    gp_payload: Dict[str, Any],
    *,
    current_local_status: str,
) -> str:
    """
    Map a raw GP `GET /status` (or `/activity`) payload to a local status.

    Defensive by design (see module docstring) — the caller (poll_status
    task / refresh_status service) is responsible for persisting the full
    raw `gp_payload` into `tsys_decision_payload` regardless of what this
    function returns, so a human can reconcile later if this guess is wrong.
    """
    raw_status = (
        gp_payload.get("applicationStatus")
        or gp_payload.get("approvalStatus")
        or gp_payload.get("status")
        or ""
    )
    raw_status_upper = str(raw_status).upper().strip()

    mapped = _GP_STATUS_MAP.get(raw_status_upper)
    if mapped:
        return mapped

    # Interim heuristic (explicitly not authoritative, C-24): presence of a
    # MID strongly implies underwriting approved the application even if the
    # status string itself doesn't match anything in _GP_STATUS_MAP.
    mid = gp_payload.get("mid") or gp_payload.get("merchantId") or gp_payload.get("MID")
    if mid:
        logger.warning(
            "status_machine: unrecognized GP status %r but mid=%r present — "
            "treating as 'approved' via interim heuristic (C-24, unconfirmed)",
            raw_status,
            mid,
        )
        return "approved"

    logger.warning(
        "status_machine: unrecognized GP status %r, no mid present — leaving "
        "local status unchanged (%r). Raw payload logged to tsys_decision_payload.",
        raw_status,
        current_local_status,
    )
    return current_local_status


__all__ = [
    "STATUSES",
    "can_transition",
    "assert_transition",
    "_map_gp_status_to_local",
]
