"""Schedule evaluator: checks due task templates and creates tasks.

This module is called by:
- peon-loop (periodic background check)
- The manual /api/schedules/{id}/trigger endpoint
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

from peon_mcp.scheduling.cron import validate_cron_expression as _validate

try:
    from croniter import CroniterBadCronError, croniter

    _CRONITER_AVAILABLE = True
except ImportError:
    _CRONITER_AVAILABLE = False

logger = logging.getLogger(__name__)


def validate_cron_expression(expression: str) -> tuple[bool, str]:
    """Validate a cron expression.

    Args:
        expression: A 5-field cron expression string.

    Returns:
        (is_valid, error_message) — error_message is empty when valid.
    """
    parts = expression.strip().split()
    if len(parts) != 5:
        return False, f"Expected 5 fields, got {len(parts)}"

    if not _validate(expression):
        return False, f"Invalid cron expression: '{expression}'"

    return True, ""


def compute_next_run(
    cron_expression: str,
    tz: str,
    after: datetime,
) -> datetime | None:
    """Compute the next scheduled run for a cron expression in the given timezone.

    Args:
        cron_expression: Standard 5-field cron expression.
        tz: IANA timezone name (e.g. 'America/New_York', 'UTC').
        after: Compute next run after this datetime (timezone-aware).

    Returns:
        Next run datetime in UTC, or None if the expression is invalid or
        the timezone is unknown.
    """
    if not _CRONITER_AVAILABLE:
        return None

    try:
        tz_obj = ZoneInfo(tz)
    except (ZoneInfoNotFoundError, KeyError):
        logger.warning("Unknown timezone '%s', falling back to UTC", tz)
        tz_obj = ZoneInfo("UTC")

    # Ensure after is timezone-aware; default to UTC if naive
    if after.tzinfo is None:
        after = after.replace(tzinfo=timezone.utc)

    # Convert to template's timezone for correct DST-aware cron evaluation
    after_in_tz = after.astimezone(tz_obj)
    after_naive = after_in_tz.replace(tzinfo=None)

    try:
        cron = croniter(cron_expression, after_naive)
        next_dt = cron.get_next(datetime)
        # Attach the template timezone so DST transitions are handled correctly
        next_dt_aware = next_dt.replace(tzinfo=tz_obj)
        return next_dt_aware.astimezone(timezone.utc)
    except (CroniterBadCronError, ValueError):
        logger.warning("Failed to compute next run for expression '%s'", cron_expression)
        return None


async def evaluate_schedules(db) -> list[dict]:
    """Find all due task templates and create tasks for them.

    Queries all enabled templates whose next_run_at is in the past (or NULL
    if they've never run) and creates a new task for each.

    Args:
        db: An aiosqlite database connection.

    Returns:
        List of created task dicts (id, project_id, title, template_id).
    """
    from peon_mcp.db import row_to_dict

    now = datetime.now(timezone.utc)
    now_iso = now.isoformat()
    # SQLite-compatible format (space separator, UTC, no tzinfo) used for
    # fields compared against SQLite's datetime('now') in next_task queries.
    now_sqlite = now.strftime("%Y-%m-%d %H:%M:%S")

    # Fetch templates that are due: next_run_at <= now, or NULL (never run)
    rows = await db.execute_fetchall(
        """
        SELECT * FROM task_templates
        WHERE enabled = 1
          AND (next_run_at IS NULL OR next_run_at <= ?)
        ORDER BY next_run_at ASC NULLS FIRST
        """,
        (now_iso,),
    )

    created_tasks: list[dict] = []

    for row in rows:
        template = row_to_dict(row)
        template_id = template["id"]
        cron_expr = template["cron_expression"]
        tz = template.get("timezone") or "UTC"

        # Determine scheduled_at for this task (the time that was due).
        # Normalize to SQLite-compatible format so the next_task WHERE clause
        # (scheduled_at <= datetime('now')) works correctly via string comparison.
        raw_scheduled_at = template.get("next_run_at") or now_sqlite
        try:
            _dt = datetime.fromisoformat(raw_scheduled_at)
            if _dt.tzinfo is not None:
                _dt = _dt.astimezone(timezone.utc).replace(tzinfo=None)
            scheduled_at = _dt.strftime("%Y-%m-%d %H:%M:%S")
        except (ValueError, AttributeError):
            scheduled_at = raw_scheduled_at

        try:
            cursor = await db.execute(
                """
                INSERT INTO tasks
                    (project_id, feature_id, title, description, priority, status, scheduled_at)
                VALUES (?, ?, ?, ?, ?, 'todo', ?)
                """,
                (
                    template["project_id"],
                    template.get("feature_id"),
                    template["title"],
                    template.get("description", ""),
                    template.get("priority", "medium"),
                    scheduled_at,
                ),
            )
            task_id = cursor.lastrowid
        except Exception:
            logger.exception("Failed to create task for template %d", template_id)
            continue

        # Compute next run time and update the template
        next_run = compute_next_run(cron_expr, tz, now)
        next_run_iso = next_run.isoformat() if next_run else None

        await db.execute(
            """
            UPDATE task_templates
            SET last_run_at = ?,
                next_run_at = ?,
                updated_at = CURRENT_TIMESTAMP
            WHERE id = ?
            """,
            (now_iso, next_run_iso, template_id),
        )

        created_tasks.append({
            "task_id": task_id,
            "template_id": template_id,
            "project_id": template["project_id"],
            "title": template["title"],
            "scheduled_at": scheduled_at,
            "next_run_at": next_run_iso,
        })

        logger.info(
            "Created task #%d from template #%d '%s' (next run: %s)",
            task_id,
            template_id,
            template["title"],
            next_run_iso,
        )

    if created_tasks:
        await db.commit()

    return created_tasks
