"""Post persistence and queue maintenance operations."""

from __future__ import annotations

import random
from datetime import UTC
from datetime import datetime
from datetime import timedelta
from math import ceil
from typing import Any

from sqlalchemy import delete
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session

from fenliu.models import Post
from fenliu.models import QueueStats
from fenliu.models import QueueStatus
from fenliu.services.spam_scoring import default_scoring_service


def _instance_from_acct(acct: str, fallback: str) -> str:
    """Derive instance hostname from an ActivityPub acct string.

    For remote accounts (acct = 'joe@instance.blog') returns 'instance.blog'.
    For local accounts (acct = 'joe') falls back to the stream instance.
    """
    if "@" in acct:
        return acct.rsplit("@", maxsplit=1)[-1]
    return fallback


def save_fetched_posts(
    db: Session,
    processed_posts: list[dict],
    stream_id: int,
    instance: str,
) -> int:
    """Persist new posts from a fetch operation, scoring each one.

    Args:
        db: Open database session.
        processed_posts: Post dicts returned by :class:`FediverseClient`.
        stream_id: ID of the parent :class:`HashtagStream`.
        instance: Fediverse instance hostname used for the fetch.

    Returns:
        Number of posts that were newly saved (duplicates skipped).

    """
    posts_saved = 0
    for post_data in processed_posts:
        existing = db.execute(select(Post).where(Post.post_id == post_data["post_id"])).scalar_one_or_none()
        if existing:
            continue
        db_post = Post(
            post_id=post_data.get("post_id", ""),
            content=post_data.get("content", ""),
            author_username=post_data.get("author_username", ""),
            author_display_name=post_data.get("author_display_name", ""),
            author_url=post_data.get("author_url", ""),
            author_is_bot=post_data.get("author_is_bot", False),
            instance=_instance_from_acct(post_data.get("author_acct", ""), instance),
            hashtags=post_data.get("hashtags", []),
            media_attachments=post_data.get("media_attachments", []),
            boosts=post_data.get("boosts", 0),
            likes=post_data.get("likes", 0),
            replies=post_data.get("replies", 0),
            url=post_data.get("url", ""),
            created_at=post_data.get("created_at"),
            stream_id=stream_id,
        )
        score_result = default_scoring_service.calculate_spam_score(post_data)
        db_post.spam_score = score_result["final_score"]
        db.add(db_post)
        posts_saved += 1
    if posts_saved > 0:
        db.flush()
    return posts_saved


def get_or_create_queue_stats(db: Session) -> QueueStats:
    """Get or create the singleton QueueStats row."""
    stats = db.get(QueueStats, 1)
    if stats is None:
        stats = QueueStats(id=1)
        db.add(stats)
        db.flush()
    return stats


def cleanup_delivered_posts(db: Session, retention_days: int = 7) -> int:
    """Delete delivered posts older than retention_days.

    Records the count in QueueStats before deletion to preserve historical stats.
    Returns the number of posts deleted.
    """
    cutoff = datetime.now(UTC) - timedelta(days=retention_days)

    # Count posts to delete
    count_stmt = select(func.count(Post.id)).where(
        Post.queue_status == QueueStatus.DELIVERED, Post.delivered_at < cutoff
    )
    count = db.execute(count_stmt).scalar() or 0

    if count > 0:
        # Update stats before deleting
        stats = get_or_create_queue_stats(db)
        stats.total_deleted_delivered += count

        # Delete posts
        delete_stmt = delete(Post).where(Post.queue_status == QueueStatus.DELIVERED, Post.delivered_at < cutoff)
        db.execute(delete_stmt)
        db.commit()

    return count


