"""
Copyright © 2025-2026 Omnissa, LLC.
"""

import json
import logging
import sys
import threading
import time
from typing import Callable, Iterator

from hcs_core.ctxp import CtxpException, panic, profile
from hcs_core.sglib.ez_client import EzClient
from hcs_core.util import duration, exit
from hcs_core.util.query_util import PageRequest, with_query

log = logging.getLogger(__name__)

_caches = {}

_client_instance_lock = threading.RLock()


def _get_service_override(service_name: str):
    profile_data = profile.current()
    override = profile_data.get("override", {})
    service_override = {}
    for k, v in override.items():
        if service_name == k.lower():
            service_override = v

    if not service_override:
        if service_name == "org-service":
            service_override = override.get("org", {})
        elif service_name.find("-") >= 0:
            camel_name = "".join(word.capitalize() for word in service_name.split("-"))
            camel_name = camel_name[0].lower() + camel_name[1:]
            service_override = override.get(camel_name, {})
    return service_override


def _lazy_init(service_name: str, hdc: str = None, region: str = None):  # make it deferred so no need to initialize profile
    if region and hdc:
        raise Exception("region and hdc cannot be specified at the same time.")

    profile_data = profile.current()

    service_override = _get_service_override(service_name)
    service_override_url = service_override.get("url")
    if service_override_url:
        log.debug(f"Using per-service override for {service_name}: {service_override_url}")
        url = service_override_url
    else:
        if hdc:
            # prod only
            hdc = hdc.upper()
            if hdc == "US":
                url = profile_data.hcs.url
            elif hdc == "EU":
                url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-eu")
            elif hdc == "JP":
                url = profile_data.hcs.url.replace("cloud-sg-us", "cloud-sg-jp")
            else:
                raise CtxpException(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
        elif region:
            url = _get_region_url(region)
            if not url:
                panic("Missing profile property: hcs.regions")
        else:
            url = profile_data.hcs.url
    url = url.rstrip("/") + "/" + service_name

    # TODO
    client_id = service_override.get("client-id", None)
    if not client_id:
        client_id = service_override.get("clientId", None)
    client_secret = service_override.get("client-secret", None)
    if not client_secret:
        client_secret = service_override.get("clientSecret", None)
    api_token = service_override.get("api-token", None)
    if not api_token:
        api_token = service_override.get("apiToken", None)
    provider = service_override.get("provider", "vmwarecsp")
    if provider == "vmwarecsp":
        token_url = profile_data.csp.url
    elif provider == "auth-service":
        token_url = profile_data.auth["tokenUrl"]
    else:
        raise CtxpException(f"Unknown provider: {provider}. Supported providers: vmwarecsp, auth-service.")

    if client_id:
        if not client_secret:
            panic(f"Client ID is specified but missing clientSecret for service {service_name} in profile override.")
    else:
        if client_secret:
            panic(f"Client secret is specified but missing clientId for service {service_name} in profile override.")

    if client_id:
        auth_config = {
            "url": token_url,
            "client_id": client_id,
            "client_secret": client_secret,
        }
    elif api_token:
        auth_config = {
            "url": token_url,
            "api_token": api_token,
        }
    else:
        auth_config = None

    from hcs_core.sglib import auth

    oauth_client = auth.oauth_client(auth_config=auth_config)
    return url, oauth_client


def hdc_service_client(service_name: str, hdc: str = None) -> EzClient:
    """A client for HDC service. Cached per service name, thread-safe, lazy initialization."""
    if hdc:
        hdc = hdc.upper()
        if hdc not in ["US", "EU", "JP"]:
            panic(f"Invalid HDC name: {hdc}. Supported HDC names: US, EU, JP.")
    else:
        hdc = "US"

    k = f"{service_name}#{hdc}"
    with _client_instance_lock:
        instance = _caches.get(k)
        if not instance:
            instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, hdc=hdc))
            _caches[k] = instance
        return instance


def _get_region_url(region: str):
    regions = profile.current().hcs.regions
    if not region:
        return regions[0].url
    for r in regions:
        if r.name.lower() == region.lower():
            return r.url
    names = []
    for r in regions:
        names.append(r.name)
    panic(f"Region not found: {region}. Available regions: {names}")


