"""Feature flag evaluation, sync, and A/B test bucketing.

Provides :class:`FlagManager` for server-side flag evaluation with
deterministic MurmurHash3-based bucketing for experiment assignment.
"""

from __future__ import annotations

import ctypes
import json
import logging
import threading
from typing import Any, Callable, Dict, List, Optional

import httpx

logger = logging.getLogger("kanban_analytics.flags")

# ---------------------------------------------------------------------------
# MurmurHash3 (32-bit) — deterministic bucketing
# ---------------------------------------------------------------------------


def murmurhash3(key: str, seed: int = 0) -> int:
    """MurmurHash3 32-bit hash, matching the JS SDK implementation."""
    data = key.encode("utf-8")
    length = len(data)
    h = ctypes.c_uint32(seed).value
    nblocks = length // 4
    c1 = 0xCC9E2D51
    c2 = 0x1B873593

    for i in range(nblocks):
        offset = i * 4
        k = (
            data[offset]
            | (data[offset + 1] << 8)
            | (data[offset + 2] << 16)
            | (data[offset + 3] << 24)
        )
        k = ctypes.c_uint32(k * c1).value
        k = ctypes.c_uint32((k << 15) | (k >> 17)).value
        k = ctypes.c_uint32(k * c2).value

        h ^= k
        h = ctypes.c_uint32((h << 13) | (h >> 19)).value
        h = ctypes.c_uint32(h * 5 + 0xE6546B64).value

    tail_idx = nblocks * 4
    k = 0
    remainder = length & 3
    if remainder >= 3:
        k ^= data[tail_idx + 2] << 16
    if remainder >= 2:
        k ^= data[tail_idx + 1] << 8
    if remainder >= 1:
        k ^= data[tail_idx]
        k = ctypes.c_uint32(k * c1).value
        k = ctypes.c_uint32((k << 15) | (k >> 17)).value
        k = ctypes.c_uint32(k * c2).value
        h ^= k

    h ^= length
    h ^= h >> 16
    h = ctypes.c_uint32(h * 0x85EBCA6B).value
    h ^= h >> 13
    h = ctypes.c_uint32(h * 0xC2B2AE35).value
    h ^= h >> 16

    return ctypes.c_uint32(h).value


def _hash_bucket(identity: str, key: str) -> int:
    return murmurhash3(identity + key) % 100


# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------

FlagDefinition = Dict[str, Any]
ExperimentDefinition = Dict[str, Any]


# ---------------------------------------------------------------------------
# FlagManager
# ---------------------------------------------------------------------------


