"""
Hourly scheduler that sends push notifications to parents when their children's
scheduled chores are going to expire within the next 75 minutes and haven't been
marked completed or submitted for confirmation yet.

Window rationale (75 min):
  The spec requires that chores expiring between 9:00 pm and 9:15 pm both
  generate a notification at 8:00 pm.  A 75-minute look-ahead window
  (now < deadline <= now + 75 min) satisfies this for the hourly cron run at
  the top of each hour.

Double-notification note:
  A chore deadline that falls within the overlap of two consecutive 75-minute
  windows (e.g. 9:10 pm appears in both the 8:00 pm and 9:00 pm windows) may
  produce two push notifications.  No deduplication is applied; this is
  intentional per the product decision made during spec review.
"""

import logging
import os
from datetime import datetime, timedelta, timezone

from apscheduler.schedulers.background import BackgroundScheduler
from tinydb import Query

from utils.schedule_utils import get_due_time_today, is_scheduled_today
from utils.push_sender import send_push_to_user

logger = logging.getLogger(__name__)

_WINDOW_MINUTES = 75


def get_expiring_chores_for_user(
    user_id: str,
    tz_str: str | None,
    now_dt: datetime,
) -> list[dict]:
    """Return chores that will expire within the next _WINDOW_MINUTES minutes.

    Each entry is::

        {
            'child_id': str,
            'child_name': str,
            'task_id': str,
            'task_name': str,
        }

    A chore is included only when ALL of the following are true:
    - It has a ChoreSchedule with enabled=True
    - It is scheduled today (days or interval mode)
    - It has a deadline (not Anytime)
    - The deadline falls in (now_dt, now_dt + _WINDOW_MINUTES]
    - There is no existing pending_confirmation record for this child + task
      (status='pending' means awaiting approval; status='approved' means done)

    Must be called within a Flask app context.
    """
    from db.db import child_db, task_db, pending_confirmations_db
    from db.chore_schedules import get_schedule

    try:
        from zoneinfo import ZoneInfo
        local_now = now_dt.astimezone(ZoneInfo(tz_str)) if tz_str else now_dt
    except Exception:
        local_now = now_dt

    local_date = local_now.date()
    window_end = local_now + timedelta(minutes=_WINDOW_MINUTES)

    ChildQ = Query()
    children = child_db.search(ChildQ.user_id == user_id)

    expiring: list[dict] = []

    for child_dict in children:
        child_id = child_dict.get('id')
        child_name = child_dict.get('name', 'Unknown')
        task_ids = child_dict.get('tasks', [])

        TaskQ = Query()
        for task_id in task_ids:
            task = task_db.get(
                (TaskQ.id == task_id) &
                (TaskQ.type == 'chore') &
                ((TaskQ.user_id == user_id) | (TaskQ.user_id == None))
            )
            if not task:
                continue

            schedule = get_schedule(child_id, task_id)
            if schedule is None:
                continue

            schedule_dict = schedule.to_dict()

            if not schedule_dict.get('enabled', True):
                continue  # paused schedules have no deadline

            if not is_scheduled_today(schedule_dict, local_date):
                continue

            due = get_due_time_today(schedule_dict, local_date)
            if due is None:
                continue  # Anytime — no expiry

            due_hour, due_minute = due
            # Build a timezone-aware deadline datetime for the next occurrence of the due time.
            # Schedules run hourly and may span midnight (e.g. a 23:00 run needs to catch
            # a chore due at 00:15 the next day), so roll forward a day when the candidate
            # deadline has already passed.
            deadline_dt = local_now.replace(
                hour=due_hour, minute=due_minute, second=0, microsecond=0
            )
            if deadline_dt <= local_now:
                deadline_dt = deadline_dt + timedelta(days=1)

            # Include only when deadline is strictly after now and within window
            if not (local_now < deadline_dt <= window_end):
                continue

            # Skip if any confirmation record exists (pending OR approved = done today)
            PendingQ = Query()
            has_confirmation = pending_confirmations_db.get(
                (PendingQ.child_id == child_id) &
                (PendingQ.entity_id == task_id) &
                (PendingQ.entity_type == 'chore') &
                (PendingQ.user_id == user_id)
            )
            if has_confirmation:
                continue

            expiring.append({
                'child_id': child_id,
                'child_name': child_name,
                'task_id': task_id,
                'task_name': task.get('name', 'Unknown'),
            })

    return expiring


def _build_push_payload(expiring: list[dict]) -> dict:
    """Build the push notification payload from a list of expiring chore entries."""
    if len(expiring) == 1:
        entry = expiring[0]
        title = f"Chore ending soon: {entry['task_name']}"
        child_id = entry['child_id']
        entity_id = entry['task_id']
    else:
        title = 'Chores ending soon'
        child_id = None
        entity_id = None

    # Group by child and format body text
    by_child: dict[str, list[str]] = {}
    for entry in expiring:
        by_child.setdefault(entry['child_name'], []).append(entry['task_name'])

    body_lines = [
        f"{name}: {', '.join(tasks)}" for name, tasks in by_child.items()
    ]
    body = '\n'.join(body_lines)

    return {
        'type': 'chore_expiring_soon',
        'title': title,
        'body': body,
        'child_id': child_id,
        'entity_id': entity_id,
        'entity_type': 'chore',
    }


def send_chore_expiry_notifications_for_user(user_id: str, tz_str: str | None) -> int:
    """Check for expiring chores and send a push notification if any are found.

    Returns the number of chores included in the notification (0 = nothing sent).
    Must be called within a Flask app context.
    """
    now_dt = datetime.now(timezone.utc)
    expiring = get_expiring_chores_for_user(user_id, tz_str, now_dt)
    if not expiring:
        return 0

    payload = _build_push_payload(expiring)
    send_push_to_user(user_id, payload)
    logger.info(
        f'Chore expiry: sent notification for {len(expiring)} chore(s) to user {user_id}'
    )
    return len(expiring)


def run_chore_expiry_check(app) -> None:
    """Check all verified, push-enabled users for soon-to-expire chores."""
    if os.environ.get('DB_ENV') == 'e2e':
        return

    with app.app_context():
        try:
            from db.db import users_db

            UserQ = Query()
            users = users_db.search(UserQ.verified == True)

            for user_dict in users:
                user_id = user_dict.get('id')
                tz_str = user_dict.get('timezone')

                if not user_dict.get('push_notifications_enabled', True):
                    continue

                try:
                    count = send_chore_expiry_notifications_for_user(user_id, tz_str)
                    if count:
                        logger.info(
                            f'Chore expiry: notified {count} expiring chore(s) for user {user_id}'
                        )
                except Exception as e:
                    logger.error(f'Chore expiry: failed for user {user_id}: {e}')

        except Exception as e:
            logger.error(f'Error in run_chore_expiry_check: {e}')


def start_chore_expiry_notification_scheduler(app) -> BackgroundScheduler:
    """Start the hourly chore-expiry notification scheduler.

    Runs at the top of every hour (minute=0), matching the existing schedulers.
    Returns the scheduler instance.
    """
    scheduler = BackgroundScheduler()
    scheduler.add_job(
        run_chore_expiry_check,
        'cron',
        minute=0,
        args=[app],
    )
    scheduler.start()
    logger.info('Chore expiry notification scheduler started (runs every hour at :00)')
    return scheduler
