"""Spam scoring service for FenLiu.

This module provides rule-based spam scoring for Fediverse posts.
Posts are scored on a 0-100 scale, where higher scores indicate higher spam likelihood.

Scoring Rules:
1. Content-based rules (length, keywords, hashtag count, link count)
2. Engagement-based rules (boosts, likes, replies)
3. Author-based rules (username patterns, display name patterns)
4. Media-based rules (media attachment count, types)
"""

import json
import logging
import re
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any
from typing import Protocol

from fenliu.custom_types import PostDataDict
from fenliu.custom_types import RuleDescriptionDict
from fenliu.custom_types import RuleResult
from fenliu.custom_types import ScoringResult
from fenliu.models import Post

logger = logging.getLogger(__name__)


class SpamRule(Protocol):
    """Protocol for spam detection rules."""

    @property
    def name(self) -> str:
        """Name of the rule."""
        ...

    @property
    def weight(self) -> float:
        """Weight of this rule in overall scoring (0.0-1.0)."""
        ...

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply rule to post data and return scoring result."""
        ...


@dataclass
class BaseRule:
    """Base class for spam detection rules."""

    name: str
    weight: float = 1.0

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply rule to post data and return scoring result."""
        raise NotImplementedError("Subclasses must implement apply method")


class ContentLengthRule(BaseRule):
    """Rule for content length analysis.

    Very short or very long posts are more likely to be spam.
    """

    def __init__(self, name: str = "content_length", weight: float = 1.0) -> None:
        """Initialize content length rule.

        Args:
            name: Rule name (default: "content_length")
            weight: Rule weight (default: 1.0)

        """
        super().__init__(name, weight)
        self.short_threshold = 10  # characters
        self.long_threshold = 1000  # characters
        self.short_penalty = 40  # score for very short posts
        self.long_penalty = 20  # score for very long posts

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply content length rule."""
        content = post_data.get("content", "")
        # Strip HTML tags if present
        if isinstance(content, str):
            content_text = re.sub(r"<[^>]+>", "", content)
        else:
            content_text = str(content)

        length = len(content_text)
        score = 0
        details: dict[str, Any] = {"content_length": length}

        if length < self.short_threshold:
            score = self.short_penalty
            details["reason"] = f"Content too short ({length} chars < {self.short_threshold})"
        elif length > self.long_threshold:
            score = self.long_penalty
            details["reason"] = f"Content too long ({length} chars > {self.long_threshold})"

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": details,
            "triggered": score > 0,
        }


class HashtagCountRule(BaseRule):
    """Rule for hashtag count analysis.

    Excessive hashtags are often used by spammers.
    """

    def __init__(self, name: str = "hashtag_count", weight: float = 1.0) -> None:
        """Initialize hashtag count rule.

        Args:
            name: Rule name (default: "hashtag_count")
            weight: Rule weight (default: 1.0)

        """
        super().__init__(name, weight)
        self.warning_threshold = 5
        self.critical_threshold = 10
        self.warning_penalty = 15
        self.critical_penalty = 40

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply hashtag count rule."""
        hashtags = post_data.get("hashtags", [])
        if isinstance(hashtags, str):
            # Try to parse JSON string
            try:
                hashtags = json.loads(hashtags)
            except (json.JSONDecodeError, TypeError):
                hashtags = []

        count = len(hashtags) if isinstance(hashtags, (list, tuple)) else 0
        score = 0
        details: dict[str, Any] = {"hashtag_count": count}

        if count >= self.critical_threshold:
            score = self.critical_penalty
            details["reason"] = f"Excessive hashtags ({count} >= {self.critical_threshold})"
        elif count >= self.warning_threshold:
            score = self.warning_penalty
            details["reason"] = f"High hashtag count ({count} >= {self.warning_threshold})"

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": details,
            "triggered": score > 0,
        }


