"""Service for determining if a post is eligible for Curated export.

Also handles bulk-rejecting posts that are blocked by reblog controls.

Applies all active reblog-control filters in order:
1. Author must not match any blocked-users pattern.
2. Post must not contain any blocked hashtag.
3. If the attachments-only setting is enabled, the post must have at least
   one media attachment.

Note: the eligibility check does NOT require ``post.approved``; it is used
both to gate export (where approval is a separate prerequisite) and to
identify posts that should be auto-rejected before review.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import UTC
from datetime import datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from fenliu.models import AppSetting
from fenliu.models import BlockedUser
from fenliu.models import Post
from fenliu.services.blocklist_cache import get_blocked_hashtags

SETTING_ATTACHMENTS_ONLY = "attachments_only"
SETTING_AUTO_REJECT_BLOCKED = "auto_reject_blocked"
SETTING_AUTO_REJECT_BOTS = "auto_reject_bots"


@dataclass(frozen=True)
class EligibilityResult:
    """Result of an export-eligibility check."""

    eligible: bool
    reason: str | None = None  # human-readable explanation when not eligible


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _get_bool_setting(db: Session, key: str) -> bool:
    """Return the boolean value of an AppSetting key (default False)."""
    row = db.execute(select(AppSetting).where(AppSetting.key == key)).scalar_one_or_none()
    if row is None:
        return False
    return row.value.lower() == "true"


def _has_media_attachments(post: Post) -> bool:
    """Return True if the post has at least one media attachment."""
    attachments: Any = post.media_attachments
    if attachments is None:
        return False
    if isinstance(attachments, list):
        return len(attachments) > 0
    return bool(attachments)


def _post_hashtags_lower(post: Post) -> set[str]:
    """Return the set of hashtags on a post, all lower-cased."""
    tags: Any = post.hashtags
    if not tags:
        return set()
    if isinstance(tags, list):
        return {str(t).lower().lstrip("#") for t in tags}
    return set()


def _build_candidate_identifiers(post: Post) -> set[str]:
    """Build the set of normalised account identifiers to match against the blocklist."""
    if not post.author_username:
        return set()
    raw = post.author_username.strip().lower()
    bare = raw.lstrip("@")
    candidates: set[str] = {raw, f"@{raw}", bare, f"@{bare}"}
    if post.instance:
        instance = post.instance.strip().lower()
        candidates.add(f"@{bare}@{instance}")
        candidates.add(f"{bare}@{instance}")
    return candidates


def _matches_pattern(value: str, pattern: str, pattern_type: str) -> bool:
    """Check if a value matches a blocked user pattern.

    Args:
        value: The account identifier to check (normalized to lowercase).
        pattern: The pattern from the blocklist (normalized to lowercase).
        pattern_type: The type of matching: "exact", "suffix", "prefix", "contains".

    Returns:
        True if the value matches the pattern.

    """
    value_lower = value.lower()
    pattern_lower = pattern.lower()

    if pattern_type == "exact":
        return value_lower == pattern_lower
    elif pattern_type == "suffix":
        return value_lower.endswith(pattern_lower)
    elif pattern_type == "prefix":
        return value_lower.startswith(pattern_lower)
    elif pattern_type == "contains":
        return pattern_lower in value_lower
    return False


def _check_author_blocklist(post: Post, db: Session) -> EligibilityResult | None:
    """Check if post author matches any blocked user pattern.

    Args:
        post: The post to check.
        db: Database session.

    Returns:
        EligibilityResult if author is blocked, None if author passes.

    """
    candidates = _build_candidate_identifiers(post)
    if not candidates:
        return None

    stmt = select(BlockedUser)
    blocked_users = list(db.execute(stmt).scalars().all())

    for blocked in blocked_users:
        for candidate in candidates:
            if _matches_pattern(candidate, blocked.account_identifier, blocked.pattern_type):
                return EligibilityResult(
                    eligible=False,
                    reason=f"author blocked: {candidate}",
                )

    return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def check_reblog_filters(post: Post, db: Session) -> EligibilityResult:
    """Check whether *post* passes all active reblog-control filters.

    This check is independent of the post's approval status and can be used
    both pre-review (auto-reject) and post-approval (export gating).

    Args:
        post: The :class:`~fenliu.models.Post` instance to evaluate.
        db:   An open SQLAlchemy :class:`~sqlalchemy.orm.Session`.

    Returns:
        An :class:`EligibilityResult` whose ``eligible`` flag is ``True``
        when the post passes all active filters, or ``False`` with a
        human-readable ``reason`` identifying the blocking filter.

    """
    # 1. Author blocklist (supports exact and pattern matching)
    author_result = _check_author_blocklist(post, db)
    if author_result is not None:
        return author_result

    # 2. Hashtag blocklist
    post_tags = _post_hashtags_lower(post)
    if post_tags:
        blocked_hashtags_set = get_blocked_hashtags(db)
        blocked_tag = next((t for t in post_tags if t in blocked_hashtags_set), None)
        if blocked_tag:
            return EligibilityResult(
                eligible=False,
                reason=f"blocked hashtag: #{blocked_tag}",
            )

    # 3. Bot account check
    if _get_bool_setting(db, SETTING_AUTO_REJECT_BOTS) and post.author_is_bot:
        return EligibilityResult(eligible=False, reason="author is a bot account")

    # 4. Attachments-only setting
    if _get_bool_setting(db, SETTING_ATTACHMENTS_ONLY) and not _has_media_attachments(post):
        return EligibilityResult(eligible=False, reason="attachments-only mode: no media attachment")

    return EligibilityResult(eligible=True)


def check_export_eligibility(post: Post, db: Session) -> EligibilityResult:
    """Check whether an *approved* post is eligible for Curated export.

    Combines the approval requirement with the reblog-control filters.

    Args:
        post: The :class:`~fenliu.models.Post` instance to evaluate.
        db:   An open SQLAlchemy :class:`~sqlalchemy.orm.Session`.

    Returns:
        An :class:`EligibilityResult`.

    """
    if not post.approved:
        return EligibilityResult(eligible=False, reason="post not approved")
    return check_reblog_filters(post, db)


def get_eligible_posts(db: Session, limit: int | None = None) -> list[tuple[Post, EligibilityResult]]:
    """Return all approved posts paired with their eligibility result.

    Useful for the Queue Preview UI where we want to surface *why* an
    approved post is excluded from the export queue.

    Args:
        db:    An open SQLAlchemy session.
        limit: Optional cap on the number of posts evaluated.

    Returns:
        A list of ``(post, result)`` tuples ordered by approval time
        (oldest first).

    """
    stmt = select(Post).where(Post.approved.is_(True)).order_by(Post.reviewed_at)
    if limit is not None:
        stmt = stmt.limit(limit)
    posts = list(db.execute(stmt).scalars().all())
    return [(post, check_export_eligibility(post, db)) for post in posts]


def reject_blocked_posts(db: Session) -> int:
    """Reject all unreviewed posts that are blocked by active reblog-control filters.

    Marks matching posts as reviewed=True, approved=False with a system note
    explaining which filter triggered the rejection.

    Args:
        db: An open SQLAlchemy session.  The caller is responsible for
            committing the transaction.

    Returns:
        The number of posts that were rejected.

    """
    stmt = select(Post).where(Post.reviewed.is_(False))
    unreviewed = list(db.execute(stmt).scalars().all())

    rejected = 0
    now = datetime.now(UTC)
    for post in unreviewed:
        result = check_reblog_filters(post, db)
        if not result.eligible:
            post.reviewed = True
            post.approved = False
            post.reviewed_at = now
            post.reviewer_notes = f"[auto-rejected] {result.reason}"
            rejected += 1

    return rejected


def get_auto_reject_setting(db: Session) -> bool:
    """Return whether auto-reject-on-fetch is currently enabled."""
    return _get_bool_setting(db, SETTING_AUTO_REJECT_BLOCKED)


def get_auto_reject_bots_setting(db: Session) -> bool:
    """Return whether auto-reject-bots is currently enabled."""
    return _get_bool_setting(db, SETTING_AUTO_REJECT_BOTS)
