# modules/nick_nudge/nick_nudge.py
import asyncio
import time
from typing import Optional, Tuple
from collections import defaultdict

import discord
from discord.ext import commands
from discord import app_commands

from mod_perms import is_moderator_userid, require_mod_interaction
from modules.common.emoji_accept import is_accept
from modules.common.settings import cfg  # ENV-first config helper

CHECK = '✅'          # approved/verified
CROSS = '❌'          # reject / no
PENDING = '✔️'        # heavy check mark = pending claim
ACCEPT = {CHECK, '🫡'}
NO_MENTIONS = discord.AllowedMentions.none()

# Per-user in-process lock to prevent duplicate reviews from concurrent reactions
_user_locks = defaultdict(asyncio.Lock)


def _ts_rel(ts: Optional[float] = None) -> str:
    """Discord relative timestamp like <t:12345:R>."""
    if ts is None:
        ts = time.time()
    return f"<t:{int(ts)}:R>"


class NickNudgeCog(commands.Cog):
    """
    Nickname review flow:
      - Atomic transition to 'pending' and open exactly ONE review.
      - Mods: ✅ approve -> mark verified; ❌ reject -> clear claim.
      - If a verified user changes their nickname, verification is revoked automatically.
    Data keys used in data_manager:
      • agreed_nickname: [user_id]
      • nick_claim_pending: [user_id]
      • nick_verified: [user_id]
      • nick_reviews: [{ message_id, guild_id, user_id, status, ... }]
      • nick_verified_name: [{ guild_id, user_id, nick, ts }]
    """

    def __init__(self, bot):
        self.bot = bot
        r = cfg(bot)

        self.modlog_channel_id = r.int('modlog_channel_id', 0)
        self.mod_channel_id = r.int('mod_channel_id', 0)

        self.loop_enabled = r.bool('nick_nudge_loop_enabled', False)
        self._task = asyncio.create_task(self._nudge_loop()) if self.loop_enabled else None

    # ---------- utils ----------

    def cog_unload(self):
        try:
            if self._task:
                self._task.cancel()
        except Exception:
            pass

    async def _modlog(self, guild: discord.Guild, content: str):
        if not self.modlog_channel_id:
            return
        ch = guild.get_channel(self.modlog_channel_id)
        if ch:
            try:
                await ch.send(content, allowed_mentions=NO_MENTIONS)
            except Exception:
                pass
        self.bot.data_manager.add('modlog', {'ts': time.time(), 'guild_id': guild.id, 'content': content})

    async def _find_last_nick_change(self, guild: discord.Guild, member: discord.Member) -> Tuple[Optional[str], Optional[str]]:
        try:
            async for entry in guild.audit_logs(limit=10, action=discord.AuditLogAction.member_update):
                if entry.target.id != member.id or not entry.changes:
                    continue
                before_n = getattr(entry.changes.before, 'get', lambda *_: None)('nick')
                after_n = getattr(entry.changes.after, 'get', lambda *_: None)('nick')
                if before_n is not None or after_n is not None:
                    return before_n, after_n
        except Exception:
            pass
        return None, None

    # ---------- atomic entry point used by all claim sources ----------

    async def ensure_pending_and_maybe_open(self, guild: discord.Guild, member: discord.Member, source: str):
        """
        Atomically:
          - set pending (idempotent)
          - open ONE review if this is the first transition to pending
        Prevents duplicate reviews when multiple reactions/commands fire.
        """
        if not guild or not self.mod_channel_id or member.bot:
            return

        newly_pending = False
        lock = _user_locks[member.id]
        async with lock:
            dm = self.bot.data_manager

            # If a pending review already exists, bail out
            for r in dm.get('nick_reviews'):
                if r.get('guild_id') == guild.id and r.get('user_id') == member.id and r.get('status') == 'pending':
                    return

            # Mark "agreed" and flip to pending if not already
            if member.id not in dm.get('agreed_nickname'):
                dm.add('agreed_nickname', int(member.id))
            dm.remove('nick_verified', lambda x: x == member.id)
            if member.id not in dm.get('nick_claim_pending'):
                dm.add('nick_claim_pending', int(member.id))
                newly_pending = True

        if newly_pending:
            try:
                await self.start_nick_review(guild, member, source=source)
            except Exception:
                # Roll back pending on failure so the user can try again
                try:
                    dm = self.bot.data_manager
                    dm.remove('nick_claim_pending', lambda x: x == member.id)
                except Exception:
                    pass

    # ---------- public API ----------

    async def start_nick_review(self, guild: discord.Guild, member: discord.Member, source: str = "claim"):
        if not guild or not self.mod_channel_id:
            return

        mod_ch = guild.get_channel(self.mod_channel_id)
        if not mod_ch:
            return

        before_n, _ = await self._find_last_nick_change(guild, member)
        now_ts = int(time.time())

        title = "📝 **Nickname Verification Request**"
        who = f"User: {member.mention} (`{member.id}`)"
        change = f"Claimed {_ts_rel(now_ts)}"
        from_to = f"From: {repr(before_n) if before_n is not None else 'unknown'} → To: {repr(member.nick) if member.nick else 'None'}"
        method = f"Method: {'/nick_same' if source == 'nick_same' else 'reaction'}"
        instructions = "Moderators: react ✅ to **approve** or ❌ to **reject**."
        content = f"{title}\n{who}\n{from_to}\n{method}\n{change}\n\n{instructions}"

        try:
            msg = await mod_ch.send(content, allowed_mentions=NO_MENTIONS)
            await msg.add_reaction(CHECK)
            await msg.add_reaction(CROSS)
        except Exception:
            return

        self.bot.data_manager.add('nick_reviews', {
            'message_id': int(msg.id),
            'guild_id': int(guild.id),
            'user_id': int(member.id),
            'before_nick': before_n if before_n is None or isinstance(before_n, str) else str(before_n),
            'claimed_nick': member.nick if member.nick else None,
            'status': 'pending',
            'source': source,
            'ts': now_ts
        })

        await self._modlog(guild, f"🔎 Nickname review opened for {member.mention} — {method} — {_ts_rel(now_ts)}.")

    # ---------- DM nudge loop ----------

    async def _nudge_loop(self):
        await self.bot.wait_until_ready()
        while not self.bot.is_closed():
            try:
                now_t = time.time()
                for guild in self.bot.guilds:
                    for member in guild.members:
                        if member.bot or not member.joined_at:
                            continue
                        if (now_t - member.joined_at.timestamp()) < 24 * 3600:
                            continue
                        dm = self.bot.data_manager
                        if (member.nick and member.nick.strip()):
                            continue
                        if member.id in dm.get('nick_verified') or member.id in dm.get('nick_claim_pending'):
                            continue
                        if member.id in dm.get('nick_nudged'):
                            continue
                        try:
                            dmchan = await member.create_dm()
                            msg = await dmchan.send(
                                "Hey! On this server we require your **server nickname** to match your in-game character name.\n\n"
                                "If your default Discord display name is already identical to your in-game name, react ✅ **or 🫡** below "
                                "or run `/nick_same`. Otherwise, please set your **server nickname** to your in-game name and react ✅ **or 🫡**."
                            )
                            await msg.add_reaction(CHECK)
                            self.bot.data_manager.add('nick_dm_map', {
                                'message_id': int(msg.id),
                                'user_id': int(member.id),
                                'guild_id': int(guild.id),
                                'ts': now_t
                            })
                            self.bot.data_manager.add('nick_nudged', int(member.id))
                            await self._modlog(guild, f"📨 Sent nickname nudge to {member.mention}")
                        except Exception:
                            pass
            except Exception:
                pass
            await asyncio.sleep(1800)

    # ---------- listeners ----------

    @commands.Cog.listener()
    async def on_member_update(self, before: discord.Member, after: discord.Member):
        """Revoke verification if a verified user changes their nick to anything."""
        if before.bot or before.guild != after.guild:
            return
        if before.nick == after.nick:
            return

        dm = self.bot.data_manager
        # Only act if the user is currently verified
        if before.id in dm.get('nick_verified'):
            dm.remove('nick_verified', lambda x: x == before.id)
            dm.remove('nick_claim_pending', lambda x: x == before.id)
            try:
                dm.remove('nick_verified_name', lambda r: r.get('guild_id') == before.guild.id and r.get('user_id') == before.id)
            except Exception:
                pass
            dm.add('nick_verified_name', {
                'guild_id': int(before.guild.id),
                'user_id': int(before.id),
                'nick': after.nick if after.nick else None,
                'ts': int(time.time())
            })

            try:
                await self._modlog(after.guild, f"⚠️ {after.mention} changed nickname; **verification revoked**. They must re-claim for a new review.")
            except Exception:
                pass

            rr = self.bot.get_cog('ReactionRoleCog')
            if rr:
                try:
                    await rr.maybe_apply_full_access(after)
                except Exception:
                    pass
            cards = self.bot.get_cog('UserCardsCog')
            if cards:
                try:
                    await cards.refresh_card(after)
                except Exception:
                    pass

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
        # 1) Handle DM nudge confirmations (user reacts with an accept in DM)
        if payload.guild_id is None and is_accept(payload.emoji) and payload.user_id != self.bot.user.id:
            entry = next((m for m in self.bot.data_manager.get('nick_dm_map') if m['message_id'] == payload.message_id), None)
            if not entry:
                return
            guild = self.bot.get_guild(entry['guild_id'])
            member = guild.get_member(entry['user_id']) if guild else None
            if not member:
                return

            try:
                await self.ensure_pending_and_maybe_open(guild, member, source="nick_same")
            except Exception:
                pass

            self.bot.data_manager.remove('nick_dm_map', lambda m: m['message_id'] == payload.message_id)

            rr = self.bot.get_cog('ReactionRoleCog')
            if rr:
                try:
                    await rr.maybe_apply_full_access(member)
                except Exception:
                    pass
            cards = self.bot.get_cog('UserCardsCog')
            if cards:
                try:
                    await cards.refresh_card(member)
                except Exception:
                    pass
            return

        # 2) Handle moderator review reactions in mod channel
        if payload.guild_id and str(payload.emoji) in (CHECK, CROSS) and payload.user_id != self.bot.user.id:
            if payload.channel_id != self.mod_channel_id or not self.mod_channel_id:
                return
            guild = self.bot.get_guild(payload.guild_id)
            if not guild:
                return
            if not is_moderator_userid(guild, payload.user_id, self.bot):
                return

            reviews = self.bot.data_manager.get('nick_reviews')
            review = next((r for r in reviews if r.get('message_id') == payload.message_id and r.get('guild_id') == guild.id), None)
            if not review or review.get('status') != 'pending':
                return

            member = guild.get_member(int(review['user_id']))
            if not member:
                self.bot.data_manager.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'closed_missing'}), r)[1])
                return

            try:
                ch = self.bot.get_channel(payload.channel_id)
                msg = await ch.fetch_message(payload.message_id)
            except Exception:
                msg = None

            dm = self.bot.data_manager
            now_ts = int(time.time())
            approver = f"<@{payload.user_id}>"

            if str(payload.emoji) == CHECK:
                if member.id not in dm.get('agreed_nickname'):
                    dm.add('agreed_nickname', int(member.id))
                dm.remove('nick_claim_pending', lambda x: x == member.id)
                if member.id not in dm.get('nick_verified'):
                    dm.add('nick_verified', int(member.id))

                try:
                    dm.remove('nick_verified_name', lambda r: r.get('guild_id') == guild.id and r.get('user_id') == member.id)
                except Exception:
                    pass
                dm.add('nick_verified_name', {
                    'guild_id': int(guild.id),
                    'user_id': int(member.id),
                    'nick': member.nick if member.nick else None,
                    'ts': now_ts
                })

                dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'approved', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])

                if msg:
                    try:
                        await msg.clear_reactions()
                    except Exception:
                        pass
                    try:
                        await msg.edit(content=f"✅ **Nickname Approved** for {member.mention} by {approver} — {_ts_rel(now_ts)}")
                    except Exception:
                        pass

                await self._modlog(guild, f"✅ Nickname **verified** for {member.mention} by {approver} — {_ts_rel(now_ts)}.")

                rr = self.bot.get_cog('ReactionRoleCog')
                if rr:
                    try:
                        await rr.maybe_apply_full_access(member)
                    except Exception:
                        pass
                cards = self.bot.get_cog('UserCardsCog')
                if cards:
                    try:
                        await cards.refresh_card(member)
                    except Exception:
                        pass

            else:
                dm.remove('agreed_nickname', lambda x: x == member.id)
                dm.remove('nick_claim_pending', lambda x: x == member.id)
                dm.remove('nick_verified', lambda x: x == member.id)

                dm.update('nick_reviews', lambda r: r is review, lambda r: (r.update({'status': 'rejected', 'decided_ts': now_ts, 'moderator_id': int(payload.user_id)}), r)[1])

                if msg:
                    try:
                        await msg.clear_reactions()
                    except Exception:
                        pass
                    try:
                        await msg.edit(content=f"❌ **Nickname Rejected** for {member.mention} by {approver} — {_ts_rel(now_ts)}")
                    except Exception:
                        pass

                await self._modlog(guild, f"❌ Nickname **rejected** for {member.mention} by {approver} — {_ts_rel(now_ts)}.")

                rr = self.bot.get_cog('ReactionRoleCog')
                if rr:
                    try:
                        await rr.maybe_apply_full_access(member)
                    except Exception:
                        pass
                cards = self.bot.get_cog('UserCardsCog')
                if cards:
                    try:
                        await cards.refresh_card(member)
                    except Exception:
                        pass

    # ---------- Mod commands to manipulate nickname reviews ----------

    @app_commands.command(name="clear_nick_reviews", description="[MOD] Delete all PENDING nickname review records for this server.")
    async def clear_nick_reviews(self, interaction: discord.Interaction):
        """Moderator-only. Clears all 'pending' entries in data_manager['nick_reviews'] for this guild."""
        # Must be used in a guild
        if not interaction.guild:
            return await interaction.response.send_message("Use this in a server.", ephemeral=True)

        # Moderator permission check (your existing gate)
        if not await require_mod_interaction(interaction):
            return  # require_mod_interaction already responded

        dm = self.bot.data_manager

        # Count pending records for this guild
        pending = [
            r for r in dm.get('nick_reviews')
            if r.get('guild_id') == interaction.guild.id and r.get('status') == 'pending'
        ]
        count = len(pending)

        # Remove pending records
        if count:
            dm.remove(
                'nick_reviews',
                lambda r: r.get('guild_id') == interaction.guild.id and r.get('status') == 'pending'
            )

        # Modlog + ephemeral confirmation
        try:
            await self._modlog(
                interaction.guild,
                f"🧹 {interaction.user.mention} cleared **{count}** pending nickname review(s)."
            )
        except Exception:
            pass

        await interaction.response.send_message(
            f"Cleared **{count}** pending nickname review{'s' if count != 1 else ''}.",
            ephemeral=True
        )

    @app_commands.command(name="recreate_nick_reviews", description="[MOD] Scan and recreate any missing pending nickname reviews for this server.")
    async def recreate_nick_reviews(self, interaction: discord.Interaction):
        """Moderator-only bulk fixer for 'grey checkmark' users (claimed but no pending review)."""
        if not interaction.guild:
            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
        if not await require_mod_interaction(interaction):
            return  # already replied

        dm = self.bot.data_manager
        guild = interaction.guild

        agreed = set(dm.get('agreed_nickname'))
        verified = set(dm.get('nick_verified'))
        # Build a quick lookup of existing pending reviews
        pending_reviews = {
            (r.get('guild_id'), r.get('user_id'))
            for r in dm.get('nick_reviews')
            if r.get('status') == 'pending'
        }

        to_fix = []
        for uid in agreed:
            # Needs a review if not verified and no pending review exists
            if (guild.id, uid) not in pending_reviews and uid not in verified:
                m = guild.get_member(uid)
                if m and not m.bot:
                    to_fix.append(m)

        fixed = 0
        skipped = 0
        for member in to_fix:
            try:
                # Clear stale pending so the atomic method will transition and open a new one
                dm.remove('nick_claim_pending', lambda x, _uid=member.id: x == _uid)
                await self.ensure_pending_and_maybe_open(guild, member, source="recreate")
                fixed += 1
            except Exception:
                skipped += 1

        try:
            await self._modlog(guild, f"🛠️ {interaction.user.mention} recreated **{fixed}** nickname review(s); skipped **{skipped}**.")
        except Exception:
            pass

        await interaction.response.send_message(
            f"Recreated **{fixed}** review(s); skipped **{skipped}**.",
            ephemeral=True
        )

    @app_commands.command(name="recreate_nick_review", description="[MOD] Recreate a missing pending nickname review for one user.")
    @app_commands.describe(user="Member to recreate review for")
    async def recreate_nick_review(self, interaction: discord.Interaction, user: discord.Member):
        """Moderator-only single-user fixer."""
        if not interaction.guild:
            return await interaction.response.send_message("Use this in a server.", ephemeral=True)
        if not await require_mod_interaction(interaction):
            return  # already replied

        guild = interaction.guild
        dm = self.bot.data_manager

        # If already verified, nothing to do
        if user.id in dm.get('nick_verified'):
            return await interaction.response.send_message("User is already verified — no review needed.", ephemeral=True)

        # If a pending review already exists, nothing to do
        has_pending = any(
            r.get('guild_id') == guild.id and r.get('user_id') == user.id and r.get('status') == 'pending'
            for r in dm.get('nick_reviews')
        )
        if has_pending:
            return await interaction.response.send_message("A pending review already exists for this user.", ephemeral=True)

        # If they never agreed/claimed, mark claim now so the state is consistent
        if user.id not in dm.get('agreed_nickname'):
            dm.add('agreed_nickname', int(user.id))

        # Clear stale pending flag, then open atomically
        dm.remove('nick_claim_pending', lambda x: x == user.id)

        try:
            await self.ensure_pending_and_maybe_open(guild, user, source="recreate")
        except Exception:
            return await interaction.response.send_message("Failed to create the review (see logs).", ephemeral=True)

        try:
            await self._modlog(guild, f"🛠️ {interaction.user.mention} recreated a nickname review for {user.mention}.")
        except Exception:
            pass

        await interaction.response.send_message("Recreated the nickname review for that user.", ephemeral=True)

async def setup(bot):
    await bot.add_cog(NickNudgeCog(bot))
