"""Anti-spam AI — heuristic-based spam scoring.

A lightweight spam detector that scores messages based on multiple signals:
  • Account age (new accounts are suspicious)
  • Message similarity to recent messages
  • Caps-lock / shouting ratio
  • Repeated characters (e.g. "هاااااااا")
  • Link / mention / emoji density
  • Persian/English script mixing patterns
  • Blacklist word matches

Each signal adds to a score. When the score crosses a configurable threshold,
the message is deleted and the user is warned / muted.

No external LLM — this runs offline and is fast.
"""
from __future__ import annotations

import logging
import re
import time
from collections import defaultdict, deque
from dataclasses import dataclass, field
from typing import Deque, Dict, Optional

from telegram import ChatPermissions, Update
from telegram.constants import ParseMode
from telegram.error import BadRequest, Forbidden
from telegram.ext import ContextTypes

from bot import db

logger = logging.getLogger(__name__)


# ───────── Scoring configuration ─────────
@dataclass
class SpamConfig:
    enabled: bool = False
    score_threshold: float = 7.0
    action: str = "delete"  # "delete" | "warn" | "mute" | "ban"
    mute_minutes: int = 30
    check_account_age: bool = True
    new_account_days: int = 7
    new_account_score: float = 2.0
    check_similarity: bool = True
    similarity_threshold: float = 0.85
    similarity_score: float = 3.0
    check_caps: bool = True
    caps_ratio_threshold: float = 0.7
    min_length_for_caps: int = 20
    caps_score: float = 1.0
    check_repeating: bool = True
    repeat_min_run: int = 6
    repeat_score: float = 1.5
    check_links: bool = True
    max_links_per_msg: int = 2
    extra_link_score: float = 1.0
    check_mentions: bool = True
    max_mentions_per_msg: int = 4
    extra_mention_score: float = 1.0
    check_emoji: bool = True
    emoji_ratio_threshold: float = 0.4
    emoji_score: float = 0.5
    check_keyword_blacklist: bool = True
    blacklist_score: float = 3.0


def config_from_settings(settings: dict) -> SpamConfig:
    c = SpamConfig()
    c.enabled = bool(settings.get("antispam_enabled", False))
    c.score_threshold = float(settings.get("antispam_threshold", 7.0))
    c.action = settings.get("antispam_action", "delete") or "delete"
    c.mute_minutes = int(settings.get("antispam_mute_minutes", 30))
    c.check_account_age = bool(settings.get("antispam_check_age", True))
    c.new_account_days = int(settings.get("antispam_new_days", 7))
    c.check_similarity = bool(settings.get("antispam_check_sim", True))
    return c


# ───────── Message history for similarity check ─────────
class ChatHistory:
    """Per-chat ring buffer of recent messages for similarity comparison."""

    def __init__(self, maxlen: int = 30, max_chats: int = 2000) -> None:
        self._buf: Dict[int, Deque[tuple[int, str]]] = defaultdict(lambda: deque(maxlen=maxlen))
        self._max_chats = max_chats

    def add(self, chat_id: int, user_id: int, text: str) -> None:
        if len(self._buf) >= self._max_chats and chat_id not in self._buf:
            # Evict oldest chat (LRU)
            oldest = min(self._buf.keys(), key=lambda k: self._buf[k][-1][0] if self._buf[k] else 0)
            del self._buf[oldest]
        self._buf[chat_id].append((user_id, text))

    def similarity(self, chat_id: int, text: str) -> float:
        """Return the highest Jaccard similarity (0..1) against recent messages in this chat."""
        buf = self._buf.get(chat_id)
        if not buf:
            return 0.0
        a = set(_normalize_tokens(text))
        if not a:
            return 0.0
        best = 0.0
        for _, prev in buf:
            b = set(_normalize_tokens(prev))
            if not b:
                continue
            inter = len(a & b)
            union = len(a | b)
            if union == 0:
                continue
            sim = inter / union
            if sim > best:
                best = sim
        return best