def _compute_post_weights(
    pending_posts: list[Any],
) -> tuple[list[int], list[float]]:
    """Compute combined deletion weights for pending posts.

    Weight dimensions:
    - Age: older posts → higher weight
    - Engagement: fewer likes → higher weight
    - Author frequency: more posts from author → higher weight

    Returns (ids, combined_weights).
    """
    now = datetime.now(UTC).replace(tzinfo=UTC)

    # Extract raw data with timezone normalization
    ids: list[int] = []
    fetched_times: list[datetime | None] = []
    like_counts: list[int] = []
    authors: list[str | None] = []

    for post_id, fetched_at, likes, author in pending_posts:
        ids.append(post_id)
        normalized_ft = fetched_at
        if normalized_ft and normalized_ft.tzinfo is None:
            normalized_ft = normalized_ft.replace(tzinfo=UTC)
        fetched_times.append(normalized_ft)
        like_counts.append(likes or 0)
        authors.append(author)

    # Compute age weights (normalized 0-1, older → higher)
    age_seconds = [(now - ft).total_seconds() if ft else 0 for ft in fetched_times]
    max_age = max(age_seconds) if age_seconds else 1
    age_weights = [s / max_age if max_age > 0 else 0.5 for s in age_seconds]

    # Compute engagement weights (1.0 - normalized likes, no likes → higher)
    max_likes = max(like_counts) if like_counts else 1
    engagement_weights = (
        [0.5 for _ in like_counts] if max_likes == 0 else [1.0 - (lc / max_likes) for lc in like_counts]
    )

    # Compute author frequency weights (prolific authors → higher)
    author_freq: dict[str | None, int] = {}
    for author in authors:
        author_freq[author] = author_freq.get(author, 0) + 1

    max_author_freq = max(author_freq.values()) if author_freq else 1
    author_freq_weights = [
        author_freq.get(author, 0) / max_author_freq if max_author_freq > 0 else 0.5 for author in authors
    ]

    # Combined weight (sum of three dimensions)
    combined_weights = [age_weights[i] + engagement_weights[i] + author_freq_weights[i] for i in range(len(ids))]

    return ids, combined_weights


def trim_pending_posts(db: Session, lookback_days: int = 3) -> int:
    """Delete excess pending posts using weighted random selection.

    Maintains invariant: pending_count >= 2 * daily_consumption_rate.

    Deletion probability is higher for:
    - Older posts (by fetched_at)
    - Posts with fewer likes (lower engagement)
    - Posts from prolific authors (many pending posts from same author)

    Returns the number of posts deleted.
    """
    # 1. Compute daily consumption rate from delivered posts
    cutoff = datetime.now(UTC) - timedelta(days=lookback_days)
    delivered_count = (
        db.execute(
            select(func.count(Post.id)).where(Post.queue_status == QueueStatus.DELIVERED, Post.delivered_at >= cutoff)
        ).scalar()
        or 0
    )

    if delivered_count == 0:
        # No consumption history yet
        return 0

    daily_rate = delivered_count / max(lookback_days, 1)
    target_buffer = ceil(2 * daily_rate)

    # 2. Count current pending posts
    pending_count = (
        db.execute(select(func.count(Post.id)).where(Post.queue_status == QueueStatus.PENDING)).scalar() or 0
    )

    if pending_count <= target_buffer:
        # Pending count is within acceptable range
        return 0

    excess = pending_count - target_buffer

    # 3. Load all pending posts with necessary fields
    stmt = select(Post.id, Post.fetched_at, Post.likes, Post.author_username).where(
        Post.queue_status == QueueStatus.PENDING
    )
    pending_posts = list(db.execute(stmt).all())

    if not pending_posts:
        return 0

    # 4. Compute deletion weights for each post
    ids, combined_weights = _compute_post_weights(pending_posts)

    # 5. Select posts for deletion using weighted random sampling (with replacement)
    # Note: Using random for non-cryptographic sampling (no sec requirement here)
    selected_indices = random.choices(range(len(ids)), weights=combined_weights, k=excess)  # noqa: S311
    selected_ids = list({ids[i] for i in selected_indices})  # Deduplicate

    # 6. Delete selected posts and update stats
    if selected_ids:
        delete_stmt = delete(Post).where(Post.id.in_(selected_ids))
        db.execute(delete_stmt)
        stats = get_or_create_queue_stats(db)
        stats.total_deleted_pending += len(selected_ids)
        db.commit()
        return len(selected_ids)

    return 0
