import time
import secrets
import threading
import requests

from pypos.core.utils.config_utils import read_endpoint_config, get_http_retry_policy


def _is_qt_ui_thread():
    # edited by glg
    # Deteksi apakah eksekusi berada di UI thread Qt.
    # Jika tidak, jalur retry berjalan normal (tanpa pump event).
    try:
        from PySide6.QtCore import QCoreApplication, QThread
    except (ImportError, ModuleNotFoundError):
        return False
    app = QCoreApplication.instance()
    if app is None:
        return False
    try:
        return bool(QThread.currentThread() == app.thread())
    except (AttributeError, RuntimeError, TypeError):
        return False


def _pump_qt_events(max_ms=15):
    # edited by glg
    # Jaga event loop tetap hidup saat caller menunggu I/O jaringan.
    try:
        from PySide6.QtCore import QCoreApplication, QEventLoop
    except (ImportError, ModuleNotFoundError):
        return
    app = QCoreApplication.instance()
    if app is None:
        return
    try:
        # edited by glg
        # Hindari re-entrancy aksi klik saat HTTP proxy menunggu respon.
        # UI tetap repaint/timer berjalan, tapi input user ditahan sementara.
        exclude_input_flag = getattr(QEventLoop, "ExcludeUserInputEvents", None)
        if exclude_input_flag is None and hasattr(QEventLoop, "ProcessEventsFlag"):
            exclude_input_flag = getattr(QEventLoop.ProcessEventsFlag, "ExcludeUserInputEvents", None)
        if exclude_input_flag is not None:
            app.processEvents(exclude_input_flag, int(max_ms))
        else:
            app.processEvents(QEventLoop.AllEvents, int(max_ms))
    except (AttributeError, RuntimeError, TypeError, ValueError):
        try:
            app.processEvents()
        except (AttributeError, RuntimeError, TypeError, ValueError):
            pass


def _execute_request(req_callable, request_kwargs):
    # edited by glg
    # Jika request dipanggil dari UI thread Qt, jalankan request pada worker
    # agar window tidak "Not Responding" saat endpoint lambat/error.
    if not _is_qt_ui_thread():
        return req_callable(**request_kwargs)

    state = {}
    done_event = threading.Event()

    def _runner():
        try:
            state["response"] = req_callable(**request_kwargs)
        except BaseException as exc:
            state["error"] = exc
        finally:
            done_event.set()

    thread = threading.Thread(target=_runner, daemon=True, name="http-retry-ui-proxy")
    thread.start()

    while not done_event.wait(0.03):
        _pump_qt_events(max_ms=15)

    if "error" in state:
        raise state["error"]
    return state.get("response")


def _sleep_backoff(wait_seconds):
    if wait_seconds <= 0:
        return
    # edited by glg
    # Sleep backoff biasa untuk worker/background thread.
    # Di UI thread, sleep dipecah kecil supaya event loop tetap terpompa.
    if not _is_qt_ui_thread():
        time.sleep(wait_seconds)
        return

    remaining = float(wait_seconds)
    while remaining > 0:
        step = min(0.05, remaining)
        time.sleep(step)
        _pump_qt_events(max_ms=10)
        remaining -= step


def _bounded_jitter(max_jitter):
    # edited by glg
    # Gunakan RNG kriptografis agar tidak memicu B311, meski konteksnya non-kriptografis.
    if max_jitter <= 0:
        return 0.0
    scale = 1_000_000
    return (secrets.randbelow(scale + 1) / float(scale)) * max_jitter