def regional_service_client(service_name: str, region: str = None):
    # 'https://dev1b-westus2-cp103a.azcp.horizon.omnissa.com/vmhub'
    region_names = [r.name.lower() for r in profile.current().hcs.regions]
    if region:
        region = region.lower()
        if region not in region_names:
            panic(f"Invalid region name: {region}. Available regions: {', '.join(region_names)}")
    else:
        region = region_names[0]

    k = f"{service_name}#{region}"
    with _client_instance_lock:
        instance = _caches.get(k)
        if not instance:
            instance = EzClient(lazy_init=lambda: _lazy_init(service_name=service_name, region=region))
            _caches[k] = instance
        return instance


def is_regional_service(service_name: str):
    regional_services = ["vmhub", "connection-service"]
    return service_name in regional_services


def service_client(service_name: str, region: str = None, hdc: str = None):
    if is_regional_service(service_name):
        return regional_service_client(service_name, region)
    else:
        return hdc_service_client(service_name, hdc)


class default_crud:
    def __init__(self, client, base_context: str, resource_type_name: str):
        self._client_impl = client
        self._base_context = base_context
        self._resource_type_name = resource_type_name

    def _client(self):
        if callable(self._client_impl):
            self._client_impl = self._client_impl()
        elif isinstance(self._client_impl, str):
            self._client_impl = hdc_service_client(self._client_impl)
        else:
            pass
        if isinstance(self._client_impl, EzClient):
            return self._client_impl
        raise CtxpException(f"Invalid client implementation: {self._client_impl}")

    def get(self, id: str, org_id: str, **kwargs):
        if org_id:
            kwargs["org_id"] = org_id
            kwargs["orgId"] = org_id
        url = with_query(f"{self._base_context}/{id}", **kwargs)
        # print(url)
        return self._client().get(url)

    def list(self, org_id: str, fn_filter: Callable = None, **kwargs) -> list:
        if org_id:
            kwargs["org_id"] = org_id
            kwargs["orgId"] = org_id

        def _get_page(query_string):
            url = self._base_context + "?" + query_string
            # print(url)
            return self._client().get(url)

        return PageRequest(_get_page, fn_filter, **kwargs).get()

    def items(self, org_id: str, fn_filter: Callable = None, **kwargs) -> Iterator:
        if org_id:
            kwargs["org_id"] = org_id
            kwargs["orgId"] = org_id

        def _get_page(query_string):
            url = self._base_context + "?" + query_string
            return self._client().get(url)

        return PageRequest(_get_page, fn_filter, **kwargs).items()

    def create(self, payload: dict, headers: dict = None, **kwargs):
        url = with_query(f"{self._base_context}", **kwargs)
        # print(url)
        # import json
        # print(json.dumps(payload, indent=4))
        if isinstance(payload, str):
            return self._client().post(url, text=payload, headers=headers)
        if isinstance(payload, dict):
            return self._client().post(url, json=payload, headers=headers)
        return self._client().post(url, json=payload, headers=headers)

    def upload(self, files, **kwargs):
        url = with_query(f"{self._base_context}", **kwargs)
        return self._client().post(url, files=files)

    def delete(self, id: str, org_id: str, **kwargs):
        if org_id:
            kwargs["org_id"] = org_id
            kwargs["orgId"] = org_id
        url = with_query(f"{self._base_context}/{id}", **kwargs)
        # print(url)
        return self._client().delete(url)

    def wait_for_deleted(self, id: str, org_id: str, timeout: str, fn_is_error: Callable = None):
        name = self._resource_type_name + "/" + id

        def fn_get():
            return self.get(id, org_id)

        return wait_for_res_deleted(name, fn_get, timeout=timeout, fn_is_error=fn_is_error)

    def update(self, id: str, org_id: str, data: dict, **kwargs):
        if org_id:
            kwargs["org_id"] = org_id
            kwargs["orgId"] = org_id
        url = with_query(f"{self._base_context}/{id}")
        return self._client().patch(url, data)


