# eda_integration/clients/eda/saas_client.py

"""
EDA SaaS API client (third workflow).

Uses edasaas/api/v1: IAM login, core/organizations, ipmanagement/subnets.
Used for shared/MSP cluster models when creating mgmt resources in a selected org.
"""

import json
import logging
import ssl

from eda_integration.clients.eda.http_client import create_pool_manager
from eda_integration.utils.constants import (
    EDA_SAAS_ORGANIZATIONS_PATH,
    EDA_SAAS_POOLS_PATH,
    EDA_SAAS_SUBNETS_PATH,
    HTTP_CREATED,
    HTTP_NO_CONTENT,
    HTTP_OK,
)
from eda_integration.utils.exceptions import EDAConnectionError
from eda_integration.utils.api_utils import parse_organizations_response

logger = logging.getLogger(__name__)


def _decode_saas_response(data: bytes, context: str = "EDA SaaS API") -> dict | list:
    """Decode UTF-8 bytes and parse JSON; raise EDAConnectionError with clear message on failure."""
    try:
        text = data.decode("utf-8")
    except UnicodeDecodeError as e:
        raise EDAConnectionError(f"{context} response was not valid UTF-8: {e}") from e
    try:
        return json.loads(text)
    except json.JSONDecodeError as e:
        raise EDAConnectionError(f"Invalid JSON from {context}: {e}") from e


def _request_with_connection_handling(http, method: str, url: str, **kwargs):
    """Run HTTP request; on connection/SSL errors raise EDAConnectionError with readable message."""
    try:
        return http.request(method, url, **kwargs)
    except ssl.SSLError as e:
        raise EDAConnectionError(f"SSL error connecting to EDA SaaS: {e}") from e
    except OSError as e:
        raise EDAConnectionError(f"Network error connecting to EDA SaaS: {e}") from e
    except Exception as e:
        err_name = type(e).__module__ + "." + type(e).__name__
        if "urllib3" in err_name or "timeout" in err_name.lower() or "connection" in err_name.lower():
            raise EDAConnectionError(f"Could not reach EDA SaaS: {e}") from e
        raise


class EDASaasClient:
    """
    Client for the EDA SaaS API (edasaas/api/v1).

    Auth: POST edasaas/api/v1/iam/login with organizationName, username, password
    to obtain an access token. Used for listing organizations and creating
    shared mgmt resources (subnets) in a chosen org.
    """

    def __init__(
        self,
        base_url: str,
        username: str,
        password: str,
        org_name: str,
        verify: bool = True,
    ):
        self.base_url = base_url.rstrip("/")
        self.username = username
        self.password = password
        self.org_name = org_name
        self.verify = verify
        self._access_token: str | None = None
        self.http = create_pool_manager(url=self.base_url, verify=self.verify)

    def login(self) -> None:
        """Authenticate via edasaas IAM login; store access token."""
        url = f"{self.base_url}/edasaas/api/v1/iam/login"
        payload = {
            "organizationName": self.org_name,
            "username": self.username,
            "password": self.password,
        }
        body = json.dumps(payload).encode("utf-8")
        headers = {"Content-Type": "application/json"}
        logger.debug("POST %s (edasaas login)", url)
        resp = _request_with_connection_handling(self.http, "POST", url, headers=headers, body=body)
        if resp.status != HTTP_OK:
            logger.debug("payload: %s", payload)
            raise EDAConnectionError(
                f"EDA SaaS login failed: {resp.status} {resp.data.decode(errors='replace')}"
            )
        data = _decode_saas_response(resp.data, "EDA SaaS login")
        token = data.get("accessToken")
        if not token:
            raise EDAConnectionError("EDA SaaS login response missing accessToken")
        self._access_token = token
        logger.debug("EDA SaaS login successful")

    def _ensure_token(self) -> None:
        if not self._access_token:
            self.login()

    def _headers(self) -> dict:
        self._ensure_token()
        return {
            "Authorization": f"Bearer {self._access_token}",
            "Content-Type": "application/json",
        }

    def get_organizations(self) -> dict[str, str]:
        """
        GET edasaas/api/v1/core/organizations.
        Returns mapping of organization name -> id.
        """
        url = f"{self.base_url}/{EDA_SAAS_ORGANIZATIONS_PATH}"
        logger.debug("GET %s", url)
        resp = _request_with_connection_handling(self.http, "GET", url, headers=self._headers())
        if resp.status != HTTP_OK:
            raise EDAConnectionError(
                f"Failed to get organizations: {resp.status} {resp.data.decode(errors='replace')}"
            )
        data = _decode_saas_response(resp.data, "organizations API")
        if not isinstance(data, list):
            raise EDAConnectionError("Organizations response was not a list")
        return parse_organizations_response(data)

    def post_subnets(self, org_id: str, payload: dict) -> dict | list:
        """
        POST edasaas/api/v1/ipmanagement/subnets with X-Scope and X-Organization.
        payload: body with subnetIps (e.g. {"subnetIps": ["10.150.150.0/24"]}).
        Returns decoded JSON response (single subnet object or list of subnet objects).
        """
        url = f"{self.base_url}/{EDA_SAAS_SUBNETS_PATH}"
        headers = {
            **self._headers(),
            "X-Scope": str(org_id),
            "X-Organization": str(org_id),
        }
        body = json.dumps(payload).encode("utf-8")
        logger.debug("POST %s (org_id=%s)", url, org_id)
        resp = _request_with_connection_handling(self.http, "POST", url, headers=headers, body=body)
        if resp.status not in (HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT):
            raise EDAConnectionError(
                f"Failed to create shared subnets: {resp.status} {resp.data.decode(errors='replace')}"
            )
        if resp.status == HTTP_NO_CONTENT or not resp.data:
            return []
        return _decode_saas_response(resp.data, "subnets API")

    def post_pool(self, org_id: str, payload: dict) -> None:
        """
        POST edasaas/api/v1/ipmanagement/pools with X-Scope and X-Organization.
        payload: body with allocations, name, reservations, subnetId.
        """
        url = f"{self.base_url}/{EDA_SAAS_POOLS_PATH}"
        headers = {
            **self._headers(),
            "X-Scope": str(org_id),
            "X-Organization": str(org_id),
        }
        body = json.dumps(payload).encode("utf-8")
        logger.debug("POST %s (org_id=%s)", url, org_id)
        resp = _request_with_connection_handling(self.http, "POST", url, headers=headers, body=body)
        if resp.status not in (HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT):
            raise EDAConnectionError(
                f"Failed to create pool: {resp.status} {resp.data.decode(errors='replace')}"
            )