def request_with_retry(
    method,
    url,
    *,
    params=None,
    data=None,
    json=None,
    headers=None,
    timeout=None,
    max_retry=None,
    backoff_sec=None,
    backoff_factor=None,
    max_backoff_sec=None,
    jitter_ratio=None,
    retry_statuses=None,
    session=None,
    retry_on=None,
    request_kwargs=None,
):
    cfg = read_endpoint_config()
    policy = get_http_retry_policy(cfg)

    if timeout is None:
        try:
            timeout = int(cfg.get("request_timeout", 10))
        except (TypeError, ValueError):
            timeout = 10

    max_retry = policy["max_retry"] if max_retry is None else int(max_retry)
    backoff_sec = policy["backoff_sec"] if backoff_sec is None else float(backoff_sec)
    backoff_factor = policy["backoff_factor"] if backoff_factor is None else float(backoff_factor)
    max_backoff_sec = policy["backoff_max_sec"] if max_backoff_sec is None else float(max_backoff_sec)
    jitter_ratio = policy["jitter_ratio"] if jitter_ratio is None else float(jitter_ratio)
    retry_statuses_set = set(policy["retry_statuses_set"])
    if retry_statuses is not None:
        retry_statuses_set = {int(v) for v in (retry_statuses or [])}

    if max_retry < 1:
        max_retry = 1
    if backoff_sec < 0:
        backoff_sec = 0.0
    if backoff_factor < 1.0:
        backoff_factor = 1.0
    if max_backoff_sec <= 0:
        max_backoff_sec = max(1.0, backoff_sec)
    jitter_ratio = max(0.0, min(1.0, jitter_ratio))

    retry_exceptions = tuple(retry_on) if retry_on else (
        requests.exceptions.Timeout,
        requests.exceptions.ConnectionError,
        requests.exceptions.HTTPError,
        requests.exceptions.RequestException,
    )

    req = session.request if session else requests.request
    extra_kwargs = dict(request_kwargs or {})
    last_exc = None

    for attempt in range(1, max_retry + 1):
        try:
            request_kwargs = {
                "method": method,
                "url": url,
                "params": params,
                "data": data,
                "json": json,
                "headers": headers,
                "timeout": timeout,
                **extra_kwargs,
            }
            resp = _execute_request(
                req,
                request_kwargs,
            )
            if resp.status_code >= 400:
                if resp.status_code in retry_statuses_set and attempt < max_retry:
                    raise requests.exceptions.HTTPError(
                        f"HTTP {resp.status_code}",
                        response=resp,
                    )
                resp.raise_for_status()
            return resp
        except retry_exceptions as exc:
            last_exc = exc
            if isinstance(exc, requests.exceptions.HTTPError):
                status_code = getattr(getattr(exc, "response", None), "status_code", None)
                if status_code is not None and int(status_code) not in retry_statuses_set:
                    raise
        except (RuntimeError, TypeError, ValueError, OSError) as exc:
            last_exc = exc

        if attempt < max_retry:
            wait_seconds = backoff_sec * (backoff_factor ** max(0, attempt - 1))
            wait_seconds = min(max_backoff_sec, wait_seconds)
            if wait_seconds > 0 and jitter_ratio > 0:
                jitter = _bounded_jitter(wait_seconds * jitter_ratio)
                wait_seconds = min(max_backoff_sec, wait_seconds + jitter)
            if wait_seconds > 0:
                _sleep_backoff(wait_seconds)

    if last_exc:
        raise last_exc
    raise RuntimeError("request_with_retry gagal tanpa exception detail.")


def get_with_retry(
    url,
    *,
    params=None,
    headers=None,
    timeout=None,
    max_retry=None,
    backoff_sec=None,
    session=None,
    retry_on=None,
    request_kwargs=None,
):
    return request_with_retry(
        "GET",
        url,
        params=params,
        headers=headers,
        timeout=timeout,
        max_retry=max_retry,
        backoff_sec=backoff_sec,
        session=session,
        retry_on=retry_on,
        request_kwargs=request_kwargs,
    )


def post_with_retry(
    url,
    *,
    data=None,
    json=None,
    headers=None,
    timeout=None,
    max_retry=None,
    backoff_sec=None,
    session=None,
    retry_on=None,
    request_kwargs=None,
):
    return request_with_retry(
        "POST",
        url,
        data=data,
        json=json,
        headers=headers,
        timeout=timeout,
        max_retry=max_retry,
        backoff_sec=backoff_sec,
        session=session,
        retry_on=retry_on,
        request_kwargs=request_kwargs,
    )
