"""Background scheduler for automated hashtag stream fetches.

Manages scheduling of periodic fetches for active hashtag streams with
support for adaptive scheduling based on fetch results.
"""

from __future__ import annotations

from datetime import UTC
from datetime import datetime
from typing import Any

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from sqlalchemy import select

from fenliu.database import get_db
from fenliu.logging import get_logger
from fenliu.models import HashtagStream
from fenliu.services.export_eligibility import get_auto_reject_setting
from fenliu.services.export_eligibility import reject_blocked_posts
from fenliu.services.fediverse import FediverseClient

logger = get_logger(__name__)

# Constants for adaptive scheduling
MIN_FETCH_INTERVAL = 5
MAX_FETCH_INTERVAL = 1440
BUSY_STREAM_THRESHOLD = 10
QUIET_STREAM_THRESHOLD = 2
MIN_ADAPTIVE_INTERVAL = 15  # No faster than 15 minutes
MAX_ADAPTIVE_INTERVAL = 240  # No slower than 4 hours (240 minutes)
BUSY_INTERVAL_FACTOR = 0.8
QUIET_INTERVAL_FACTOR = 1.2

# Global scheduler instance
_scheduler: StreamScheduler | None = None


class StreamScheduler:
    """Manages background scheduling of hashtag stream fetches."""

    def __init__(self) -> None:
        """Initialize the scheduler."""
        self.scheduler = AsyncIOScheduler()
        self._fetch_results: dict[int, dict[str, Any]] = {}  # Track results for adaptive scheduling

    async def start(self) -> None:
        """Start the scheduler and load all active streams."""
        logger.info("Starting StreamScheduler...")
        self.scheduler.start()
        await self._load_all_streams()
        self._schedule_cleanup_job()
        self._schedule_trim_pending_job()
        logger.info("StreamScheduler started successfully")

    async def stop(self) -> None:
        """Stop the scheduler gracefully."""
        logger.info("Stopping StreamScheduler...")
        self.scheduler.shutdown(wait=True)
        logger.info("StreamScheduler stopped")

    async def _load_all_streams(self) -> None:
        """Load all active streams from the database and schedule them."""
        with get_db() as db:
            stmt = select(HashtagStream).where(HashtagStream.active.is_(True))
            streams = db.execute(stmt).scalars().all()

        for stream in streams:
            await self.schedule_stream(stream)
        logger.info(f"Scheduled {len(streams)} active streams")

    async def schedule_stream(self, stream: HashtagStream) -> None:
        """Schedule a single stream for periodic fetches.

        Args:
            stream: The HashtagStream to schedule.

        """
        if not stream.enable_scheduling:
            logger.debug(f"Scheduling disabled for stream {stream.id} (#{stream.hashtag})")
            await self._remove_job(stream.id)
            return

        # Remove existing job if present
        await self._remove_job(stream.id)

        # Calculate next fetch time
        now = datetime.now(UTC)
        if stream.next_scheduled_fetch is None or stream.next_scheduled_fetch <= now:
            # Schedule immediately if missed (e.g., after downtime)
            next_fetch = now
        else:
            next_fetch = stream.next_scheduled_fetch

        # Schedule the job
        self.scheduler.add_job(
            self._fetch_stream_job,
            "interval",
            minutes=stream.fetch_interval_minutes,
            id=f"stream_{stream.id}",
            args=[stream.id, stream.hashtag, stream.instance],
            next_run_time=next_fetch,
            replace_existing=False,
        )
        logger.debug(
            f"Scheduled stream {stream.id} (#{stream.hashtag}) "
            f"with interval {stream.fetch_interval_minutes}m, "
            f"next fetch at {next_fetch}"
        )

    async def unschedule_stream(self, stream_id: int) -> None:
        """Remove a stream from the scheduler.

        Args:
            stream_id: The ID of the stream to unschedule.

        """
        await self._remove_job(stream_id)
        logger.debug(f"Unscheduled stream {stream_id}")

    async def update_stream_schedule(
        self,
        stream_id: int,
        enable_scheduling: bool | None = None,
        fetch_interval_minutes: int | None = None,
    ) -> None:
        """Update the schedule for a stream.

        Args:
            stream_id: The ID of the stream to update.
            enable_scheduling: Whether scheduling should be enabled.
            fetch_interval_minutes: New fetch interval in minutes.

        """
        with get_db() as db:
            stmt = select(HashtagStream).where(HashtagStream.id == stream_id)
            stream = db.execute(stmt).scalar_one_or_none()

            if stream is None:
                logger.warning(f"Stream {stream_id} not found")
                return

            if enable_scheduling is not None:
                stream.enable_scheduling = enable_scheduling
            if fetch_interval_minutes is not None:
                stream.fetch_interval_minutes = fetch_interval_minutes
                # Reset next scheduled fetch to trigger immediate reschedule
                stream.next_scheduled_fetch = None

            db.commit()

        # Reschedule the stream
        if stream:
            await self.schedule_stream(stream)

    async def _remove_job(self, stream_id: int) -> None:
        """Remove a job from the scheduler if it exists.

        Args:
            stream_id: The ID of the stream.

        """
        job_id = f"stream_{stream_id}"
        try:
            self.scheduler.remove_job(job_id)
            logger.debug(f"Removed job {job_id}")
        except Exception as e:
            # Job doesn't exist, that's fine
            logger.debug(f"Job {job_id} not found: {e}")

    async def _fetch_stream_job(self, stream_id: int, hashtag: str, instance: str) -> None:
        """Background job to fetch a single stream.

        Args:
            stream_id: The ID of the stream.
            hashtag: The hashtag to fetch.
            instance: The instance to fetch from.

        """
        logger.debug(f"Starting fetch job for stream {stream_id} (#{hashtag}@{instance})")
        try:
            with get_db() as db:
                # Refresh stream data
                stmt = select(HashtagStream).where(HashtagStream.id == stream_id)
                stream = db.execute(stmt).scalar_one_or_none()

                if stream is None:
                    logger.warning(f"Stream {stream_id} not found during fetch")
                    return

                if not stream.active:
                    logger.debug(f"Stream {stream_id} is inactive, skipping fetch")
                    await self.unschedule_stream(stream_id)
                    return

                # Fetch posts
                client = FediverseClient(instance=instance)
                try:
                    processed_posts = await client.fetch_and_process_hashtag(hashtag, limit=50)
                finally:
                    await client.close()

                # Import here to avoid circular imports
                from fenliu.api import _save_fetched_posts  # noqa: PLC0415

                posts_saved = _save_fetched_posts(db, processed_posts, stream_id, instance)
                stream.last_check = datetime.now(UTC)
                stream.last_fetch_count = posts_saved

                # Apply auto-reject if enabled
                auto_rejected = 0
                if posts_saved > 0 and get_auto_reject_setting(db):
                    auto_rejected = reject_blocked_posts(db)

                db.commit()

                # Store results for adaptive scheduling
                self._fetch_results[stream_id] = {
                    "posts_fetched": len(processed_posts),
                    "posts_saved": posts_saved,
                    "auto_rejected": auto_rejected,
                    "timestamp": datetime.now(UTC),
                }

                logger.info(
                    f"Stream {stream_id} (#{hashtag}): "
                    f"fetched={len(processed_posts)}, saved={posts_saved}, auto_rejected={auto_rejected}"
                )

                # Adaptive scheduling: adjust interval based on results
                await self._adapt_schedule(stream, posts_saved)

        except Exception as e:
            logger.error(f"Error fetching stream {stream_id} (#{hashtag}): {e}", exc_info=True)

    def _schedule_cleanup_job(self) -> None:
        """Schedule the daily cleanup job for old delivered posts."""
        self.scheduler.add_job(
            self._cleanup_job,
            "interval",
            hours=24,
            id="cleanup_delivered_posts",
            replace_existing=True,
        )
        logger.info("Scheduled daily cleanup job for old delivered posts")

    async def _cleanup_job(self) -> None:
        """Background job to clean up old delivered posts."""
        logger.debug("Starting cleanup job for delivered posts")
        try:
            with get_db() as db:
                # Import here to avoid circular imports
                from fenliu.api.curated import _cleanup_delivered_posts  # noqa: PLC0415

                deleted = _cleanup_delivered_posts(db, retention_days=7)
                logger.info(f"Cleanup job: deleted {deleted} old delivered posts")
        except Exception as e:
            logger.error(f"Error during cleanup job: {e}", exc_info=True)

    def _schedule_trim_pending_job(self) -> None:
        """Schedule the daily trim pending posts job."""
        self.scheduler.add_job(
            self._trim_pending_job,
            "interval",
            hours=24,
            id="trim_pending_posts",
            replace_existing=True,
        )
        logger.info("Scheduled daily trim job for excess pending posts")

    async def _trim_pending_job(self) -> None:
        """Background job to trim excess pending posts."""
        logger.debug("Starting trim job for pending posts")
        try:
            with get_db() as db:
                # Import here to avoid circular imports
                from fenliu.api.curated import _trim_pending_posts  # noqa: PLC0415

                deleted = _trim_pending_posts(db, lookback_days=3)
                logger.info(f"Trim job: deleted {deleted} excess pending posts")
        except Exception as e:
            logger.error(f"Error during trim job: {e}", exc_info=True)

    async def _adapt_schedule(self, stream: HashtagStream, posts_saved: int) -> None:
        """Adapt the fetch interval based on activity.

        Busier streams (more posts) get checked more frequently.
        Quieter streams get longer intervals to save resources.

        Args:
            stream: The stream that was just fetched.
            posts_saved: Number of posts saved in this fetch.

        """
        # Simple adaptive logic:
        # - If many posts (>10): reduce interval by 20% (minimum 15 min)
        # - If few posts (<2): increase interval by 20% (maximum 240 min)
        # - Otherwise: keep current interval

        current_interval = stream.fetch_interval_minutes
        new_interval = current_interval

        if posts_saved > BUSY_STREAM_THRESHOLD:
            new_interval = max(MIN_ADAPTIVE_INTERVAL, int(current_interval * BUSY_INTERVAL_FACTOR))
        elif posts_saved < QUIET_STREAM_THRESHOLD:
            new_interval = min(MAX_ADAPTIVE_INTERVAL, int(current_interval * QUIET_INTERVAL_FACTOR))

        if new_interval != current_interval:
            logger.debug(
                f"Adapting stream {stream.id} interval: "
                f"{current_interval}m -> {new_interval}m (posts_saved={posts_saved})"
            )
            await self.update_stream_schedule(stream.id, fetch_interval_minutes=new_interval)


async def get_scheduler() -> StreamScheduler:
    """Get or create the global scheduler instance."""
    global _scheduler  # noqa: PLW0603
    if _scheduler is None:
        _scheduler = StreamScheduler()
    return _scheduler


async def start_scheduler() -> None:
    """Start the global scheduler."""
    scheduler = await get_scheduler()
    await scheduler.start()


async def stop_scheduler() -> None:
    """Stop the global scheduler."""
    if _scheduler is not None:
        await _scheduler.stop()
