from __future__ import annotations

import json
from collections import defaultdict
from datetime import date, datetime, time, timedelta

from sqlalchemy import and_, or_

from ..extensions import db
from ..models import BadgeDefinition, MonthlyScoreSnapshot, TaskInstance, User, UserBadge
from .dates import month_bounds, previous_month


def _max_day_streak(days: set[date]) -> int:
    if not days:
        return 0
    ordered = sorted(days)
    best = streak = 1
    for previous, current in zip(ordered, ordered[1:]):
        if current == previous + timedelta(days=1):
            streak += 1
        else:
            streak = 1
        best = max(best, streak)
    return best


def award_badge(
    user: User,
    badge_key: str,
    *,
    awarded_at: datetime | None = None,
    context: dict | None = None,
) -> UserBadge | None:
    definition = BadgeDefinition.query.filter_by(key=badge_key, active=True).first()
    if not definition:
        return None

    existing = UserBadge.query.filter_by(user_id=user.id, badge_definition_id=definition.id).first()
    if existing:
        return None

    award = UserBadge(
        user_id=user.id,
        badge_definition_id=definition.id,
        awarded_at=awarded_at or datetime.utcnow(),
        context=json.dumps(context, sort_keys=True) if context else None,
    )
    db.session.add(award)
    db.session.flush()
    return award


def badge_awards_for_month(user_id: int, year: int, month: int) -> list[UserBadge]:
    start, end = month_bounds(year, month)
    return (
        UserBadge.query.filter(
            UserBadge.user_id == user_id,
            UserBadge.awarded_at >= start,
            UserBadge.awarded_at < end,
        )
        .join(BadgeDefinition)
        .filter(BadgeDefinition.active.is_(True))
        .order_by(UserBadge.awarded_at.asc())
        .all()
    )


def earned_badges_for_user(user_id: int) -> list[UserBadge]:
    return (
        UserBadge.query.filter_by(user_id=user_id)
        .join(BadgeDefinition)
        .order_by(UserBadge.awarded_at.desc())
        .all()
    )


def _completion_metrics(user: User) -> dict[str, int]:
    tasks = (
        TaskInstance.query.filter(
            TaskInstance.completed_by_user_id == user.id,
            TaskInstance.completed_at.isnot(None),
        )
        .order_by(TaskInstance.completed_at.asc())
        .all()
    )

    completion_days: set[date] = set()
    metrics = defaultdict(int)
    max_points = 0

    for task in tasks:
        completion_day = task.completed_at.date()
        completion_days.add(completion_day)
        metrics["first_task_completed"] += 1
        metrics["total_tasks_completed"] += 1
        if completion_day <= task.due_date:
            metrics["on_time_tasks_completed"] += 1
        if completion_day <= task.due_date - timedelta(days=1):
            metrics["early_tasks_completed"] += 1
        if task.assigned_user_ids and user.id not in task.assigned_user_ids:
            metrics["foreign_tasks_completed"] += 1
        max_points = max(max_points, task.points_awarded)

    metrics["streak_days"] = _max_day_streak(completion_days)
    metrics["high_point_task"] = max_points
    return metrics


def evaluate_task_badges(user: User, *, notify: bool = False) -> list[UserBadge]:
    definitions = BadgeDefinition.query.filter_by(active=True).all()
    metrics = _completion_metrics(user)
    unlocked: list[UserBadge] = []

    for definition in definitions:
        metric_value = metrics.get(definition.trigger_type)
        if metric_value is None:
            continue
        if metric_value >= definition.threshold:
            award = award_badge(user, definition.key)
            if award:
                unlocked.append(award)

    if unlocked:
        db.session.commit()
        if notify:
            from .notifications import send_badge_notifications_for_awards

            send_badge_notifications_for_awards(unlocked)
    return unlocked


def _month_end_award_time(year: int, month: int) -> datetime:
    _, end = month_bounds(year, month)
    return end - timedelta(seconds=1)


def _user_had_clean_month(user_id: int, year: int, month: int) -> bool:
    start_date = date(year, month, 1)
    end_date = (month_bounds(year, month)[1] - timedelta(days=1)).date()
    tasks = TaskInstance.query.filter(
        or_(
            TaskInstance.assigned_user_id == user_id,
            TaskInstance.assigned_user_secondary_id == user_id,
        ),
        TaskInstance.due_date >= start_date,
        TaskInstance.due_date <= end_date,
    ).all()

    if not tasks:
        return False

    for task in tasks:
        if not task.completed_at:
            return False
        if task.completed_at.date() > task.due_date:
            return False
    return True


def _winner_user_ids(year: int, month: int) -> set[int]:
    rows = MonthlyScoreSnapshot.query.filter_by(year=year, month=month, rank=1).all()
    return {row.user_id for row in rows}


def evaluate_monthly_badges(year: int, month: int, *, notify: bool = False) -> list[UserBadge]:
    award_time = _month_end_award_time(year, month)
    unlocked: list[UserBadge] = []
    winners = _winner_user_ids(year, month)
    previous_year, previous_month_value = previous_month(year, month)
    previous_winners = _winner_user_ids(previous_year, previous_month_value)

    for user in User.query.order_by(User.id.asc()).all():
        if user.id in winners:
            award = award_badge(user, "monthly_champion", awarded_at=award_time, context={"year": year, "month": month})
            if award:
                unlocked.append(award)

            if user.id in previous_winners:
                award = award_badge(user, "title_defender", awarded_at=award_time, context={"year": year, "month": month})
                if award:
                    unlocked.append(award)
            elif MonthlyScoreSnapshot.query.filter_by(year=previous_year, month=previous_month_value, user_id=user.id).first():
                award = award_badge(user, "comeback_kid", awarded_at=award_time, context={"year": year, "month": month})
                if award:
                    unlocked.append(award)

        if _user_had_clean_month(user.id, year, month):
            award = award_badge(user, "white_glove", awarded_at=award_time, context={"year": year, "month": month})
            if award:
                unlocked.append(award)

    if unlocked:
        db.session.commit()
        if notify:
            from .notifications import send_badge_notifications_for_awards

            send_badge_notifications_for_awards(unlocked)
    return unlocked


def sync_existing_badges() -> None:
    for user in User.query.order_by(User.id.asc()).all():
        evaluate_task_badges(user)

    archived_months = (
        db.session.query(MonthlyScoreSnapshot.year, MonthlyScoreSnapshot.month)
        .group_by(MonthlyScoreSnapshot.year, MonthlyScoreSnapshot.month)
        .order_by(MonthlyScoreSnapshot.year.asc(), MonthlyScoreSnapshot.month.asc())
        .all()
    )
    for row in archived_months:
        evaluate_monthly_badges(row.year, row.month)
