import math
import threading
import time

from pypos.core.base_service import BaseService
from pypos.modules.auth.config.auth_config import get_login_security_guard_config


# edited by glg
class LoginAttemptGuardService(BaseService):
    SCOPE_LOGIN = "login"
    SCOPE_LOGIN_GLOBAL = "login_global"
    SCOPE_OFFLINE_ADMIN = "offline_admin"
    SCOPE_OFFLINE_ADMIN_GLOBAL = "offline_admin_global"
    SCOPE_MASTER_PASSWORD = "master_password"

    _LOCK = threading.Lock()
    _STATE = {}

    def __init__(self, policy_getter=get_login_security_guard_config, now_fn=None):
        super().__init__()
        self.policy_getter = policy_getter
        self.now_fn = now_fn or time.monotonic

    def _now(self):
        return float(self.now_fn())

    @staticmethod
    def _normalize_identity(identity):
        text = str(identity or "").strip().lower()
        return text or "_anonymous_"

    @staticmethod
    def _to_int(value, default, minimum):
        try:
            parsed = int(value)
        except (TypeError, ValueError):
            parsed = int(default)
        return max(int(minimum), parsed)

    def _resolve_policy(self, scope):
        raw = self.policy_getter() if callable(self.policy_getter) else {}
        cfg = raw if isinstance(raw, dict) else {}
        enabled = bool(cfg.get("enabled", True))
        scope_cfg = cfg.get(str(scope or "").strip()) if isinstance(cfg.get(str(scope or "").strip()), dict) else {}
        return {
            "enabled": enabled,
            "window_seconds": self._to_int(scope_cfg.get("window_seconds"), default=300, minimum=1),
            "max_attempts": self._to_int(scope_cfg.get("max_attempts"), default=5, minimum=1),
            "lockout_seconds": self._to_int(scope_cfg.get("lockout_seconds"), default=300, minimum=1),
        }

    @staticmethod
    def _prune_failures(failures, now_ts, window_seconds):
        if not isinstance(failures, list):
            return []
        min_ts = float(now_ts) - float(window_seconds)
        cleaned = []
        for item in failures:
            try:
                value = float(item)
            except (TypeError, ValueError):
                continue
            if value >= min_ts:
                cleaned.append(value)
        return cleaned

    def _make_key(self, scope, identity):
        safe_scope = str(scope or self.SCOPE_LOGIN).strip().lower() or self.SCOPE_LOGIN
        safe_identity = self._normalize_identity(identity)
        return f"{safe_scope}:{safe_identity}"

    def check_attempt(self, scope, identity):
        policy = self._resolve_policy(scope)
        now_ts = self._now()
        if not policy["enabled"]:
            return {
                "locked": False,
                "remaining_seconds": 0,
                "fail_count": 0,
                "max_attempts": int(policy["max_attempts"]),
            }

        key = self._make_key(scope, identity)
        with self._LOCK:
            entry = self._STATE.get(key)
            if not isinstance(entry, dict):
                return {
                    "locked": False,
                    "remaining_seconds": 0,
                    "fail_count": 0,
                    "max_attempts": int(policy["max_attempts"]),
                }

            failures = self._prune_failures(
                entry.get("failures"),
                now_ts=now_ts,
                window_seconds=policy["window_seconds"],
            )
            entry["failures"] = failures
            locked_until = float(entry.get("locked_until") or 0.0)
            if locked_until > now_ts:
                remaining = int(max(1, math.ceil(locked_until - now_ts)))
                self._STATE[key] = entry
                return {
                    "locked": True,
                    "remaining_seconds": remaining,
                    "fail_count": len(failures),
                    "max_attempts": int(policy["max_attempts"]),
                }

            entry["locked_until"] = 0.0
            if failures:
                self._STATE[key] = entry
            else:
                self._STATE.pop(key, None)
            return {
                "locked": False,
                "remaining_seconds": 0,
                "fail_count": len(failures),
                "max_attempts": int(policy["max_attempts"]),
            }

    def record_failure(self, scope, identity):
        policy = self._resolve_policy(scope)
        now_ts = self._now()
        if not policy["enabled"]:
            return {
                "locked": False,
                "remaining_seconds": 0,
                "fail_count": 0,
                "max_attempts": int(policy["max_attempts"]),
            }

        key = self._make_key(scope, identity)
        with self._LOCK:
            entry = self._STATE.get(key) if isinstance(self._STATE.get(key), dict) else {}
            failures = self._prune_failures(
                entry.get("failures"),
                now_ts=now_ts,
                window_seconds=policy["window_seconds"],
            )
            failures.append(float(now_ts))
            locked_until = float(entry.get("locked_until") or 0.0)
            is_locked = locked_until > now_ts

            if len(failures) >= int(policy["max_attempts"]):
                locked_until = float(now_ts) + float(policy["lockout_seconds"])
                is_locked = True

            entry["failures"] = failures
            entry["locked_until"] = locked_until if is_locked else 0.0
            self._STATE[key] = entry

            remaining = int(max(1, math.ceil(locked_until - now_ts))) if is_locked else 0
            return {
                "locked": bool(is_locked),
                "remaining_seconds": remaining,
                "fail_count": len(failures),
                "max_attempts": int(policy["max_attempts"]),
            }

    def reset_attempt(self, scope, identity):
        key = self._make_key(scope, identity)
        with self._LOCK:
            self._STATE.pop(key, None)

    @staticmethod
    def build_lock_message(label, state):
        safe_label = str(label or "autentikasi").strip()
        remaining = 0
        if isinstance(state, dict):
            try:
                remaining = int(state.get("remaining_seconds") or 0)
            except (TypeError, ValueError):
                remaining = 0
        if remaining <= 0:
            return f"Terlalu banyak percobaan {safe_label}. Silakan coba lagi nanti."
        return (
            f"Terlalu banyak percobaan {safe_label}. "
            f"Coba lagi dalam {remaining} detik."
        )

    # edited by glg
    @classmethod
    def _clear_all_for_test(cls):
        with cls._LOCK:
            cls._STATE.clear()