class LinkCountRule(BaseRule):
    """Rule for link count analysis.

    Multiple links in a post often indicate spam.
    """

    def __init__(self, name: str = "link_count", weight: float = 1.0) -> None:
        """Initialize link count rule.

        Args:
            name: Rule name (default: "link_count")
            weight: Rule weight (default: 1.0)

        """
        super().__init__(name, weight)
        self.link_pattern = re.compile(r"https?://[^\s]+")
        self.warning_threshold = 2
        self.critical_threshold = 4
        self.warning_penalty = 20
        self.critical_penalty = 50

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply link count rule."""
        content = post_data.get("content", "")
        if not isinstance(content, str):
            content = str(content)

        links = self.link_pattern.findall(content)
        count = len(links)
        score = 0
        details: dict[str, Any] = {"link_count": count, "links": links[:5]}  # Limit for details

        if count >= self.critical_threshold:
            score = self.critical_penalty
            details["reason"] = f"Excessive links ({count} >= {self.critical_threshold})"
        elif count >= self.warning_threshold:
            score = self.warning_penalty
            details["reason"] = f"Multiple links ({count} >= {self.warning_threshold})"

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": details,
            "triggered": score > 0,
        }


class EngagementRule(BaseRule):
    """Rule for engagement analysis.

    Low engagement (boosts, likes, replies) can indicate spam.
    """

    def __init__(self, name: str = "engagement", weight: float = 0.8) -> None:
        """Initialize engagement rule.

        Args:
            name: Rule name (default: "engagement")
            weight: Rule weight (default: 0.8)

        """
        super().__init__(name, weight)
        self.boosts_threshold = 1
        self.likes_threshold = 2
        self.replies_threshold = 0
        self.penalty = 30

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply engagement rule."""
        boosts = post_data.get("boosts", 0) or 0
        likes = post_data.get("likes", 0) or 0
        replies = post_data.get("replies", 0) or 0

        score = 0
        details = {
            "boosts": boosts,
            "likes": likes,
            "replies": replies,
        }

        # Check if engagement is below thresholds
        if boosts < self.boosts_threshold and likes < self.likes_threshold and replies <= self.replies_threshold:
            score = self.penalty
            details["reason"] = (
                f"Low engagement (boosts<{self.boosts_threshold}, "
                f"likes<{self.likes_threshold}, replies<={self.replies_threshold})"
            )

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": details,
            "triggered": score > 0,
        }


class KeywordRule(BaseRule):
    """Rule for spam keyword detection.

    Certain keywords and phrases are commonly used by spammers.
    """

    def __init__(self, name: str = "keywords", weight: float = 1.2):
        """Initialize keyword rule.

        Args:
            name: Rule name (default: "keywords")
            weight: Rule weight (default: 1.2)

        """
        super().__init__(name, weight)
        # Common spam keywords (case-insensitive)
        self.spam_keywords = [
            r"buy\s+followers",
            r"get\s+rich",
            r"make\s+money",
            r"click\s+here",
            r"limited\s+time",
            r"act\s+now",
            r"free\s+gift",
            r"100%\s+free",
            r"risk-free",
            r"special\s+promotion",
            r"earn\s+cash",
            r"work\s+from\s+home",
            r"double\s+your",
            r"guaranteed",
            r"no\s+cost",
            r"diet\s+pills",
            r"weight\s+loss",
            r"miracle",
            r"cure",
            r"increase\s+size",
        ]
        self.keyword_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.spam_keywords]
        self.penalty_per_keyword = 15
        self.max_penalty = 60

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply keyword rule."""
        content = post_data.get("content", "")
        if not isinstance(content, str):
            content = str(content)

        # Strip HTML tags
        content_text = re.sub(r"<[^>]+>", "", content)

        matches = []
        for pattern in self.keyword_patterns:
            if pattern.search(content_text):
                matches.append(pattern.pattern)

        score = min(len(matches) * self.penalty_per_keyword, self.max_penalty)

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": {
                "keyword_matches": matches,
                "total_matches": len(matches),
            },
            "triggered": score > 0,
        }


class UsernamePatternRule(BaseRule):
    """Rule for suspicious username patterns.

    Spammers often use certain patterns in usernames.
    """

    def __init__(self, name: str = "username_pattern", weight: float = 0.7):
        """Initialize username pattern rule.

        Args:
            name: Rule name (default: "username_pattern")
            weight: Rule weight (default: 0.7)

        """
        super().__init__(name, weight)
        # Patterns for suspicious usernames
        self.suspicious_patterns = [
            r"\d{4,}",  # 4 or more consecutive digits
            r".*_.*_.*",  # Multiple underscores
            r".*-.*-.*",  # Multiple hyphens
            r"^[a-z]+\d+$",  # Letters followed by digits
            r"^[0-9]+$",  # All digits
        ]
        self.pattern_regexes = [re.compile(pattern) for pattern in self.suspicious_patterns]
        self.penalty = 25

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply username pattern rule."""
        username = post_data.get("author_username", "")
        if not isinstance(username, str):
            username = str(username)

        score = 0
        matched_patterns = []

        for pattern_regex in self.pattern_regexes:
            if pattern_regex.search(username):
                matched_patterns.append(pattern_regex.pattern)

        if matched_patterns:
            score = self.penalty

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": {
                "username": username,
                "matched_patterns": matched_patterns,
            },
            "triggered": score > 0,
        }