def _parse_timeout(timeout: str):
    if isinstance(timeout, int):
        return timeout
    if isinstance(timeout, str):
        return duration.to_seconds(timeout)

    raise CtxpException(f"Invalid timout. Type={type(timeout).__name__}, value={timeout}")


def wait_for_res_deleted(
    resource_name: str,
    fn_get: Callable,
    timeout: str,
    polling_interval: int = 10,
    fn_is_error: Callable = None,
):
    timeout_seconds = _parse_timeout(timeout)
    polling_interval_seconds = _parse_timeout(polling_interval)
    if polling_interval_seconds < 3:
        polling_interval_seconds = 3
    start = time.time()
    while True:
        t = fn_get()
        if t is None:
            return
        if fn_is_error:
            if fn_is_error(t):
                msg = f"Failed deleting resource '{resource_name}', resource in Error state."
                raise CtxpException(msg)

        now = time.time()
        remaining_seconds = timeout_seconds - (now - start)
        if remaining_seconds < 1:
            msg = f"Timeout waiting for resource '{resource_name}' to be deleted."
            raise TimeoutError(msg)
        sleep_seconds = remaining_seconds
        if sleep_seconds > polling_interval_seconds:
            sleep_seconds = polling_interval_seconds
        time.sleep(sleep_seconds)


# flake8: noqa: E731
def wait_for_res_status(
    resource_name: str,
    fn_get: Callable,
    get_status: Callable,
    status_map: dict = None,
    is_ready: Callable = None,
    is_error: Callable = None,
    is_transition: Callable = None,
    timeout: str = "10m",
    polling_interval: str = "20s",
    not_found_as_success: bool = False,
):
    timeout_seconds = _parse_timeout(timeout)
    polling_interval_seconds = _parse_timeout(polling_interval)
    if polling_interval_seconds < 3:
        polling_interval_seconds = 3
    start = time.time()
    prefix = f"Error waiting for resource {resource_name}: "

    if isinstance(get_status, str):
        field_name = get_status
        get_status = lambda t: t[field_name]
    if status_map:
        if is_ready:
            raise CtxpException("Can not specify is_ready when status_map is provided.")
        if is_error:
            raise CtxpException("Can not specify is_error when status_map is provided.")
        if is_transition:
            raise CtxpException("Can not specify is_transition when status_map is provided.")

        ready_status = status_map["ready"]
        error_status = status_map["error"]
        transition_status = status_map["transition"]
        if isinstance(ready_status, str):
            ready_status = [ready_status]
        if isinstance(error_status, str):
            error_status = [error_status]
        if isinstance(transition_status, str):
            transition_status = [transition_status]
        is_ready = lambda s: s in ready_status
        is_error = lambda s: error_status is None or s in error_status
        is_transition = lambda s: transition_status is None or s in transition_status
    else:
        if not is_ready:
            raise CtxpException("Either status_map or is_ready must be specified.")
        if not is_error:
            raise CtxpException("Either status_map or is_error must be specified.")
        if not is_transition:
            raise CtxpException("Either status_map or is_transition must be specified.")
        ready_status = None
        error_status = None
        transition_status = None

    while True:
        t = fn_get()
        if t is None:
            if not_found_as_success:
                return
            raise CtxpException(prefix + "Not found.")
        status = get_status(t)
        if is_error(status):
            msg = prefix + f"Unexpected terminal state. Actual={status}"
            if ready_status:
                msg += f", expected={ready_status}"
            print("-- DUMP START --", file=sys.stderr)
            print(json.dumps(t, indent=4), file=sys.stderr)
            print("-- DUMP END --", file=sys.stderr)
            raise CtxpException(msg)
        if is_ready(status):
            return t
        if not is_transition(status):
            raise CtxpException(prefix + f"Unexpected status: {status}. If this is a transition, add it to status_map['transition'].")

        now = time.time()
        remaining_seconds = timeout_seconds - (now - start)
        if remaining_seconds < 1:
            msg = prefix + f"Timeout. Current: {status}."
            if ready_status:
                msg += f" Expected: {ready_status}."
            raise TimeoutError(msg)
        sleep_seconds = remaining_seconds
        if sleep_seconds > polling_interval_seconds:
            sleep_seconds = polling_interval_seconds

        exit.sleep(sleep_seconds)
