import hmac
import json
import re
from datetime import timedelta

from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import IntegrityError
from django.db.models import Count, Max, Q
from django.http import JsonResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_GET, require_POST

from .models import CouponVote, StoreComment

SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,254}$")
HASH_RE = re.compile(r"^[a-f0-9]{16,64}$")
LINK_RE = re.compile(r"(https?://|www\.|\.(com|net|org|ru|xyz|info|top)\b)", re.I)

MAX_COMMENTS_PER_HOUR = 5
MAX_VOTES_PER_HOUR = 60


def _secret_ok(request):
    """POSTs only come from the Next.js server, which sends a shared secret."""
    secret = getattr(settings, "COMMUNITY_API_SECRET", "") or ""
    sent = request.headers.get("X-Community-Secret", "")
    return bool(secret) and hmac.compare_digest(secret, sent)


def _json_body(request):
    try:
        return json.loads(request.body.decode("utf-8") or "{}")
    except (ValueError, UnicodeDecodeError):
        return None


def _vote_summary(coupon_ids):
    rows = (
        CouponVote.objects.filter(coupon_id__in=coupon_ids)
        .values("coupon_id")
        .annotate(
            yes=Count("id", filter=Q(worked=True)),
            no=Count("id", filter=Q(worked=False)),
            last_worked=Max("updated_at", filter=Q(worked=True)),
        )
    )
    return {
        str(r["coupon_id"]): {
            "yes": r["yes"],
            "no": r["no"],
            "last_worked": r["last_worked"].isoformat() if r["last_worked"] else None,
        }
        for r in rows
    }


@require_GET
def comments_for_store(request, slug):
    if not SLUG_RE.match(slug):
        return JsonResponse({"error": "bad slug"}, status=400)
    qs = StoreComment.objects.filter(store_slug=slug, approved=True).order_by("-created_at")[:50]
    data = [
        {
            "id": c.id,
            "name": c.name,
            "body": c.body,
            "reply": c.reply,
            "created_at": c.created_at.isoformat(),
        }
        for c in qs
    ]
    resp = JsonResponse({"comments": data})
    resp["Cache-Control"] = "public, max-age=60"
    return resp


@csrf_exempt
@require_POST
def create_comment(request):
    if not _secret_ok(request):
        return JsonResponse({"error": "forbidden"}, status=403)
    data = _json_body(request)
    if data is None:
        return JsonResponse({"error": "bad json"}, status=400)

    slug = str(data.get("store_slug", "")).strip().lower()
    name = str(data.get("name", "")).strip()
    email = str(data.get("email", "")).strip()
    body = str(data.get("body", "")).strip()
    ip_hash = str(data.get("ip_hash", "")).strip().lower()

    errors = {}
    if not SLUG_RE.match(slug):
        errors["store_slug"] = "Unknown store."
    if not 2 <= len(name) <= 60:
        errors["name"] = "Please enter your name (2–60 characters)."
    if not 10 <= len(body) <= 1000:
        errors["body"] = "Comments need 10–1000 characters."
    elif LINK_RE.search(body):
        errors["body"] = "Please leave out links."
    if email:
        try:
            validate_email(email)
        except ValidationError:
            errors["email"] = "That email doesn't look right."
    if not HASH_RE.match(ip_hash):
        errors["ip_hash"] = "missing"
    if errors:
        return JsonResponse({"errors": errors}, status=400)

    hour_ago = timezone.now() - timedelta(hours=1)
    if StoreComment.objects.filter(ip_hash=ip_hash, created_at__gte=hour_ago).count() >= MAX_COMMENTS_PER_HOUR:
        return JsonResponse({"error": "Too many comments. Please try again later."}, status=429)

    StoreComment.objects.create(
        store_slug=slug, name=name, email=email, body=body, ip_hash=ip_hash, approved=False
    )
    return JsonResponse({"ok": True, "pending": True}, status=201)


@require_GET
def votes_for_store(request, slug):
    if not SLUG_RE.match(slug):
        return JsonResponse({"error": "bad slug"}, status=400)
    ids = list(
        CouponVote.objects.filter(store_slug=slug).values_list("coupon_id", flat=True).distinct()
    )
    resp = JsonResponse({"votes": _vote_summary(ids)})
    resp["Cache-Control"] = "public, max-age=30"
    return resp


@csrf_exempt
@require_POST
def cast_vote(request):
    if not _secret_ok(request):
        return JsonResponse({"error": "forbidden"}, status=403)
    data = _json_body(request)
    if data is None:
        return JsonResponse({"error": "bad json"}, status=400)

    slug = str(data.get("store_slug", "")).strip().lower()
    voter_hash = str(data.get("voter_hash", "")).strip().lower()
    worked = data.get("worked")
    try:
        coupon_id = int(data.get("coupon_id"))
    except (TypeError, ValueError):
        coupon_id = 0

    if not SLUG_RE.match(slug) or coupon_id <= 0 or not isinstance(worked, bool) or not HASH_RE.match(voter_hash):
        return JsonResponse({"error": "bad vote"}, status=400)

    hour_ago = timezone.now() - timedelta(hours=1)
    if CouponVote.objects.filter(voter_hash=voter_hash, updated_at__gte=hour_ago).count() >= MAX_VOTES_PER_HOUR:
        return JsonResponse({"error": "Too many votes. Please try again later."}, status=429)

    try:
        CouponVote.objects.update_or_create(
            coupon_id=coupon_id,
            voter_hash=voter_hash,
            defaults={"worked": worked, "store_slug": slug},
        )
    except IntegrityError:
        pass

    summary = _vote_summary([coupon_id]).get(str(coupon_id), {"yes": 0, "no": 0, "last_worked": None})
    return JsonResponse({"ok": True, "coupon_id": coupon_id, **summary})
