import re
from datetime import datetime, timedelta

try:
    import requests
except (ImportError, ModuleNotFoundError):
    requests = None

from pypos.core.utils.app_state_utils import get_value, set_value
from pypos.core.utils.config_utils import get_current_server_hash
from pypos.modules.sinkronisasi.config import (
    get_sync_circuit_breaker_enabled,
    get_sync_circuit_failure_threshold,
    get_sync_circuit_open_seconds,
    get_sync_circuit_retryable_statuses,
)


class SyncCircuitBreakerService:
    _STATUS_PATTERNS = (
        re.compile(r"status\s*[=:]\s*(\d{3})", flags=re.IGNORECASE),
        re.compile(r"http[_\s-]*error[_\s-]*(\d{3})", flags=re.IGNORECASE),
        re.compile(r"\((\d{3})\)"),
    )

    def __init__(
        self,
        enabled_getter=get_sync_circuit_breaker_enabled,
        threshold_getter=get_sync_circuit_failure_threshold,
        open_seconds_getter=get_sync_circuit_open_seconds,
        retryable_status_getter=get_sync_circuit_retryable_statuses,
        state_getter=get_value,
        state_setter=set_value,
        server_hash_getter=get_current_server_hash,
    ):
        self.enabled_getter = enabled_getter
        self.threshold_getter = threshold_getter
        self.open_seconds_getter = open_seconds_getter
        self.retryable_status_getter = retryable_status_getter
        self.state_getter = state_getter
        self.state_setter = state_setter
        self.server_hash_getter = server_hash_getter

    def _scope(self):
        server_hash = str(self.server_hash_getter() or "default")
        return server_hash[:16] if server_hash else "default"

    def _state_key(self, name):
        return f"sync_circuit:{self._scope()}:{name}"

    def _get_int(self, key_name, default_value=0):
        raw = self.state_getter(self._state_key(key_name), str(default_value))
        try:
            return int(raw)
        except (TypeError, ValueError):
            return int(default_value)

    def _set_int(self, key_name, value):
        self.state_setter(self._state_key(key_name), str(int(value)))

    def _get_open_until(self):
        return str(self.state_getter(self._state_key("open_until"), "") or "").strip()

    def _set_open_until(self, value):
        self.state_setter(self._state_key("open_until"), str(value or "").strip())

    def _now(self):
        return datetime.now()

    def _parse_datetime(self, value):
        text = str(value or "").strip()
        if not text:
            return None
        for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
            try:
                return datetime.strptime(text, fmt)
            except (TypeError, ValueError):
                continue
        return None

    def is_enabled(self):
        return bool(self.enabled_getter())

    def get_failure_count(self):
        return self._get_int("failure_count", 0)

    def get_open_remaining_seconds(self):
        open_until = self._parse_datetime(self._get_open_until())
        if open_until is None:
            return 0
        remaining = int((open_until - self._now()).total_seconds())
        return max(0, remaining)

    def is_open(self):
        return self.get_open_remaining_seconds() > 0

    def guard_or_raise(self):
        if not self.is_enabled():
            return
        remaining = self.get_open_remaining_seconds()
        if remaining <= 0:
            return
        raise RuntimeError(
            f"Sinkronisasi dijeda sementara karena koneksi belum stabil. Coba lagi dalam {remaining} detik."
        )

    def reset(self):
        self._set_int("failure_count", 0)
        self._set_open_until("")

    def mark_success(self):
        if not self.is_enabled():
            return
        self.reset()

    def mark_failure(self, error):
        if not self.is_enabled():
            return
        if not self.is_retryable_failure(error):
            self.reset()
            return

        threshold = max(1, int(self.threshold_getter()))
        open_seconds = max(5, int(self.open_seconds_getter()))
        failure_count = self.get_failure_count() + 1
        self._set_int("failure_count", failure_count)
        if failure_count < threshold:
            return

        open_until = self._now() + timedelta(seconds=open_seconds)
        self._set_open_until(open_until.strftime("%Y-%m-%d %H:%M:%S"))

    def _extract_http_status(self, error):
        if error is None:
            return None
        if requests is not None and isinstance(error, requests.exceptions.HTTPError):
            response = getattr(error, "response", None)
            status_code = getattr(response, "status_code", None)
            try:
                return int(status_code)
            except (TypeError, ValueError):
                return None
        text = str(error or "")
        for pattern in self._STATUS_PATTERNS:
            match = pattern.search(text)
            if not match:
                continue
            try:
                return int(match.group(1))
            except (TypeError, ValueError):
                continue
        return None

    def is_retryable_failure(self, error):
        if error is None:
            return False

        if requests is not None:
            if isinstance(error, (requests.exceptions.Timeout, requests.exceptions.ConnectionError)):
                return True
            if isinstance(error, requests.exceptions.RequestException):
                status_code = self._extract_http_status(error)
                if status_code is None:
                    return True
                return status_code in set(self.retryable_status_getter())

        status_code = self._extract_http_status(error)
        if status_code is not None:
            return status_code in set(self.retryable_status_getter())

        message = str(error or "").lower()
        retryable_hints = (
            "timeout",
            "timed out",
            "connection error",
            "network",
            "temporarily unavailable",
            "service unavailable",
            "gateway timeout",
            "bad gateway",
        )
        for hint in retryable_hints:
            if hint in message:
                return True
        return False
