"""Pure guards and payload helpers for reversible gateway HIL operations."""

from __future__ import annotations

import ipaddress
import json
import urllib.parse
from typing import Any


FIXTURE_GATE = "UMEC-DLM-DUT-FIXTURE"
MODE_GATE = "UMEC-DLM-MODE"
NAME_GATE = "UMEC-DLM-NAME"
FIELDBUS_LINE_GATE = "UMEC-FIELDBUS-LINE-APPLY"
HIL_TITLE_PREFIX = "umec-hil-dlm-"
TASK118_OFFLINE_SERIAL = "umec-hil-dlm-118-offline-201"
TASK118_RENAME_STAGES = {
    "first": "umec-hil-dlm-118-managed-a-201",
    "second": "umec-hil-dlm-118-managed-b-201",
    "third": "umec-hil-dlm-118-managed-c-201",
}
TASK118_RENAME_PREVIOUS = {
    "first": frozenset({TASK118_OFFLINE_SERIAL, TASK118_RENAME_STAGES["first"]}),
    "second": frozenset({TASK118_RENAME_STAGES["first"], TASK118_RENAME_STAGES["second"]}),
    "third": frozenset({TASK118_RENAME_STAGES["second"], TASK118_RENAME_STAGES["third"]}),
}
SUPPORTED_CLOUD_MODES = frozenset(
    {"UMEC_SPACE", "UMEC_LITE", "MQTT_GENERIC", "MQTT_DISABLED"}
)


def require_private_http_origin(value: str) -> str:
    parsed = urllib.parse.urlsplit(value)
    if (
        parsed.scheme != "http"
        or not parsed.hostname
        or parsed.username
        or parsed.password
        or parsed.path not in ("", "/")
        or parsed.query
        or parsed.fragment
        or parsed.port is not None
    ):
        raise ValueError("HIL base URL must be an exact private HTTP origin without a port")
    try:
        address = ipaddress.ip_address(parsed.hostname)
    except ValueError as exc:
        raise ValueError("HIL base URL must use a literal private IP address") from exc
    if not address.is_private:
        raise ValueError("HIL base URL must use a private IP address")
    return f"http://{address}"


def validate_bus(value: int) -> int:
    if value not in (1, 2, 3):
        raise ValueError("bus must be 1, 2 or 3")
    return value


def validate_fieldbus_line(value: int) -> int:
    if value not in (1, 2):
        raise ValueError("fieldbus line must be 1 or 2")
    return value


def validate_fixture_id(value: int) -> int:
    if value < 200 or value > 239:
        raise ValueError("HIL fixture device id must be in the reserved range 200..239")
    return value


def validate_fixture_payload(raw: bytes, expected_id: int) -> dict[str, Any]:
    if not raw or len(raw) > 65536:
        raise ValueError("fixture payload is empty or too large")
    try:
        payload = json.loads(raw.decode("utf-8"))
    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
        raise ValueError("fixture payload must be one UTF-8 JSON object") from exc
    if not isinstance(payload, dict):
        raise ValueError("fixture payload must be one JSON object")
    if payload.get("id") != expected_id:
        raise ValueError("fixture payload id does not match the exact target")
    title = payload.get("title")
    if not isinstance(title, str) or not title.startswith(HIL_TITLE_PREFIX):
        raise ValueError(f"fixture title must start with {HIL_TITLE_PREFIX}")
    if not isinstance(payload.get("serial"), str) or not payload["serial"].startswith(HIL_TITLE_PREFIX):
        raise ValueError(f"fixture serial must start with {HIL_TITLE_PREFIX}")
    if not isinstance(payload.get("channels", []), list):
        raise ValueError("fixture channels must be an array")
    if not isinstance(payload.get("commands", []), list):
        raise ValueError("fixture commands must be an array")
    if len(payload.get("channels", [])) > 64 or len(payload.get("commands", [])) > 32:
        raise ValueError("fixture capability count exceeds the HIL bound")
    return payload


def rename_task118_fixture(raw: bytes, expected_id: int, stage: str) -> tuple[dict[str, Any], bytes]:
    payload = validate_fixture_payload(raw, expected_id)
    if expected_id != 201 or payload.get("serial") != TASK118_OFFLINE_SERIAL:
        raise ValueError("rename target is not the exact TASK-118 offline fixture")
    if stage not in TASK118_RENAME_STAGES:
        raise ValueError("rename stage must be first, second or third")
    if payload.get("title") not in TASK118_RENAME_PREVIOUS[stage]:
        raise ValueError("fixture is not in the expected rename stage")
    renamed = {**payload, "title": TASK118_RENAME_STAGES[stage]}
    return renamed, json.dumps(
        renamed,
        ensure_ascii=False,
        separators=(",", ":"),
    ).encode("utf-8")


def validate_mode(value: str) -> str:
    if value not in SUPPORTED_CLOUD_MODES:
        raise ValueError("unsupported cloud mode")
    return value


def validate_name(value: str) -> str:
    if not isinstance(value, str):
        raise ValueError("name must be a string")
    value = value.strip(" \t\n\r\v\f")
    encoded = value.encode("utf-8")
    if len(encoded) > 64:
        raise ValueError("name must be at most 64 UTF-8 bytes")
    if any(ord(char) < 32 or ord(char) == 127 for char in value):
        raise ValueError("name contains a control character")
    if "/" in value or "\\" in value:
        raise ValueError("name contains a path separator")
    return value


def require_gate(actual: str | None, expected: str) -> None:
    if actual != expected:
        raise ValueError(f"operation requires --gate {expected}")
