from __future__ import annotations
import asyncio
import os
import discord
from discord.ext import commands

from .sessions import SessionManager
from .router import ButtonRouter
from .types import GameCtx, ButtonCtx
from .ratelimit import TokenBucketLimiter, CooldownTable
from .dedup import InflightDedup
from .anticheat.engine import AntiCheat, Rule

class Vision:
    """
    Vision Kernel (production-grade):
    - Signed custom_id routing (anti spoof)
    - One session per channel per game
    - Per-session lock + per-user lock
    - Anti-spam interaction limiter (token bucket) + per-action cooldown
    - Inflight dedup + TTL cache helper (for expensive handlers)
    - Phase machine attached to sessions
    - AntiCheat with strike escalation + kick hook
    """

    def __init__(
        self,
        bot: commands.Bot,
        *,
        secret: str | None = None,
        spam_rate: float = 10,     # tokens
        spam_per: float = 3,       # seconds
        action_cooldown_s: float = 0.15,  # very small to stop double-click spam
        dedup_ttl_s: float = 0.0,   # optional TTL for dedup results
    ):
        self.bot = bot
        self.sessions = SessionManager()
        self.router = ButtonRouter(secret=secret)

        # anti-spam
        self._spam = TokenBucketLimiter(rate=spam_rate, per=spam_per)
        self._cooldowns = CooldownTable()
        self._action_cooldown_s = float(action_cooldown_s)

        # dedup helper (you can use it inside handlers)
        self.dedup = InflightDedup(ttl_seconds=dedup_ttl_s)

        self.anticheat: AntiCheat | None = None

        # game callbacks
        self._button_handlers: dict[str, callable] = {}
        self._ctx_builders: dict[str, callable] = {}  # optional game context enrich

        # hook events once
        bot.add_listener(self._on_interaction, "on_interaction")
        bot.add_listener(self._on_message, "on_message")

    def use_anticheat(self, ac: AntiCheat):
        self.anticheat = ac

        # default kick hook: calls a registered game hook if exists
        async def _kick_hook(msg: discord.Message, ctx: GameCtx, rule: Rule):
            hook = getattr(self, "_kick_handler", None)
            if hook:
                await hook(msg, ctx, rule)
        ac.on_kick = _kick_hook

    def on_kick(self):
        """
        Decorator for kick handler:
        async def kick(msg, ctx, rule): ...
        This is where your game removes player from session, etc.
        """
        def deco(fn):
            self._kick_handler = fn
            return fn
        return deco

    def on_button(self, game_name: str):
        """
        Register a button handler for a game.
        Handler: async def handler(btn_ctx: ButtonCtx, session) -> None
        """
        def deco(fn):
            self._button_handlers[game_name] = fn
            return fn
        return deco

    def ctx_builder(self, game_name: str):
        """
        Optional: add extra fields to ctx.meta or compute phase/player mapping.
        Builder: async def build(ctx: GameCtx, session) -> GameCtx
        """
        def deco(fn):
            self._ctx_builders[game_name] = fn
            return fn
        return deco

    # ──────────────────────────────────────────────
    # Session helpers
    # ──────────────────────────────────────────────

    async def create_session(self, game_name: str, interaction: discord.Interaction) -> tuple[GameCtx | None, str]:
        if not interaction.guild or not interaction.channel:
            return None, "هذا يعمل داخل السيرفر فقط!"
        sess, msg = await self.sessions.create(
            guild_id=interaction.guild.id,
            channel_id=interaction.channel.id,
            host_id=interaction.user.id,
            game_name=game_name,
            session_id_factory=self.router.new_session_id,
        )
        if not sess:
            return None, msg

        ctx = GameCtx(
            guild_id=sess.guild_id,
            channel_id=sess.channel_id,
            game_name=game_name,
            session_id=sess.session_id,
            phase=sess.phases.phase,
            player_id=interaction.user.id,
            in_game=True,
            is_host=True,
            meta=sess.meta,
        )
        return ctx, msg

    def make_button_id(self, game: str, session_id: str, action: str, payload: dict | None = None, *, user_id: int | None = None) -> str:
        """
        Generate signed custom_id.
        user_id != None => private button (only that user can press)
        """
        return self.router.encode(game=game, sid=session_id, act=action, payload=payload or {}, uid=user_id)

    # ──────────────────────────────────────────────
    # Event listeners
    # ──────────────────────────────────────────────

    async def _on_interaction(self, interaction: discord.Interaction):
        if interaction.type != discord.InteractionType.component:
            return

        data = interaction.data or {}
        custom_id = data.get("custom_id")
        if not custom_id:
            return

        routed = self.router.decode(custom_id)
        if not routed:
            return

        if not interaction.guild or not interaction.channel:
            try:
                return await interaction.response.send_message("لازم داخل سيرفر.", ephemeral=True)
            except Exception:
                return

        sess = self.sessions.get(interaction.guild.id, interaction.channel.id, routed.game)
        if not sess or sess.ended or sess.session_id != routed.sid:
            try:
                return await interaction.response.send_message("الجلسة انتهت أو غير موجودة.", ephemeral=True)
            except Exception:
                return

        # private button guard
        if routed.uid is not None and interaction.user.id != routed.uid:
            try:
                return await interaction.response.send_message("هذا الزر مو لك.", ephemeral=True)
            except Exception:
                return

        # anti-spam (per session+user)
        spam_key = f"{sess.session_id}:{interaction.user.id}"
        ok, retry = self._spam.allow(spam_key, cost=1.0)
        if not ok:
            try:
                return await interaction.response.send_message(f"خفف ضغط 😅 انتظر {retry:.1f}s", ephemeral=True)
            except Exception:
                return

        # per-action tiny cooldown (stops double submit)
        cd_key = f"{sess.session_id}:{interaction.user.id}:{routed.act}"
        ok, retry = self._cooldowns.allow(cd_key, self._action_cooldown_s)
        if not ok:
            # silently ignore
            try:
                return await interaction.response.defer(ephemeral=True, thinking=False)
            except Exception:
                return

        # Build ctx
        game_ctx = GameCtx(
            guild_id=sess.guild_id,
            channel_id=sess.channel_id,
            game_name=routed.game,
            session_id=sess.session_id,
            phase=sess.phases.phase,
            player_id=interaction.user.id,
            in_game=True,
            is_host=(interaction.user.id == sess.host_id),
            meta=sess.meta,
        )

        # allow game-specific ctx enrichment
        builder = self._ctx_builders.get(routed.game)
        if builder:
            try:
                res = builder(game_ctx, sess)
                if asyncio.iscoroutine(res):
                    game_ctx = await res
                else:
                    game_ctx = res
            except Exception:
                pass

        btn_ctx = ButtonCtx(interaction=interaction, game_ctx=game_ctx, action=routed.act, payload=routed.payload)

        handler = self._button_handlers.get(routed.game)
        if not handler:
            try:
                return await interaction.response.send_message("لا يوجد handler لهذه اللعبة.", ephemeral=True)
            except Exception:
                return

        # concurrency: per-session + per-user lock (مثل نظامك)
        user_lock = sess.get_user_lock(interaction.user.id)
        async with sess.session_lock:
            async with user_lock:
                await handler(btn_ctx, sess)

    async def _on_message(self, message: discord.Message):
        if message.author.bot:
            return
        if not message.guild or not message.channel:
            return
        if not self.anticheat:
            return

        # find any active session in this channel for registered games
        for game_name in self._button_handlers.keys():
            sess = self.sessions.get(message.guild.id, message.channel.id, game_name)
            if sess and not sess.ended:
                ctx = GameCtx(
                    guild_id=sess.guild_id,
                    channel_id=sess.channel_id,
                    game_name=game_name,
                    session_id=sess.session_id,
                    phase=sess.phases.phase,
                    player_id=message.author.id,
                    in_game=True,
                    is_host=(message.author.id == sess.host_id),
                    meta=sess.meta,
                )
                await self.anticheat.handle_message(message, ctx)
                break