"""F5 BIG-IP read-only tools via iControl REST API."""

import time

import requests
import urllib3

from mcp_network_jcd.models.f5 import (
    F5_OBJECT_TYPES,
    F5_SUBCOLLECTIONS,
    F5Credentials,
    F5Response,
)
from mcp_network_jcd.models.paloalto import ErrorInfo

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

_REQUEST_KWARGS = {
    "verify": False,  # F5 devices commonly use self-signed certs
    "proxies": {"http": "", "https": ""},  # Always direct access, never via proxy
}


def _create_error_response(host: str, code: str, message: str, start_time: float) -> dict:
    """Create an error response dictionary."""
    duration = int((time.time() - start_time) * 1000)
    return F5Response(
        success=False,
        host=host,
        error=ErrorInfo(code=code, message=message),
        duration_ms=duration,
    ).model_dump()


def _encode_object_name(object_name: str) -> str:
    """Encode F5 object name for use in iControl REST API URLs.

    The F5 REST API uses ~ as partition separator in URLs, but fullPath values
    returned by the API use / (e.g., /Common/mypool). This converts
    /Common/mypool → ~Common~mypool so the URL is valid.

    Names already encoded with ~ or plain names (no partition) pass through unchanged.
    """
    if "/" in object_name and "~" not in object_name:
        return object_name.replace("/", "~")
    return object_name


def _validate_object_type(object_type: str) -> str | None:
    """Validate object_type. Returns error message if invalid, None if valid."""
    if object_type not in F5_OBJECT_TYPES:
        return f"Invalid object_type '{object_type}'. Must be one of: {', '.join(sorted(F5_OBJECT_TYPES))}"
    return None


def _validate_sub_collection(object_type: str, sub_collection: str, object_name: str) -> str | None:
    """Validate sub_collection. Returns error message if invalid, None if valid."""
    if not sub_collection:
        return None
    if not object_name:
        return "sub_collection requires object_name to be set"
    allowed = F5_SUBCOLLECTIONS.get(object_type)
    if allowed is None:
        return f"object_type '{object_type}' does not support sub-collections"
    if sub_collection not in allowed:
        return (
            f"Invalid sub_collection '{sub_collection}' for '{object_type}'. "
            f"Must be one of: {', '.join(sorted(allowed))}"
        )
    return None


def _filter_items(data: dict, name_filter: str) -> dict:
    """Filter items in F5 response by case-insensitive substring match.

    Matches against name, fullPath, and destination fields.
    Returns data unchanged if no filter or no items array.
    """
    if not name_filter or not isinstance(data, dict) or "items" not in data:
        return data
    pattern = name_filter.lower()
    filtered = [
        item
        for item in data["items"]
        if any(
            pattern in str(item.get(field, "")).lower()
            for field in ("name", "fullPath", "destination")
        )
    ]
    return {**data, "items": filtered}


def _load_credentials(host: str, start_time: float) -> F5Credentials | dict:
    """Load F5 credentials. Returns credentials or error response dict."""
    try:
        return F5Credentials.from_env()
    except ValueError as e:
        return _create_error_response(host, "CONFIG_ERROR", str(e), start_time)


def _get_auth_token(host: str, credentials: F5Credentials, timeout: int) -> str:
    """Obtain an auth token from the F5 device via /mgmt/shared/authn/login.

    Returns the token string.
    Raises requests exceptions or ValueError on failure.
    """
    login_url = f"https://{host}/mgmt/shared/authn/login"
    response = requests.post(
        login_url,
        json={"username": credentials.username, "password": credentials.password},
        timeout=timeout,
        **_REQUEST_KWARGS,
    )
    if response.status_code in (401, 403):
        raise ValueError("Authentication failed: invalid credentials")
    response.raise_for_status()
    data = response.json()
    return data["token"]["token"]


def _make_request(
    method: str,
    url: str,
    token: str,
    timeout: int,
    json_payload: dict | None = None,
) -> requests.Response:
    """Execute an authenticated HTTP request to the F5 iControl REST API.

    Uses X-F5-Auth-Token header for authentication.
    Raises requests exceptions on failure.
    """
    headers = {
        "X-F5-Auth-Token": token,
        "Content-Type": "application/json",
    }
    return requests.request(
        method,
        url,
        headers=headers,
        json=json_payload,
        timeout=timeout,
        **_REQUEST_KWARGS,
    )