class FlagManager:
    """Manages feature flag sync, evaluation, and experiment bucketing."""

    def __init__(
        self,
        api_key: str,
        sync_endpoint: str,
        sync_interval: float = 60.0,
        debug: bool = False,
        timeout: float = 5.0,
    ) -> None:
        self._api_key = api_key
        self._sync_endpoint = sync_endpoint
        self._sync_interval = sync_interval
        self._debug = debug
        self._timeout = timeout

        self._flags: Dict[str, FlagDefinition] = {}
        self._user_id: Optional[str] = None
        self._anonymous_id: Optional[str] = None
        self._user_traits: Dict[str, Any] = {}

        self._lock = threading.Lock()
        self._sync_timer: Optional[threading.Timer] = None
        self._running = False
        self._client: Optional[httpx.Client] = None

    def init(self, anonymous_id: Optional[str] = None, user_id: Optional[str] = None) -> None:
        self._anonymous_id = anonymous_id
        self._user_id = user_id
        self._client = httpx.Client(timeout=self._timeout)
        self._running = True
        self._sync_flags()
        self._schedule_sync()

    def destroy(self) -> None:
        self._running = False
        if self._sync_timer:
            self._sync_timer.cancel()
            self._sync_timer = None
        if self._client:
            self._client.close()
            self._client = None

    def set_user_identity(
        self,
        anonymous_id: Optional[str] = None,
        user_id: Optional[str] = None,
    ) -> None:
        self._anonymous_id = anonymous_id
        self._user_id = user_id

    def set_user_traits(self, traits: Dict[str, Any]) -> None:
        with self._lock:
            self._user_traits.update(traits)

    # -- public evaluation --------------------------------------------------

    def is_enabled(self, flag_name: str, fallback: bool = False) -> bool:
        with self._lock:
            flag = self._flags.get(flag_name)
        if flag is None:
            return fallback
        if not flag.get("enabled", False):
            return False
        return self._evaluate_flag(flag)

    def get_variant(self, experiment_name: str) -> Optional[str]:
        with self._lock:
            flags = list(self._flags.values())
        for flag in flags:
            if not flag.get("enabled", False):
                continue
            for exp in flag.get("experiments", []):
                if exp.get("name") == experiment_name or exp.get("id") == experiment_name:
                    if exp.get("status") != "running":
                        return None
                    return self._assign_variant(
                        self._get_identity(),
                        exp["id"],
                        exp.get("variants", []),
                    )
        return None

    def get_active_experiment_assignments(self) -> Dict[str, str]:
        assignments: Dict[str, str] = {}
        with self._lock:
            flags = list(self._flags.values())
        for flag in flags:
            if not flag.get("enabled", False):
                continue
            if not self._evaluate_flag(flag):
                continue
            for exp in flag.get("experiments", []):
                if exp.get("status") != "running":
                    continue
                variant = self._assign_variant(
                    self._get_identity(),
                    exp["id"],
                    exp.get("variants", []),
                )
                if variant:
                    assignments[exp["id"]] = variant
        return assignments

    def create_experiment_middleware(self) -> Callable:
        """Returns a middleware that attaches experiment context to events."""
        def middleware(event: Dict[str, Any], next_fn: Callable) -> Any:
            assignments = self.get_active_experiment_assignments()
            if assignments:
                context = dict(event.get("context") or {})
                context["experiment"] = assignments
                event = {**event, "context": context}
            return next_fn(event)
        return middleware

    # -- private ------------------------------------------------------------

    def _get_identity(self) -> str:
        return self._user_id or self._anonymous_id or "anonymous"

    def _evaluate_flag(self, flag: FlagDefinition) -> bool:
        if not flag.get("enabled", False):
            return False
        rules = flag.get("rules", [])
        if rules:
            result = self._evaluate_rules(rules)
            if result is not None:
                return result
        rollout = flag.get("rollout_percentage", 100)
        return _hash_bucket(self._get_identity(), flag.get("name", "")) < rollout

    def _evaluate_rules(self, rules: List[Dict[str, Any]]) -> Optional[bool]:
        for rule in rules:
            if self._evaluate_rule(rule):
                return True
        return None

    def _evaluate_rule(self, rule: Dict[str, Any]) -> bool:
        prop = rule.get("property", "")
        operator = rule.get("operator", "")
        value = rule.get("value")

        user_value = self._get_user_property(prop)
        if user_value is None:
            return False

        if operator == "equals":
            return str(user_value) == str(value)
        elif operator == "not_equals":
            return str(user_value) != str(value)
        elif operator == "contains":
            return str(value) in str(user_value)
        elif operator == "gt":
            try:
                return float(user_value) > float(value)
            except (ValueError, TypeError):
                return False
        elif operator == "lt":
            try:
                return float(user_value) < float(value)
            except (ValueError, TypeError):
                return False
        elif operator == "in":
            return isinstance(value, list) and str(user_value) in [str(v) for v in value]
        return False

    def _get_user_property(self, prop: str) -> Any:
        if prop == "user_id":
            return self._user_id
        if prop == "anonymous_id":
            return self._anonymous_id
        with self._lock:
            return self._user_traits.get(prop)

    def _assign_variant(
        self,
        identity: str,
        experiment_id: str,
        variants: List[Dict[str, Any]],
    ) -> Optional[str]:
        if not variants:
            return None
        bucket = _hash_bucket(identity, experiment_id)
        cumulative = 0
        for v in variants:
            cumulative += v.get("weight", 0)
            if bucket < cumulative:
                return v.get("name")
        return variants[-1].get("name")

    def _sync_flags(self) -> None:
        if not self._client:
            return
        try:
            response = self._client.get(
                self._sync_endpoint,
                headers={
                    "x-api-key": self._api_key,
                    "Content-Type": "application/json",
                },
                timeout=self._timeout,
            )
            if response.status_code == 200:
                data = response.json()
                with self._lock:
                    self._flags.clear()
                    for flag in data:
                        self._flags[flag["name"]] = flag
                self._debug_log(f"Synced {len(data)} flags")
            else:
                self._debug_log(f"Flag sync failed: {response.status_code}")
        except Exception as exc:
            self._debug_log(f"Flag sync error: {exc}")

    def _schedule_sync(self) -> None:
        if not self._running:
            return
        self._sync_timer = threading.Timer(self._sync_interval, self._sync_loop)
        self._sync_timer.daemon = True
        self._sync_timer.start()

    def _sync_loop(self) -> None:
        if not self._running:
            return
        self._sync_flags()
        self._schedule_sync()

    def _debug_log(self, message: str) -> None:
        if self._debug:
            logger.debug("[Flags] %s", message)
