"""In-memory cache for blocklists to avoid repeated database queries.

This module provides a thread-safe cache for blocked users and hashtags,
with functions to load, query, and invalidate the cache.
"""

from __future__ import annotations

import threading
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from sqlalchemy.orm import Session

from sqlalchemy import select

from fenliu.models import AppSetting
from fenliu.models import BlockedHashtag
from fenliu.models import BlockedUser

# Global cache state
_cache_lock = threading.Lock()
_blocked_users_cache: set[str] | None = None
_blocked_hashtags_cache: set[str] | None = None
_app_settings_cache: dict[str, str] | None = None
# List of (account_identifier, pattern_type) tuples for pattern-aware matching
_blocked_user_patterns_cache: list[tuple[str, str]] | None = None


def _load_cache(db: Session) -> tuple[set[str], set[str]]:
    """Load blocklists from database.

    Args:
        db: Open SQLAlchemy session.

    Returns:
        Tuple of (blocked_users_set, blocked_hashtags_set), both normalized to lowercase.

    """
    users = db.execute(select(BlockedUser)).scalars().all()
    blocked_users = {u.account_identifier.lower() for u in users}

    tags = db.execute(select(BlockedHashtag)).scalars().all()
    blocked_hashtags = {t.hashtag.lower() for t in tags}

    return blocked_users, blocked_hashtags


def initialize_cache(db: Session) -> None:
    """Eagerly initialize the blocklist cache from database.

    Call this during application startup to pre-load the cache.

    Args:
        db: Open SQLAlchemy session.

    """
    global _blocked_users_cache  # noqa: PLW0603
    global _blocked_hashtags_cache  # noqa: PLW0603

    with _cache_lock:
        users, tags = _load_cache(db)
        _blocked_users_cache = users
        _blocked_hashtags_cache = tags


def get_blocked_users(db: Session) -> set[str]:
    """Get the set of blocked user identifiers (all lowercase).

    Lazily loads from cache; loads from DB if cache is empty.

    Args:
        db: Open SQLAlchemy session.

    Returns:
        Set of blocked user account identifiers (normalized to lowercase).

    """
    global _blocked_users_cache  # noqa: PLW0603

    with _cache_lock:
        if _blocked_users_cache is None:
            users, _ = _load_cache(db)
            _blocked_users_cache = users
        return _blocked_users_cache.copy()


def get_blocked_hashtags(db: Session) -> set[str]:
    """Get the set of blocked hashtags (all lowercase).

    Lazily loads from cache; loads from DB if cache is empty.

    Args:
        db: Open SQLAlchemy session.

    Returns:
        Set of blocked hashtags (normalized to lowercase, no leading #).

    """
    global _blocked_hashtags_cache  # noqa: PLW0603

    with _cache_lock:
        if _blocked_hashtags_cache is None:
            _, tags = _load_cache(db)
            _blocked_hashtags_cache = tags
        return _blocked_hashtags_cache.copy()


def add_blocked_user(identifier: str) -> None:
    """Add a user to the blocklist and update cache.

    Args:
        identifier: User account identifier to block (will be normalized to lowercase).

    """
    global _blocked_users_cache  # noqa: PLW0602
    global _blocked_user_patterns_cache  # noqa: PLW0603

    normalized = identifier.lower()

    with _cache_lock:
        if _blocked_users_cache is not None:
            _blocked_users_cache.add(normalized)
        # Patterns cache includes pattern_type which we don't have here; invalidate
        # so it reloads from DB on next access.
        _blocked_user_patterns_cache = None


def add_blocked_hashtag(hashtag: str) -> None:
    """Add a hashtag to the blocklist and update cache.

    Args:
        hashtag: Hashtag to block (will be normalized to lowercase, # stripped).

    """
    global _blocked_hashtags_cache  # noqa: PLW0602

    normalized = hashtag.lower().lstrip("#")

    with _cache_lock:
        if _blocked_hashtags_cache is not None:
            _blocked_hashtags_cache.add(normalized)