def _handle_response(
    response: requests.Response,
    host: str,
    start_time: float,
) -> dict:
    """Process an HTTP response into an F5Response dict.

    Handles HTTP status codes and returns structured response.
    """
    if response.status_code in (401, 403):
        return _create_error_response(host, "AUTH_FAILED", "Authentication failed", start_time)

    if response.status_code >= 400:
        return _create_error_response(
            host,
            "HTTP_ERROR",
            f"HTTP {response.status_code}: {response.text[:500]}",
            start_time,
        )

    duration = int((time.time() - start_time) * 1000)
    try:
        data = response.json()
    except ValueError:
        data = response.text

    return F5Response(
        success=True,
        host=host,
        data=data,
        duration_ms=duration,
    ).model_dump()


def _execute_request(
    method: str,
    url: str,
    host: str,
    token: str,
    timeout: int,
    start_time: float,
    json_payload: dict | None = None,
) -> dict:
    """Execute a request with full error handling. Returns F5Response dict."""
    try:
        response = _make_request(method, url, token, timeout, json_payload)
    except requests.exceptions.Timeout:
        return _create_error_response(
            host, "CONNECTION_TIMEOUT", f"Request timed out after {timeout} seconds", start_time
        )
    except requests.exceptions.ConnectionError:
        return _create_error_response(
            host, "CONNECTION_REFUSED", f"Cannot connect to {host}", start_time
        )
    except Exception as e:
        return _create_error_response(
            host, "UNKNOWN_ERROR", f"Unexpected error: {type(e).__name__}: {e}", start_time
        )

    return _handle_response(response, host, start_time)


def _authenticate_and_execute(
    method: str,
    url: str,
    host: str,
    credentials: F5Credentials,
    timeout: int,
    start_time: float,
    json_payload: dict | None = None,
) -> dict:
    """Authenticate via token, then execute the request."""
    try:
        token = _get_auth_token(host, credentials, timeout)
    except ValueError:
        return _create_error_response(host, "AUTH_FAILED", "Authentication failed", start_time)
    except requests.exceptions.Timeout:
        return _create_error_response(
            host, "CONNECTION_TIMEOUT", f"Login timed out after {timeout} seconds", start_time
        )
    except requests.exceptions.ConnectionError:
        return _create_error_response(
            host, "CONNECTION_REFUSED", f"Cannot connect to {host}", start_time
        )
    except Exception as e:
        return _create_error_response(
            host, "UNKNOWN_ERROR", f"Login error: {type(e).__name__}: {e}", start_time
        )

    return _execute_request(method, url, host, token, timeout, start_time, json_payload)


def f5_list(
    host: str,
    object_type: str,
    object_name: str = "",
    timeout: int = 20,
    sub_collection: str = "",
    name_filter: str = "",
) -> dict:
    """List or show an F5 LTM object configuration via iControl REST API.

    Args:
        host: F5 device hostname or IP address.
        object_type: LTM object type (virtual, pool, irule, profile).
        object_name: Object name. If empty, lists all objects of that type.
        timeout: Request timeout in seconds (default: 20).
        sub_collection: Sub-collection to query (e.g. members, policies, profiles).
        name_filter: Case-insensitive substring filter on name, fullPath, destination.

    Returns:
        dict: F5Response with object configuration data.
    """
    start_time = time.time()

    error = _validate_object_type(object_type)
    if error:
        return _create_error_response(host, "VALIDATION_ERROR", error, start_time)

    error = _validate_sub_collection(object_type, sub_collection, object_name)
    if error:
        return _create_error_response(host, "VALIDATION_ERROR", error, start_time)

    result = _load_credentials(host, start_time)
    if isinstance(result, dict):
        return result
    credentials = result

    url = f"https://{host}/mgmt/tm/ltm/{object_type}"
    if object_name:
        url = f"{url}/{_encode_object_name(object_name)}"
    if sub_collection:
        url = f"{url}/{sub_collection}"

    result = _authenticate_and_execute("GET", url, host, credentials, timeout, start_time)

    if result.get("success") and name_filter:
        result["data"] = _filter_items(result["data"], name_filter)

    return result


def f5_stats(
    host: str,
    object_type: str,
    object_name: str,
    timeout: int = 20,
) -> dict:
    """Show statistics for an F5 LTM object via iControl REST API.

    Args:
        host: F5 device hostname or IP address.
        object_type: LTM object type (virtual, pool, irule, profile).
        object_name: Name of the object to get stats for.
        timeout: Request timeout in seconds (default: 20).

    Returns:
        dict: F5Response with object statistics data.
    """
    start_time = time.time()

    error = _validate_object_type(object_type)
    if error:
        return _create_error_response(host, "VALIDATION_ERROR", error, start_time)

    result = _load_credentials(host, start_time)
    if isinstance(result, dict):
        return result
    credentials = result

    url = f"https://{host}/mgmt/tm/ltm/{object_type}/{_encode_object_name(object_name)}/stats"

    return _authenticate_and_execute("GET", url, host, credentials, timeout, start_time)