def _normalize_tokens(text: str) -> list[str]:
    """Tokenize and normalize for similarity comparison."""
    return re.findall(r"\w+", text.lower(), flags=re.UNICODE)


# ───────── Feature extractors ─────────
URL_RE = re.compile(
    r"(?:(?:https?|ftp)://|www\.)[^\s<>\"']+|(?:t\.me/|telegram\.me/)[^\s<>\"']+",
    re.IGNORECASE,
)
MENTION_RE = re.compile(r"@\w+")
EMOJI_RE = re.compile(
    r"[\U0001F300-\U0001FAFF\U0001F600-\U0001F64F\U0001F680-\U0001F6FF\u2600-\u27BF]+"
)
REPEAT_RE = re.compile(r"(.)\1{" + str(5) + ",}")
CAPS_RE = re.compile(r"[A-Z]")


@dataclass
class Score:
    total: float = 0.0
    reasons: list[str] = field(default_factory=list)

    def add(self, value: float, reason: str) -> None:
        if value > 0:
            self.total += value
            self.reasons.append(reason)


def score_message(
    message,
    user,
    cfg: SpamConfig,
    history: ChatHistory,
    blacklist_words: set[str],
) -> Score:
    s = Score()
    text = message.text or message.caption or ""
    if not text:
        return s

    # Account age — disabled in v7 (requires async DB lookup)
    # Enable by adding first_seen tracking to db.py and making score_message async

    # Similarity
    if cfg.check_similarity and history is not None:
        sim = history.similarity(message.chat_id, text)
        if sim >= cfg.similarity_threshold:
            s.add(cfg.similarity_score, f"تکرار ({int(sim * 100)}%)")

    # Caps
    if cfg.check_caps and len(text) >= cfg.min_length_for_caps:
        letters = [c for c in text if c.isalpha()]
        if letters:
            upper = sum(1 for c in letters if c.isupper())
            ratio = upper / len(letters)
            if ratio >= cfg.caps_ratio_threshold:
                s.add(cfg.caps_score, f"حروف بزرگ ({int(ratio * 100)}%)")

    # Repeating
    if cfg.check_repeating and REPEAT_RE.search(text):
        s.add(cfg.repeat_score, "تکرار حروف")

    # Links
    if cfg.check_links:
        n_links = len(URL_RE.findall(text))
        if n_links > cfg.max_links_per_msg:
            extra = n_links - cfg.max_links_per_msg
            s.add(cfg.extra_link_score * extra, f"تعداد لینک ({n_links})")

    # Mentions
    if cfg.check_mentions:
        n_mentions = len(MENTION_RE.findall(text))
        if n_mentions > cfg.max_mentions_per_msg:
            extra = n_mentions - cfg.max_mentions_per_msg
            s.add(cfg.extra_mention_score * extra, f"تعداد منشن ({n_mentions})")

    # Emoji flood
    if cfg.check_emoji:
        emojis = EMOJI_RE.findall(text)
        if emojis:
            ratio = sum(len(e) for e in emojis) / max(1, len(text))
            if ratio >= cfg.emoji_ratio_threshold:
                s.add(cfg.emoji_score, "ایموجی زیاد")

    # Keyword blacklist
    if cfg.check_keyword_blacklist and blacklist_words:
        low = text.lower()
        for w in blacklist_words:
            if w and w.lower() in low:
                s.add(cfg.blacklist_score, f"کلمه سیاه ({w})")
                break

    return s


