from collections.abc import Iterator
from datetime import timedelta
from typing import Any

from django.conf import settings
from django.db import transaction
from django.db.models import Max, Min
from django.db.models.query import QuerySet
from django.utils import timezone

import abstract_block_dumper._internal.services.utils as abd_utils
import abstract_block_dumper.models as abd_models


def get_ready_to_retry_attempts() -> Iterator[abd_models.TaskAttempt]:
    return (
        abd_models.TaskAttempt.objects.filter(
            next_retry_at__isnull=False,
            next_retry_at__lte=timezone.now(),
            attempt_count__lt=abd_utils.get_max_attempt_limit(),
        )
        .exclude(
            status=abd_models.TaskAttempt.Status.SUCCESS,
        )
        .iterator()
    )


def executed_block_numbers(executable_path: str, args_json: str, from_block: int, to_block: int) -> set[int]:
    block_numbers = (
        abd_models.TaskAttempt.objects.filter(
            executable_path=executable_path,
            args_json=args_json,
            block_number__gte=from_block,
            block_number__lt=to_block,
            status=abd_models.TaskAttempt.Status.SUCCESS,
        )
        .values_list("block_number", flat=True)
        .iterator()
    )
    return set(block_numbers)


def reset_to_pending(task: abd_models.TaskAttempt) -> None:
    task.celery_task_id = None
    task.status = abd_models.TaskAttempt.Status.PENDING
    task.save()


def revert_to_failed(task: abd_models.TaskAttempt) -> None:
    task.status = abd_models.TaskAttempt.Status.FAILED
    task.save()


def get_recent_phantom_tasks() -> QuerySet[abd_models.TaskAttempt]:
    """
    Get tasks marked as SUCCESS but never actually started.

    Only clean up recent phantom tasks to avoid deleting legitimate external successes
    """
    return abd_models.TaskAttempt.objects.filter(
        status=abd_models.TaskAttempt.Status.SUCCESS,
        last_attempted_at__isnull=True,
        celery_task_id__isnull=True,  # Additional safety check
        created_at__gte=timezone.now() - timedelta(hours=1),  # Only recent tasks
    )


def task_can_retry(task: abd_models.TaskAttempt) -> bool:
    blocked_statuses = {task.Status.SUCCESS, task.Status.RUNNING}
    return task.status not in blocked_statuses and task.attempt_count < abd_utils.get_max_attempt_limit()


def task_mark_as_started(task: abd_models.TaskAttempt, celery_task_id: str) -> None:
    task.celery_task_id = celery_task_id
    task.status = abd_models.TaskAttempt.Status.RUNNING
    task.last_attempted_at = timezone.now()
    task.save()


def task_mark_as_success(task: abd_models.TaskAttempt, result_data: dict) -> None:
    task.status = task.Status.SUCCESS
    task.execution_result = result_data
    task.last_attempted_at = timezone.now()
    task.next_retry_at = None
    task.save()


def task_mark_as_failed(task: abd_models.TaskAttempt) -> None:
    DEFAULT_BLOCK_TASK_RETRY_BACKOFF = 1
    MAX_RETRY_DELAY_MINUTES = 1440  # 24 hours max delay

    task.status = task.Status.FAILED
    task.last_attempted_at = timezone.now()
    task.attempt_count += 1

    if task_can_retry(task):
        base_retry_backoff = getattr(settings, "BLOCK_TASK_RETRY_BACKOFF", DEFAULT_BLOCK_TASK_RETRY_BACKOFF)
        max_delay_minutes = getattr(settings, "BLOCK_TASK_MAX_RETRY_DELAY_MINUTES", MAX_RETRY_DELAY_MINUTES)

        # Calculate exponential backoff with bounds checking
        backoff_minutes = base_retry_backoff**task.attempt_count
        backoff_minutes = min(backoff_minutes, max_delay_minutes)

        task.next_retry_at = timezone.now() + timedelta(minutes=backoff_minutes)
    else:
        task.next_retry_at = None
    task.save()


def task_schedule_to_retry(task: abd_models.TaskAttempt) -> None:
    task.status = abd_models.TaskAttempt.Status.PENDING
    task.save()


def task_create_or_get_pending(
    block_number: int,
    executable_path: str,
    args: dict[str, Any] | None = None,
) -> tuple[abd_models.TaskAttempt, bool]:
    """
    Create or get a pending task attempt.

    Returns (task, created) where created indicates if a new task was created.

    For failed tasks that can retry:
    - If next_retry_at is in the future, leave task as FAILED (will be picked up by scheduler)
    - If next_retry_at is in the past or None, reset to PENDING for immediate execution
    """
    if args is None:
        args = {}

    args_json = abd_utils.serialize_args(args)

    with transaction.atomic():
        task, created = abd_models.TaskAttempt.objects.get_or_create(
            block_number=block_number,
            executable_path=executable_path,
            args_json=args_json,
            defaults={"status": abd_models.TaskAttempt.Status.PENDING},
        )

        # Don't modify tasks that are already in a terminal or active state
        active_state = {abd_models.TaskAttempt.Status.SUCCESS, abd_models.TaskAttempt.Status.RUNNING}
        if created or task.status in active_state:
            return task, created

        # For failed tasks that can retry, only reset to PENDING if retry time has passed
        if task.status == abd_models.TaskAttempt.Status.FAILED and task_can_retry(task):
            now = timezone.now()
            if task.next_retry_at is None or task.next_retry_at <= now:
                task.status = abd_models.TaskAttempt.Status.PENDING
                task.save()
    return task, created


def get_the_latest_executed_block_number() -> int | None:
    result = abd_models.TaskAttempt.objects.aggregate(max_block=Max("block_number"))
    return result["max_block"]


def get_block_range() -> tuple[int | None, int | None]:
    """Get the min and max block numbers from all task attempts."""
    result = abd_models.TaskAttempt.objects.aggregate(
        min_block=Min("block_number"),
        max_block=Max("block_number"),
    )
    return result["min_block"], result["max_block"]


def get_successful_block_numbers(from_block: int, to_block: int) -> set[int]:
    """Get all block numbers with at least one successful task in the range."""
    block_numbers = (
        abd_models.TaskAttempt.objects.filter(
            block_number__gte=from_block,
            block_number__lte=to_block,
            status=abd_models.TaskAttempt.Status.SUCCESS,
        )
        .values_list("block_number", flat=True)
        .distinct()
        .iterator()
    )
    return set(block_numbers)