def remove_blocked_user(identifier: str) -> None:
    """Remove a user from the blocklist cache.

    Args:
        identifier: User account identifier to unblock.

    """
    global _blocked_users_cache  # noqa: PLW0602
    global _blocked_user_patterns_cache  # noqa: PLW0603

    normalized = identifier.lower()

    with _cache_lock:
        if _blocked_users_cache is not None:
            _blocked_users_cache.discard(normalized)
        _blocked_user_patterns_cache = None


def remove_blocked_hashtag(hashtag: str) -> None:
    """Remove a hashtag from the blocklist cache.

    Args:
        hashtag: Hashtag to unblock (will be normalized to lowercase, # stripped).

    """
    global _blocked_hashtags_cache  # noqa: PLW0602

    normalized = hashtag.lower().lstrip("#")

    with _cache_lock:
        if _blocked_hashtags_cache is not None:
            _blocked_hashtags_cache.discard(normalized)


def get_bool_setting(db: Session, key: str) -> bool:
    """Return the boolean value of an AppSetting key (default False).

    Loads all settings from the database on first call and caches them in memory.
    Call ``invalidate_cache()`` after writing settings to pick up the new values.

    Args:
        db: Open SQLAlchemy session (used only on cache miss).
        key: AppSetting key to look up.

    Returns:
        Parsed boolean value, defaulting to False if the key is absent.

    """
    global _app_settings_cache  # noqa: PLW0603

    with _cache_lock:
        if _app_settings_cache is None:
            rows = db.execute(select(AppSetting)).scalars().all()
            _app_settings_cache = {row.key: row.value for row in rows}
        value = _app_settings_cache.get(key)
    return value is not None and value.lower() == "true"


def get_blocked_user_patterns(db: Session) -> list[tuple[str, str]]:
    """Return all blocked-user patterns as (account_identifier, pattern_type) tuples.

    Loads from the database on first call and caches the result in memory.
    Call ``invalidate_cache()`` after writing blocked users to pick up changes.

    Args:
        db: Open SQLAlchemy session (used only on cache miss).

    Returns:
        List of (account_identifier, pattern_type) pairs (identifiers are lowercase).

    """
    global _blocked_user_patterns_cache  # noqa: PLW0603

    with _cache_lock:
        if _blocked_user_patterns_cache is None:
            users = db.execute(select(BlockedUser)).scalars().all()
            _blocked_user_patterns_cache = [(u.account_identifier.lower(), u.pattern_type) for u in users]
        return list(_blocked_user_patterns_cache)


def invalidate_cache() -> None:
    """Invalidate the entire blocklist and settings cache.

    Call this when blocklists or app settings are modified externally
    (e.g., via API endpoints or the settings page).
    Each cache will be reloaded from the database on next access.

    """
    global _blocked_users_cache  # noqa: PLW0603
    global _blocked_hashtags_cache  # noqa: PLW0603
    global _app_settings_cache  # noqa: PLW0603
    global _blocked_user_patterns_cache  # noqa: PLW0603

    with _cache_lock:
        _blocked_users_cache = None
        _blocked_hashtags_cache = None
        _app_settings_cache = None
        _blocked_user_patterns_cache = None


def is_user_blocked(identifier: str, db: Session) -> bool:
    """Check if a user identifier is in the blocklist.

    Args:
        identifier: User account identifier to check.
        db: Open SQLAlchemy session.

    Returns:
        True if the identifier is blocked, False otherwise.

    """
    blocked = get_blocked_users(db)
    return identifier.lower() in blocked


def is_hashtag_blocked(hashtag: str, db: Session) -> bool:
    """Check if a hashtag is in the blocklist.

    Args:
        hashtag: Hashtag to check (leading # is optional).
        db: Open SQLAlchemy session.

    Returns:
        True if the hashtag is blocked, False otherwise.

    """
    blocked = get_blocked_hashtags(db)
    normalized = hashtag.lower().lstrip("#")
    return normalized in blocked