class MediaAttachmentRule(BaseRule):
    """Rule for media attachment analysis.

    Spammers often use certain types of media or excessive media.
    """

    def __init__(self, name: str = "media_attachments", weight: float = 0.6) -> None:
        """Initialize media attachment rule.

        Args:
            name: Rule name (default: "media_attachments")
            weight: Rule weight (default: 0.6)

        """
        super().__init__(name, weight)
        self.excessive_threshold = 4
        self.penalty = 20

    def apply(self, post_data: PostDataDict) -> RuleResult:
        """Apply media attachment rule."""
        media_attachments = post_data.get("media_attachments", [])
        if isinstance(media_attachments, str):
            # Try to parse JSON string
            try:
                media_attachments = json.loads(media_attachments)
            except (json.JSONDecodeError, TypeError):
                media_attachments = []

        count = len(media_attachments) if isinstance(media_attachments, (list, tuple)) else 0
        score = 0
        details: dict[str, Any] = {"media_count": count}

        if count >= self.excessive_threshold:
            score = self.penalty
            details["reason"] = f"Excessive media attachments ({count} >= {self.excessive_threshold})"

        return {
            "rule_name": self.name,
            "score": score,
            "weight": self.weight,
            "details": details,
            "triggered": score > 0,
        }


class SpamScoringService:
    """Service for scoring posts for spam likelihood."""

    def __init__(self, rules: Sequence[SpamRule] | None = None) -> None:
        """Initialize spam scoring service with rules.

        Args:
            rules: Optional list of spam rules to use. If None, uses default rules.

        """
        if rules is None:
            self.rules = self.get_default_rules()
        else:
            self.rules = list(rules)

        self.total_weight = sum(rule.weight for rule in self.rules)
        self.logger = logging.getLogger(__name__)

    @staticmethod
    def get_default_rules() -> list[SpamRule]:
        """Get default set of spam detection rules."""
        return [
            ContentLengthRule(),
            HashtagCountRule(),
            LinkCountRule(),
            EngagementRule(),
            KeywordRule(),
            UsernamePatternRule(),
            MediaAttachmentRule(),
        ]

    def calculate_spam_score(self, post_data: PostDataDict) -> ScoringResult:
        """Calculate spam score for a post.

        Args:
            post_data: Dictionary containing post data. Should match fields
                      from Post model or fediverse API response.

        Returns:
            Dictionary with scoring results including:
            - final_score: Overall spam score (0-100)
            - rule_results: List of individual rule results
            - triggered_rules: List of names of rules that were triggered
            - confidence: Confidence level based on triggered rules

        """
        self.logger.debug(f"Calculating spam score for post: {post_data.get('post_id', 'unknown')}")

        # Apply all rules to the post
        rule_results, weighted_scores, total_weight = self._apply_rules(post_data)

        # Calculate final score
        final_score = self._calculate_final_score(weighted_scores, total_weight)

        # Get triggered rule names
        triggered_rules = [r["rule_name"] for r in rule_results if r["triggered"]]

        # Calculate confidence
        confidence = self._calculate_confidence(rule_results)

        return {
            "final_score": final_score,
            "rule_results": rule_results,
            "triggered_rules": triggered_rules,
            "confidence": confidence,
            "post_id": post_data.get("post_id", "unknown"),
        }

    def _apply_rules(self, post_data: PostDataDict) -> tuple[list[RuleResult], float, float]:
        """Apply all spam rules to post data.

        Args:
            post_data: Dictionary containing post data.

        Returns:
            Tuple of (rule_results, weighted_scores, total_weight)

        """
        rule_results = []
        weighted_scores = 0.0
        total_weight = 0.0

        for rule in self.rules:
            try:
                result = rule.apply(post_data)
                rule_results.append(result)

                if result["triggered"]:
                    weighted_scores += result["score"] * result["weight"]
                    total_weight += result["weight"]
            except Exception as e:
                self.logger.error(f"Error applying rule {rule.name}: {e}")
                continue

        return rule_results, weighted_scores, total_weight

    def _calculate_final_score(self, weighted_scores: float, total_weight: float) -> int:
        """Calculate final spam score from weighted scores.

        Args:
            weighted_scores: Sum of weighted rule scores.
            total_weight: Total weight of triggered rules.

        Returns:
            Final spam score (0-100).

        """
        if total_weight == 0:
            return 0

        raw_score = weighted_scores / total_weight
        # Apply sigmoid-like scaling to keep score in 0-100 range
        return min(100, int(raw_score))

    def _calculate_confidence(self, rule_results: list[RuleResult]) -> float:
        """Calculate confidence based on triggered rules.

        Args:
            rule_results: List of rule result dictionaries.

        Returns:
            Confidence score (0.0-1.0).

        """
        triggered_rules = [r for r in rule_results if r["triggered"]]
        if not triggered_rules:
            return 0.0

        triggered_weight = sum(r["weight"] for r in triggered_rules)
        if self.total_weight == 0:
            return 0.0

        return triggered_weight / self.total_weight

    def score_post(self, post: Post) -> ScoringResult:
        """Calculate spam score for a Post model instance.

        Args:
            post: Post model instance.

        Returns:
            Dictionary with scoring results.

        """
        # Convert Post model to dictionary
        post_data = self._post_to_dict(post)
        return self.calculate_spam_score(post_data)

    def _post_to_dict(self, post: Post) -> PostDataDict:
        """Convert Post model to dictionary for scoring.

        Args:
            post: Post model instance.

        Returns:
            Dictionary with post data.

        """
        return {
            "post_id": post.post_id,
            "content": post.content,
            "author_username": post.author_username,
            "author_display_name": post.author_display_name,
            "author_url": post.author_url,
            "instance": post.instance,
            "hashtags": post.hashtags or [],
            "media_attachments": post.media_attachments or [],
            "boosts": post.boosts or 0,
            "likes": post.likes or 0,
            "replies": post.replies or 0,
            "url": post.url,
            "created_at": post.created_at,
            "spam_score": post.spam_score,  # Current score if any
        }

    def get_rule_descriptions(self) -> list[RuleDescriptionDict]:
        """Get descriptions of all configured rules.

        Returns:
            List of dictionaries with rule information.

        """
        descriptions = []
        for rule in self.rules:
            descriptions.append(
                {
                    "name": rule.name,
                    "weight": rule.weight,
                    "type": type(rule).__name__,
                }
            )
        return descriptions


# Default instance for convenience
default_scoring_service = SpamScoringService()


def score_post(post: Post) -> int:
    """Score a post using the default scoring service.

    Args:
        post: Post model instance.

    Returns:
        Spam score (0-100).

    """
    result = default_scoring_service.score_post(post)
    return result["final_score"]


def analyze_post(post: Post) -> ScoringResult:
    """Analyze a post with detailed results using the default scoring service.

    Args:
        post: Post model instance.

    Returns:
        Detailed scoring results.

    """
    return default_scoring_service.score_post(post)
