"""Scheduler engine — asyncio-based background loop for autonomous outreach.

Runs alongside the MCP server via FastMCP's lifespan hook.
Ticks every 60 seconds, checking for ready work and executing it.

Key responsibilities:
1. Execute ready jobs from the DB-backed job queue
2. Schedule new work (invites, follow-ups) for autopilot campaigns
3. Periodically check for replies (every 5 min)
4. Periodically schedule engagement warm-ups (every 30 min)
5. Retry failed jobs (up to 3 attempts)
6. Cleanup old completed jobs (7 days)
"""

from __future__ import annotations

import asyncio
import logging
import time
from typing import Any

from ..config import is_scheduler_enabled
from ..constants import (
    JOB_CHECK_REPLIES,
    JOB_COMPLETED,
    JOB_ENGAGE,
    JOB_FAILED,
    JOB_FOLLOWUP,
    JOB_INVITE,
    SCHEDULER_CLEANUP_DAYS,
    SCHEDULER_ENGAGEMENT_SECONDS,
    SCHEDULER_MAX_RETRIES,
    SCHEDULER_REPLY_CHECK_SECONDS,
    SCHEDULER_RETRY_DELAY,
    SCHEDULER_TICK_SECONDS,
    STATUS_ACTIVE,
)
from ..db.queries import (
    claim_job,
    cleanup_old_jobs,
    complete_job,
    get_ready_jobs,
    get_setting,
    list_campaigns,
    retry_job,
)

logger = logging.getLogger(__name__)


class SchedulerEngine:
    """Asyncio-based scheduler for autonomous outreach execution.

    Runs as a background task in the same event loop as the MCP server.
    All outreach logic is delegated to executors (which reuse existing tool internals).
    """

    def __init__(self) -> None:
        self._running = False
        self._task: asyncio.Task[None] | None = None
        self._last_reply_check: float = 0
        self._last_engagement_plan: float = 0
        self._last_cleanup: float = 0
        self._tick_count: int = 0
        self._errors_in_row: int = 0

    @property
    def is_running(self) -> bool:
        return self._running

    @property
    def tick_count(self) -> int:
        return self._tick_count

    async def start(self) -> None:
        """Start the scheduler background loop."""
        if self._running:
            logger.warning("Scheduler already running")
            return
        self._running = True
        self._task = asyncio.create_task(self._run_loop())
        logger.info("Scheduler engine started")

    async def stop(self) -> None:
        """Gracefully stop the scheduler."""
        self._running = False
        if self._task and not self._task.done():
            self._task.cancel()
            try:
                await self._task
            except asyncio.CancelledError:
                pass
        self._task = None
        logger.info("Scheduler engine stopped")

    async def _run_loop(self) -> None:
        """Main tick loop — runs every SCHEDULER_TICK_SECONDS."""
        # Small initial delay to let the MCP server finish startup
        await asyncio.sleep(5)

        while self._running:
            try:
                await self._tick()
                self._errors_in_row = 0
            except asyncio.CancelledError:
                break
            except Exception as e:
                self._errors_in_row += 1
                logger.error("Scheduler tick error (%d in a row): %s", self._errors_in_row, e)
                # Back off if too many consecutive errors
                if self._errors_in_row >= 5:
                    logger.warning("Too many scheduler errors, backing off for 5 minutes")
                    await asyncio.sleep(300)
                    self._errors_in_row = 0

            await asyncio.sleep(SCHEDULER_TICK_SECONDS)

    async def _tick(self) -> None:
        """Single scheduler tick — the heart of the autonomous system."""
        self._tick_count += 1

        # Guard: scheduler must be enabled in config
        if not is_scheduler_enabled():
            return

        # Guard: setup must be complete
        if not get_setting("setup_complete", False):
            return

        now = time.time()

        # 1. Execute ready jobs from the DB queue
        await self._process_ready_jobs()

        # 2. Schedule new work for all active autopilot campaigns
        await self._schedule_new_work()

        # 3. Periodic: check replies (every 5 min)
        if now - self._last_reply_check >= SCHEDULER_REPLY_CHECK_SECONDS:
            await self._schedule_reply_checks()
            self._last_reply_check = now

        # 4. Periodic: engagement warm-ups (every 30 min)
        if now - self._last_engagement_plan >= SCHEDULER_ENGAGEMENT_SECONDS:
            await self._schedule_engagements()
            self._last_engagement_plan = now

        # 5. Cleanup old completed jobs (once per hour)
        if now - self._last_cleanup >= 3600:
            try:
                deleted = cleanup_old_jobs(days=SCHEDULER_CLEANUP_DAYS)
                if deleted > 0:
                    logger.debug("Cleaned up %d old scheduler jobs", deleted)
            except Exception as e:
                logger.debug("Cleanup failed: %s", e)
            self._last_cleanup = now

    async def _process_ready_jobs(self) -> None:
        """Find and execute jobs that are ready (scheduled_at <= now)."""
        from .executors import execute_job

        ready = get_ready_jobs(limit=5)
        for job in ready:
            job_id = job["id"]
            job_type = job["job_type"]

            # Claim the job atomically (prevents double execution)
            if not claim_job(job_id):
                logger.debug("Job %s already claimed, skipping", job_id)
                continue

            logger.info("Executing scheduler job: %s (type=%s)", job_id, job_type)

            try:
                result = await execute_job(job)
                complete_job(job_id)
                logger.info("Job %s completed successfully", job_id)
            except Exception as e:
                error_msg = str(e)[:500]
                complete_job(job_id, error=error_msg)
                logger.warning("Job %s failed: %s", job_id, error_msg)

                # Retry if under max retries
                retry_count = job.get("retry_count", 0) + 1
                if retry_count < SCHEDULER_MAX_RETRIES:
                    new_time = int(time.time()) + SCHEDULER_RETRY_DELAY
                    retry_job(job_id, new_time)
                    logger.info("Job %s scheduled for retry #%d", job_id, retry_count)

    async def _schedule_new_work(self) -> None:
        """Schedule new invite and follow-up jobs for active autopilot campaigns."""
        from .planner import plan_campaign_work

        campaigns = list_campaigns(status=STATUS_ACTIVE)
        for campaign in campaigns:
            # Only schedule for autopilot campaigns
            if campaign.get("mode") != "autopilot":
                continue

            try:
                await plan_campaign_work(campaign["id"])
            except Exception as e:
                logger.debug("Planning failed for campaign %s: %s", campaign["id"], e)

    async def _schedule_reply_checks(self) -> None:
        """Schedule reply check jobs for all active campaigns."""
        from ..db.queries import create_scheduler_job, get_pending_job_count

        campaigns = list_campaigns(status=STATUS_ACTIVE)
        now = int(time.time())

        for campaign in campaigns:
            campaign_id = campaign["id"]
            # Skip if a reply check is already pending
            if get_pending_job_count(campaign_id, JOB_CHECK_REPLIES) > 0:
                continue

            create_scheduler_job(
                campaign_id=campaign_id,
                job_type=JOB_CHECK_REPLIES,
                scheduled_at=now,  # Execute immediately
            )
            logger.debug("Scheduled reply check for campaign %s", campaign_id)

    async def _schedule_engagements(self) -> None:
        """Schedule engagement warm-up jobs for active autopilot campaigns."""
        from .planner import plan_engagements

        campaigns = list_campaigns(status=STATUS_ACTIVE)
        for campaign in campaigns:
            if campaign.get("mode") != "autopilot":
                continue

            try:
                await plan_engagements(campaign["id"])
            except Exception as e:
                logger.debug("Engagement planning failed for campaign %s: %s", campaign["id"], e)
