"""Helpers for CLI `connect` command and local onboarding config updates."""

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import TYPE_CHECKING

import yaml

from mindroom.cli.owner import parse_owner_matrix_user_id, replace_owner_placeholders_in_text
from mindroom.config.yaml_includes import load_yaml_config_source
from mindroom.constants import OWNER_MATRIX_USER_ID_ENV
from mindroom.http_error_detail import error_detail_from_response

from .env_file import env_path_for_config, upsert_env_values

if TYPE_CHECKING:
    from collections.abc import Callable
    from pathlib import Path

    import httpx

_PAIR_CODE_RE = re.compile(r"^[A-Z0-9]{4}-[A-Z0-9]{4}$")
_NAMESPACE_RE = re.compile(r"^[a-z0-9]{4,32}$")


@dataclass(frozen=True)
class _PairCompleteResult:
    """Credentials returned by the provisioning pair-complete endpoint."""

    client_id: str
    client_secret: str
    namespace: str
    owner_user_id: str | None = None
    namespace_invalid: bool = False
    owner_user_id_invalid: bool = False


def is_valid_pair_code(pair_code: str) -> bool:
    """Return True if pair_code has the expected ABCD-EFGH form."""
    return bool(_PAIR_CODE_RE.fullmatch(pair_code))


def complete_local_pairing(
    *,
    provisioning_url: str,
    pair_code: str,
    client_name: str,
    client_fingerprint: str,
    matrix_ssl_verify: bool,
    post_request: Callable[..., httpx.Response] | None = None,
) -> _PairCompleteResult:
    """Call the provisioning API and return local client credentials."""
    import httpx  # noqa: PLC0415

    payload = {
        "pair_code": pair_code,
        "client_name": client_name.strip(),
        "client_pubkey_or_fingerprint": client_fingerprint,
    }
    endpoint = f"{provisioning_url.rstrip('/')}/v1/local-mindroom/pair/complete"
    request = post_request or httpx.post

    try:
        response = request(endpoint, json=payload, timeout=10, verify=matrix_ssl_verify)
    except httpx.HTTPError as exc:
        msg = f"Could not reach provisioning service: {exc}"
        raise ValueError(msg) from exc

    if not response.is_success:
        detail = error_detail_from_response(response)
        msg = f"Pairing failed ({response.status_code}): {detail}"
        raise ValueError(msg)

    try:
        data = response.json()
    except ValueError as exc:
        msg = "Provisioning service returned invalid JSON."
        raise ValueError(msg) from exc
    if not isinstance(data, dict):
        msg = "Provisioning service returned unexpected response."
        raise TypeError(msg)

    raw_owner_user_id = data.get("owner_user_id")
    parsed_owner_user_id = parse_owner_matrix_user_id(raw_owner_user_id)
    owner_user_id_invalid = (
        isinstance(raw_owner_user_id, str) and bool(raw_owner_user_id.strip()) and parsed_owner_user_id is None
    )
    client_id = _required_non_empty_string(data, "client_id")
    raw_namespace = data.get("namespace")
    parsed_namespace = _parse_namespace(raw_namespace)
    namespace_invalid = isinstance(raw_namespace, str) and bool(raw_namespace.strip()) and parsed_namespace is None
    if parsed_namespace is None:
        parsed_namespace = ""

    return _PairCompleteResult(
        client_id=client_id,
        client_secret=_required_non_empty_string(data, "client_secret"),
        namespace=parsed_namespace,
        owner_user_id=parsed_owner_user_id,
        namespace_invalid=namespace_invalid,
        owner_user_id_invalid=owner_user_id_invalid,
    )


def persist_local_provisioning_env(
    *,
    provisioning_url: str,
    client_id: str,
    client_secret: str,
    namespace: str,
    owner_user_id: str | None = None,
    config_path: str | Path,
) -> Path:
    """Write local provisioning credentials to .env next to the active config file."""
    updates = {
        "MINDROOM_PROVISIONING_URL": provisioning_url.rstrip("/"),
        "MINDROOM_LOCAL_CLIENT_ID": client_id,
        "MINDROOM_LOCAL_CLIENT_SECRET": client_secret,
        "MINDROOM_NAMESPACE": namespace,
    }
    if parsed_owner_user_id := parse_owner_matrix_user_id(owner_user_id):
        updates[OWNER_MATRIX_USER_ID_ENV] = parsed_owner_user_id

    return upsert_env_values(env_path_for_config(config_path), updates)


def _owner_placeholder_config_files(config_path: Path) -> tuple[Path, ...]:
    """Return every config source file: the top-level file plus each !include target."""
    try:
        _, source_files = load_yaml_config_source(config_path)
    except (OSError, yaml.YAMLError, UnicodeError):
        # A config that fails to parse still gets top-level replacement.
        return (config_path,)
    return tuple(sorted(source_files))


def replace_owner_placeholders_in_config(*, config_path: Path, owner_user_id: str) -> bool:
    """Replace owner placeholder tokens in the config and its !include files."""
    if parse_owner_matrix_user_id(owner_user_id) is None:
        return False
    if not config_path.exists():
        return False

    replaced_any = False
    for source_file in _owner_placeholder_config_files(config_path):
        content = source_file.read_text(encoding="utf-8")
        replaced = replace_owner_placeholders_in_text(content, owner_user_id)
        if replaced != content:
            source_file.write_text(replaced, encoding="utf-8")
            replaced_any = True
    return replaced_any


def _required_non_empty_string(data: dict[str, object], key: str) -> str:
    """Read a required string field from a JSON dict."""
    raw_value = data.get(key)
    if isinstance(raw_value, str):
        value = raw_value.strip()
        if value:
            return value
    msg = f"Provisioning response missing {key}."
    raise ValueError(msg)


def _parse_namespace(raw_value: object) -> str | None:
    """Parse optional installation namespace from pairing response."""
    if not isinstance(raw_value, str):
        return None
    namespace = raw_value.strip().lower()
    if not namespace:
        return None
    if _NAMESPACE_RE.fullmatch(namespace):
        return namespace
    return None
