"""Plan gating enforcement for qRemoteX.

qRemoteX executors are only available for Pro and Enterprise plans.
This module handles:
  1. Graceful handling of gateway rejection due to plan limits
  2. Feature capability restrictions based on plan entitlements
  3. Connection-time plan validation response handling

The gateway (qRunX) is the authoritative enforcement point — it rejects
connections from subscriptions that don't have the remote executor
entitlement. This module handles the CLIENT-SIDE of that contract.
"""

from __future__ import annotations

import logging
from typing import Any

logger = logging.getLogger("qremotex.plan_gating")

# Plan tiers as documented in Section 16
PLAN_EXECUTOR_LIMITS = {
    "free": 0,
    "starter": 0,
    "pro": 5,       # Up to 5 per project
    "enterprise": -1,  # Unlimited (-1)
}

PLAN_API_KEY_LIMITS = {
    "free": 0,
    "starter": 0,
    "pro": 10,
    "enterprise": -1,
}

PLAN_FILE_TRANSFER_LIMITS_MB = {
    "free": 0,
    "starter": 0,
    "pro": 100,       # 100 MB/file
    "enterprise": 1024,  # 1 GB/file
}

# Error codes from gateway rejection
REJECTION_PLAN_NOT_ALLOWED = "PLAN_NOT_ALLOWED"
REJECTION_EXECUTOR_LIMIT = "EXECUTOR_LIMIT_REACHED"
REJECTION_KEY_REVOKED = "API_KEY_REVOKED"
REJECTION_KEY_EXPIRED = "API_KEY_EXPIRED"


class PlanGatingError(Exception):
    """Raised when a plan gating check fails."""

    def __init__(self, code: str, message: str) -> None:
        self.code = code
        self.message = message
        super().__init__(f"{code}: {message}")


def handle_gateway_rejection(close_code: int | None, close_reason: str) -> PlanGatingError | None:
    """Parse a WebSocket close frame for plan-gating rejections.

    The gateway sends specific close codes when rejecting connections:
      4001 — Plan does not allow remote executors (Free/Starter)
      4002 — Executor limit reached for this project (Pro: max 5)
      4003 — API key revoked
      4004 — API key expired

    Returns:
        A PlanGatingError if this is a plan-gating rejection, None otherwise.
    """
    if close_code is None:
        return None

    mapping = {
        4001: (REJECTION_PLAN_NOT_ALLOWED, "Remote executors require Pro or Enterprise plan"),
        4002: (REJECTION_EXECUTOR_LIMIT, "Executor limit reached for this project"),
        4003: (REJECTION_KEY_REVOKED, "API key has been revoked"),
        4004: (REJECTION_KEY_EXPIRED, "API key has expired"),
    }

    if close_code in mapping:
        code, default_msg = mapping[close_code]
        msg = close_reason if close_reason else default_msg
        logger.error("Gateway rejected connection: [%d] %s — %s", close_code, code, msg)
        return PlanGatingError(code, msg)

    return None


def is_permanent_rejection(error: PlanGatingError) -> bool:
    """Check if a rejection should stop reconnect attempts.

    Plan-not-allowed and key-revoked are permanent — reconnecting won't help.
    Limit-reached and key-expired may resolve on their own.
    """
    return error.code in (REJECTION_PLAN_NOT_ALLOWED, REJECTION_KEY_REVOKED)


def check_entitlements(entitlements: dict[str, Any]) -> list[str]:
    """Validate entitlements received from gateway after hello.

    The gateway may send entitlements in its hello response.
    Returns a list of warnings (empty = all good).
    """
    warnings: list[str] = []

    max_file_mb = entitlements.get("max_file_transfer_mb", 0)
    if max_file_mb > 0:
        logger.info("Plan file transfer limit: %d MB/file", max_file_mb)

    mtls_allowed = entitlements.get("mtls_allowed", False)
    if not mtls_allowed:
        logger.debug("mTLS not available on current plan")

    ip_allowlist_allowed = entitlements.get("ip_allowlist_allowed", False)
    if not ip_allowlist_allowed:
        logger.debug("IP allowlisting not available on current plan")

    log_retention_days = entitlements.get("log_retention_days", 0)
    if log_retention_days > 0:
        logger.info("Execution log retention: %d days", log_retention_days)

    return warnings