# ───────── Action handlers ─────────
async def apply_action(
    context: ContextTypes.DEFAULT_TYPE,
    score: Score,
    cfg: SpamConfig,
    chat_id: int,
    user_id: int,
    message,
) -> None:
    """Apply the configured action for spam (delete + warn/mute/ban)."""
    try:
        await message.delete()
    except (BadRequest, Forbidden):
        pass

    if cfg.action == "delete":
        return
    if cfg.action == "warn":
        try:
            await context.bot.send_message(
                chat_id,
                f"پیامت اسپم تشخیص داده شد (امتیاز {score.total:.1f})\n"
                f"{', '.join(score.reasons[:3])}",
                parse_mode=ParseMode.HTML,
            )
        except (BadRequest, Forbidden):
            pass
        return
    if cfg.action == "mute":
        from datetime import datetime, timedelta
        until = datetime.utcnow() + timedelta(minutes=cfg.mute_minutes)
        try:
            await context.bot.restrict_chat_member(
                chat_id, user_id,
                permissions=ChatPermissions(),  # All permissions disabled
                until_date=until,
            )
        except (BadRequest, Forbidden):
            pass
    elif cfg.action == "ban":
        try:
            await context.bot.ban_chat_member(chat_id, user_id)
        except (BadRequest, Forbidden):
            pass


# ───────── Singleton history ─────────
_history: Optional[ChatHistory] = None


def get_history() -> ChatHistory:
    global _history
    if _history is None:
        _history = ChatHistory()
    return _history


# ───────── Command: toggle antispam ─────────
async def setantispam_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
    """Configure antispam: /setantispam on|off|<threshold>"""
    chat = update.effective_chat
    text = (update.message.text or "").split()
    if len(text) < 2:
        cur = await db.get_setting(chat.id, "antispam_enabled")
        thr = await db.get_setting(chat.id, "antispam_threshold")
        action = await db.get_setting(chat.id, "antispam_action") or "delete"
        await update.message.reply_text(
            f"<b>ضد اسپم</b>\n"
            f"وضعیت: <b>{'فعال' if cur else 'غیرفعال'}</b>\n"
            f"آستانه: <b>{thr or 7.0}</b>\n"
            f"عملکرد: <b>{action}</b>\n\n"
            f"دستورات:\n"
            f"• <code>setantispam on/off</code>\n"
            f"• <code>setantispam threshold 8</code>\n"
            f"• <code>setantispam action delete/warn/mute/ban</code>\n"
            f"• <code>setantispam status</code>",
            parse_mode=ParseMode.HTML,
        )
        return

    arg = text[1].lower()
    if arg == "on":
        await db.set_setting(chat.id, "antispam_enabled", True)
        await update.message.reply_text("ضد اسپم فعال شد.")
    elif arg == "off":
        await db.set_setting(chat.id, "antispam_enabled", False)
        await update.message.reply_text("ضد اسپم غیرفعال شد.")
    elif arg == "threshold" and len(text) >= 3:
        try:
            v = float(text[2])
        except ValueError:
            await update.message.reply_text("مقدار اشتباهه")
            return
        await db.set_setting(chat.id, "antispam_threshold", v)
        await update.message.reply_text(f"آستانه اسپم روی <b>{v}</b> تنظیم شد.", parse_mode=ParseMode.HTML)
    elif arg == "action" and len(text) >= 3:
        if text[2] not in ("delete", "warn", "mute", "ban"):
            await update.message.reply_text("عملکرد اشتباهه")
            return
        await db.set_setting(chat.id, "antispam_action", text[2])
        await update.message.reply_text(f"عملکرد اسپم روی <b>{text[2]}</b> تنظیم شد.", parse_mode=ParseMode.HTML)
    else:
        await update.message.reply_text("دستور اشتباهه")


# ───────── Registration ─────────
def register(app) -> None:
    from bot.persian_router import register_mixed
    register_mixed(
        app,
        {
            "antispam": setantispam_cmd,
            "setantispam": setantispam_cmd,
        },
        {
            "ضد اسپم": setantispam_cmd,
            "تنظیم ضد اسپم": setantispam_cmd,
            "ضد اسپم فعال": setantispam_cmd,
            "ضد اسپم غیرفعال": setantispam_cmd,
            "آنتی اسپم": setantispam_cmd,
            "اسپم": setantispam_cmd,
            "ضد اسپم رو فعال کن": setantispam_cmd,
            "ضد اسپم رو غیرفعال کن": setantispam_cmd,
            "وضعیت ضد اسپم": setantispam_cmd,
        },
    )
