from datetime import datetime, time

from django.db.models import Count, Q
from django.utils.dateparse import parse_date, parse_datetime
from django.utils import timezone
from rest_framework.decorators import api_view, authentication_classes, permission_classes, throttle_classes
from .permissions import HasAPIKey
from rest_framework.response import Response
from rest_framework import status

from .authentication import APIKeyAuthentication
from .models import City, Engagement, Spot, Ward
from .serializers import RealityPotentialSerializer, SpotDetailSerializer, WardSerializer
from .throttling import APIKeyRateThrottle


AUTH = [APIKeyAuthentication]
THROTTLES = [APIKeyRateThrottle]


def ok(data):
    return Response({"success": True, "data": data, "error": None})


def error(code, message, http_status=status.HTTP_400_BAD_REQUEST):
    return Response(
        {"success": False, "data": None, "error": {"code": code, "message": message}},
        status=http_status,
    )


def parse_bool(value, name):
    if value is None:
        return None
    lowered = value.lower()
    if lowered in {"true", "1", "yes"}:
        return True
    if lowered in {"false", "0", "no"}:
        return False
    raise ValueError(f"The '{name}' parameter must be true or false.")


def parse_iso_datetime(value, name, end_of_day=False):
    if not value:
        return None
    parsed = parse_datetime(value)
    if parsed:
        return parsed
    parsed_date = parse_date(value)
    if parsed_date:
        value = datetime.combine(parsed_date, time.max if end_of_day else time.min)
        return timezone.make_aware(value, timezone.get_current_timezone())
    raise ValueError(f"The '{name}' parameter must be a valid ISO 8601 date/datetime.")


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def hello_world(request):
    return ok({"message": "Hello, world!"})


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def city_wards(request, city_name):
    wards = Ward.objects.filter(city__name__iexact=city_name).prefetch_related("spots").order_by("rank")
    if not wards.exists():
        return error("NOT_FOUND", f"No data found for city '{city_name}'.", status.HTTP_404_NOT_FOUND)
    city = wards.first().city.name
    return ok({"city": city, "wards": WardSerializer(wards, many=True).data})


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def wards(request):
    city_name = request.query_params.get("city")
    if not city_name:
        return error("INVALID_PARAMETER", "The 'city' parameter is required.")
    return city_wards(request, city_name)


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def spot_detail(request, spot_id):
    spot = Spot.objects.prefetch_related("images").filter(spot_id=spot_id).first()
    if not spot:
        return error("NOT_FOUND", "Spot does not exist.", status.HTTP_404_NOT_FOUND)
    return ok(SpotDetailSerializer(spot).data)


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def leaderboard(request):
    city_name = request.query_params.get("city") or request.query_params.get("region")
    period = request.query_params.get("period", "most-improved")
    if period not in {"most-improved", "top", "highest-score"}:
        return error("INVALID_PARAMETER", "The 'period' parameter is not supported.")

    qs = Ward.objects.select_related("city")
    if city_name:
        qs = qs.filter(city__name__iexact=city_name)
    if not qs.exists():
        return error("NOT_FOUND", "No leaderboard data found.", status.HTTP_404_NOT_FOUND)

    # The supplied specification only defines the 'most-improved' response.
    # Use trend as the improvement metric for that period; top/highest-score use score.
    if period == "most-improved":
        qs = qs.order_by("-trend", "-score", "ward")
    else:
        qs = qs.order_by("-score", "-trend", "ward")

    wards = []
    for ward in qs:
        wards.append({
            "ward": ward.ward,
            "name": ward.name,
            "score": float(ward.score),
            "center": ward.center,
            "status": "excellent" if float(ward.score) >= 8 else "good" if float(ward.score) >= 6 else "needs-improvement",
        })
    return ok({"city": city_name or qs.first().city.name, "period": period, "wards": wards})


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def reality_potential(request):
    try:
        page = int(request.query_params.get("page", 1))
        limit = int(request.query_params.get("limit", 5))
    except ValueError:
        return error("INVALID_PARAMETER", "The 'page' and 'limit' parameters must be integers.")
    if page < 1:
        return error("INVALID_PARAMETER", "The 'page' parameter must be at least 1.")
    if limit < 1 or limit > 20:
        return error("INVALID_PARAMETER", "The 'limit' parameter must be between 1 and 20.")

    try:
        shortlisted = parse_bool(request.query_params.get("shortlisted"), "shortlisted")
        from_date = parse_iso_datetime(request.query_params.get("from_date"), "from_date")
        to_date = parse_iso_datetime(request.query_params.get("to_date"), "to_date", end_of_day=True)
    except ValueError as exc:
        return error("INVALID_PARAMETER", str(exc))

    qs = Spot.objects.filter(kind="anchor").select_related("ward__city").prefetch_related("tags", "images")
    if shortlisted is not None:
        qs = qs.filter(shortlisted=shortlisted)
    spot_name = request.query_params.get("spot_name")
    if spot_name:
        qs = qs.filter(spot_name__icontains=spot_name)
    tags = [t.strip().lower() for t in request.query_params.get("tags", "").split(",") if t.strip()]
    if tags:
        qs = qs.filter(tags__name__in=tags).distinct()
    if from_date:
        qs = qs.filter(created_at__gte=from_date)
    if to_date:
        qs = qs.filter(created_at__lte=to_date)

    total = qs.count()
    start = (page - 1) * limit
    spots = qs.order_by("-created_at", "id")[start:start + limit]
    return ok({
        "page": page,
        "limit": limit,
        "total_records": total,
        "spots": RealityPotentialSerializer(spots, many=True).data,
    })


@api_view(["GET"])
@authentication_classes(AUTH)
@permission_classes([HasAPIKey])
@throttle_classes(THROTTLES)
def summary(request):
    try:
        from_date = parse_iso_datetime(request.query_params.get("from_date"), "from_date")
        to_date = parse_iso_datetime(request.query_params.get("to_date"), "to_date", end_of_day=True)
    except ValueError as exc:
        return error("INVALID_PARAMETER", str(exc))

    spot_qs = Spot.objects.all()
    engagement_qs = Engagement.objects.all()
    region = request.query_params.get("region")
    if region:
        spot_qs = spot_qs.filter(ward__city__name__iexact=region)
        engagement_qs = engagement_qs.filter(city__name__iexact=region)
    if from_date:
        spot_qs = spot_qs.filter(created_at__gte=from_date)
        engagement_qs = engagement_qs.filter(occurred_at__gte=from_date)
    if to_date:
        spot_qs = spot_qs.filter(created_at__lte=to_date)
        engagement_qs = engagement_qs.filter(occurred_at__lte=to_date)

    return ok({
        "total_spots_mapped": spot_qs.count(),
        "citizen_stakeholders_engaged": engagement_qs.count(),
        "spots_transformed": spot_qs.filter(transformation_at__isnull=False).count(),
    })
