from __future__ import annotations

import re
from dataclasses import dataclass

from .models import CATEGORIES, Detection


@dataclass(frozen=True, slots=True)
class Rule:
    category: str
    risk: float
    reason: str
    patterns: tuple[str, ...]


class SafetyClassifier:
    """Local, deterministic line classifier.

    The rule set is intentionally visible and testable. It is not a replacement
    for a professional moderation model, but it makes the complete app work
    without an API key or internet connection.
    """

    def __init__(self) -> None:
        self.external_judge = None
        try:
            from run_easy import judge_text  # type: ignore

            self.external_judge = judge_text
        except Exception:
            self.external_judge = None

        self.rules: tuple[Rule, ...] = (
            Rule(
                "SCAM",
                0.98,
                "Uses urgency, impersonation, prizes, payments, or a request for sensitive information.",
                (
                    r"\b(?:account|email|bank|package|payment|subscription).{0,55}(?:locked|suspended|closed|frozen|deleted|failed)\b",
                    r"\b(?:verify|confirm|send|reply with|enter|provide).{0,42}(?:password|passcode|pin|verification code|social security|bank details|card number|login)\b",
                    r"\b(?:won|winner|selected|prize|gift card|refund|cash reward).{0,65}(?:fee|payment|claim|click|link|redeem)\b",
                    r"\b(?:gift card|cryptocurrency|bitcoin|wire transfer).{0,55}(?:pay|send|code|numbers?|purchase)\b",
                    r"\b(?:act now|within \d+ minutes?|immediately|urgent|final warning).{0,85}(?:click|pay|verify|reply|account|claim)\b",
                    r"\bdo not (?:contact|call|visit).{0,60}(?:company|bank|support|official|police)\b",
                    r"https?://[^\s]*(?:verify|claim|secure|reward|login|update|refund)[^\s]*",
                    r"\b(?:irs|police|government|bank support|tech support).{0,55}(?:pay|gift card|crypto|remote access)\b",
                ),
            ),
            Rule(
                "DANGEROUS_ADVICE",
                0.99,
                "Encourages a preventable physical, electrical, chemical, medication, fire, or driving hazard.",
                (
                    r"\b(?:put|place|leave|use).{0,42}\bmetal\b.{0,42}\bmicrowave\b",
                    r"\bmetal\b.{0,42}\b(?:safe|fine|okay|ok|harmless)\b.{0,42}\bmicrowave\b",
                    r"\bmicrowave\b.{0,42}\bmetal\b.{0,42}\b(?:safe|fine|okay|ok|harmless)\b",
                    r"\b(?:mix|combine).{0,42}\b(?:bleach|ammonia|cleaning chemicals?|cleaners?)\b",
                    r"\b(?:look|stare).{0,35}\bdirectly at (?:the )?sun\b",
                    r"\b(?:touch|approach|pick up).{0,50}\b(?:fallen|downed) power line\b",
                    r"\b(?:ignore|disable|remove).{0,42}\b(?:fire alarm|smoke alarm|carbon monoxide alarm)\b",
                    r"\b(?:take|use|swallow).{0,42}\bmore than (?:the )?(?:recommended|prescribed) (?:dose|amount)\b",
                    r"\b(?:drive|keep driving).{0,50}\b(?:tired|sleepy|exhausted|drowsy)\b",
                    r"\b(?:safe|fine|harmless).{0,45}\b(?:gas leak|exposed wire|wild mushroom|unknown substance)\b",
                    r"\b(?:charge|charging).{0,50}\b(?:phone|laptop|battery).{0,40}\bunder (?:a )?pillow\b",
                    r"\b(?:light|use) (?:a )?(?:match|lighter|candle).{0,45}\b(?:gas leak|smell gas)\b",
                    r"\b(?:eat|taste).{0,40}\bunknown (?:mushroom|berry|plant|chemical)\b",
                ),
            ),
            Rule(
                "MISINFORMATION",
                0.91,
                "States an unreliable factual claim as certain or uses fake authority language.",
                (
                    r"\bscientists? (?:have )?(?:confirmed|proved).{0,110}\b(?:always|never|completely|100 percent)\b",
                    r"\b(?:cold water|chewing gum).{0,80}\b(?:stops digestion|stays in your stomach for seven years)\b",
                    r"\b(?:vaccines?|antibiotics?).{0,70}\b(?:cause every|cure all|work against viruses|contain tracking chips)\b",
                    r"\bcorrelation.{0,50}\b(?:proves?|means).{0,45}\bcaus",
                    r"\bno evidence of harm.{0,65}\b(?:completely )?safe\b",
                    r"\b(?:earth is flat|moon landing was fake)\b",
                    r"\b(?:doctors?|experts?) do not want you to know\b",
                    r"\b(?:one weird trick|secret cure).{0,60}\b(?:cures?|eliminates?|prevents?)\b",
                ),
            ),
            Rule(
                "SPAM",
                0.86,
                "Looks like repeated, forwarded, or mass-distributed content.",
                (
                    r"\b(hey|hello|buy now|click here)(?:[!,. ]+\1){3,}",
                    r"\b(?:sent to|forward to|share with) \d+ (?:people|friends|contacts)\b",
                    r"\b(?:limited offer|exclusive deal).{0,65}\b(?:unsubscribe|promo code|text stop)\b",
                    r"\b([a-z]{2,12})(?:\s+\1){4,}\b",
                    r"(?:https?://\S+\s*){3,}",
                ),
            ),
            Rule(
                "PROMOTION",
                0.78,
                "Contains advertising, subscription, discount, or sales language.",
                (
                    r"(?:\bsave \d+%|\bsale\b|\bdiscount\b|\bshop now\b|\bbuy now\b|\bnew product\b|\bsubscribe\b|\bfree trial\b)",
                    r"\b(?:join our mailing list|limited-time offer|now available in stores|use promo code)\b",
                ),
            ),
            Rule(
                "TOXIC",
                0.90,
                "Contains a direct insult, degrading statement, or hostile command.",
                (
                    r"\b(?:you are|you're) (?:a |an )?(?:stupid|useless|worthless|pathetic|terrible|idiot|loser|failure)(?:\s+(?:idiot|loser|failure))?\b",
                    r"\b(?:nobody wants you|you ruin everything|everyone hates you)\b",
                    r"\b(?:shut up|go away),? (?:idiot|loser|moron)\b",
                ),
            ),
            Rule(
                "PROFANITY",
                0.84,
                "Contains strong profanity.",
                (r"\b(?:fuck|shit|asshole|bitch|bastard)(?:ing|ed|s|es)?\b",),
            ),
        )

    @staticmethod
    def _clean(text: str) -> str:
        return " ".join(text.replace("\u200b", "").split()).strip()

    @staticmethod
    def _is_warning_context(lowered: str) -> bool:
        warning_markers = (
            "do not ",
            "don't ",
            "never ",
            "is dangerous",
            "is not safe",
            "isn't safe",
            "can cause a fire",
            "can hurt",
            "avoid ",
            "warning:",
            "false:",
            "misinformation:",
            "scam example:",
        )
        return any(marker in lowered for marker in warning_markers)

    def classify(
        self,
        text: str,
        *,
        enabled_categories: list[str] | tuple[str, ...] = CATEGORIES,
        source: str = "manual",
    ) -> Detection:
        cleaned = self._clean(text)
        if not cleaned:
            return Detection("SAFE", 0.0, "", "Empty line.", source=source)

        enabled = set(enabled_categories)
        lowered = cleaned.lower()

        if self.external_judge is not None:
            try:
                result = self.external_judge(cleaned, "balanced")
                category = str(result.get("label", "SAFE"))
                risk = float(result.get("risk_score", 0.0))
                if category in enabled or category == "SAFE":
                    return Detection(category, risk, cleaned, "Local ML model result.", source=source)
            except Exception:
                pass

        matches: list[tuple[str, float, str]] = []
        warning_context = self._is_warning_context(lowered)
        for rule in self.rules:
            if rule.category not in enabled:
                continue
            if any(re.search(pattern, lowered, re.IGNORECASE) for pattern in rule.patterns):
                adjusted_risk = rule.risk
                # A sentence that clearly warns against a hazard should not be
                # labeled as dangerous advice. Other categories may still apply.
                if rule.category == "DANGEROUS_ADVICE" and warning_context:
                    continue
                matches.append((rule.category, adjusted_risk, rule.reason))

        urgency = bool(re.search(r"\b(?:urgent|immediately|act now|within \d+|today only|final warning)\b", lowered))
        sensitive = bool(
            re.search(
                r"\b(?:password|pin|verification code|credit card|bank account|social security|gift card|crypto)\b",
                lowered,
            )
        )
        requested_action = bool(re.search(r"\b(?:click|reply|send|pay|enter|verify|confirm|purchase)\b", lowered))
        if "SCAM" in enabled and urgency and sensitive and requested_action:
            matches.append(("SCAM", 0.99, "Combines urgency with a request for sensitive information or payment."))

        if not matches:
            return Detection("SAFE", 0.08, cleaned, "No enabled warning pattern matched.", source=source)

        category, risk, reason = max(matches, key=lambda item: item[1])
        return Detection(category, risk, cleaned, reason, source=source)

    def classify_lines(
        self,
        text: str,
        *,
        enabled_categories: list[str] | tuple[str, ...] = CATEGORIES,
        source: str = "manual",
    ) -> list[Detection]:
        results: list[Detection] = []
        for raw_line in text.splitlines():
            line = raw_line.strip()
            if not line:
                continue
            results.append(
                self.classify(
                    line,
                    enabled_categories=enabled_categories,
                    source=source,
                )
            )
        return results
