"""Recurring task rule validation, date calculation, and scheduled spawning."""
import calendar
import logging
from datetime import date, datetime, timedelta, timezone

from sqlalchemy import and_, select

from scribe.models import async_session
from scribe.models.note import Note, TaskStatus

logger = logging.getLogger(__name__)

_VALID_UNITS = {"day", "week", "month", "year"}


def validate_recurrence_rule(rule: dict) -> None:
    """Raise ValueError if rule is structurally invalid."""
    if not isinstance(rule, dict):
        raise ValueError("recurrence_rule must be an object")
    rule_type = rule.get("type")
    if rule_type not in ("interval", "calendar"):
        raise ValueError("recurrence_rule.type must be 'interval' or 'calendar'")
    unit = rule.get("unit")
    if unit not in _VALID_UNITS:
        raise ValueError(f"recurrence_rule.unit must be one of {_VALID_UNITS}")
    if rule_type == "interval":
        every = rule.get("every")
        if not isinstance(every, int) or every < 1:
            raise ValueError("recurrence_rule.every must be a positive integer")
    elif rule_type == "calendar":
        day = rule.get("day_of_month")
        if not isinstance(day, int) or not 1 <= day <= 31:
            raise ValueError("recurrence_rule.day_of_month must be an integer between 1 and 31")
        if unit == "year":
            month = rule.get("month")
            if not isinstance(month, int) or not 1 <= month <= 12:
                raise ValueError("recurrence_rule.month must be an integer between 1 and 12")
        elif unit != "month":
            raise ValueError("calendar recurrence only supports unit 'month' or 'year'")


def _add_months(d: date, months: int) -> date:
    month = d.month - 1 + months
    year = d.year + month // 12
    month = month % 12 + 1
    day = min(d.day, calendar.monthrange(year, month)[1])
    return d.replace(year=year, month=month, day=day)


def _next_day_of_month(from_date: date, day: int) -> date:
    """Return the next occurrence of day-of-month strictly after from_date."""
    year, month = from_date.year, from_date.month
    max_day = calendar.monthrange(year, month)[1]
    clamped = min(day, max_day)
    if clamped > from_date.day:
        return from_date.replace(day=clamped)
    # Advance one month
    if month == 12:
        year, month = year + 1, 1
    else:
        month += 1
    return date(year, month, min(day, calendar.monthrange(year, month)[1]))


def _next_annual_date(from_date: date, month: int, day: int) -> date:
    """Return the next occurrence of month/day after from_date."""
    max_day = calendar.monthrange(from_date.year, month)[1]
    candidate = date(from_date.year, month, min(day, max_day))
    if candidate > from_date:
        return candidate
    return date(
        from_date.year + 1,
        month,
        min(day, calendar.monthrange(from_date.year + 1, month)[1]),
    )


def calculate_next_due(rule: dict, from_date: date) -> date:
    """Return the next due date after from_date according to rule."""
    rule_type = rule["type"]
    unit = rule["unit"]
    if rule_type == "interval":
        every = rule["every"]
        if unit == "day":
            return from_date + timedelta(days=every)
        if unit == "week":
            return from_date + timedelta(weeks=every)
        if unit == "month":
            return _add_months(from_date, every)
        if unit == "year":
            return _add_months(from_date, every * 12)
    elif rule_type == "calendar":
        if unit == "month":
            return _next_day_of_month(from_date, rule["day_of_month"])
        if unit == "year":
            return _next_annual_date(from_date, rule["month"], rule["day_of_month"])
    raise ValueError(f"Unhandled recurrence rule: {rule!r}")


async def spawn_recurring_tasks() -> int:
    """Create child tasks for all recurring tasks whose spawn date has arrived.

    Returns the number of tasks spawned.
    """
    from scribe.services.notes import create_note

    now = datetime.now(timezone.utc)
    spawned = 0

    async with async_session() as session:
        result = await session.execute(
            select(Note).where(
                and_(
                    Note.recurrence_rule.isnot(None),
                    Note.recurrence_next_spawn_at <= now,
                    # Never spawn children off a trashed parent — that would
                    # resurrect work the user explicitly deleted.
                    Note.deleted_at.is_(None),
                )
            )
        )
        tasks = result.scalars().all()

    for task in tasks:
        base_date = task.due_date or now.date()
        next_due = calculate_next_due(task.recurrence_rule, base_date)
        try:
            await create_note(
                task.user_id,
                title=task.title,
                body=task.body or "",
                status=TaskStatus.todo.value,
                priority=task.priority or "none",
                due_date=next_due,
                tags=list(task.tags or []),
                project_id=task.project_id,
                milestone_id=task.milestone_id,
                recurrence_rule=task.recurrence_rule,
            )
            # No explicit embed here: create_note calls embed_note itself, so
            # the spawn path stopped being a second copy of the embedding rule
            # the moment that moved into the service (#2056, #280).
        except Exception:
            logger.exception("Failed to spawn recurring task %d", task.id)
            continue

        async with async_session() as session:
            parent = await session.get(Note, task.id)
            if parent:
                parent.recurrence_next_spawn_at = None
                await session.commit()

        spawned += 1
        logger.info("Spawned recurring task %d → child due %s", task.id, next_due)

    return spawned
