import hashlib
import hmac
import ipaddress
import logging
import time
from dataclasses import dataclass
from datetime import timedelta

from django.conf import settings
from django.core import signing
from django.db import IntegrityError, transaction
from django.utils import timezone


logger = logging.getLogger("home.security")


@dataclass(frozen=True)
class RateLimitResult:
    allowed: bool
    retry_after: int = 0


def client_ip(request):
    candidates = []
    if settings.TRUST_CLOUDFLARE_IP_HEADER:
        candidates.append(request.META.get("HTTP_CF_CONNECTING_IP", ""))
    candidates.append(request.META.get("REMOTE_ADDR", ""))
    for candidate in candidates:
        try:
            return str(ipaddress.ip_address(candidate.strip()))
        except (AttributeError, ValueError):
            continue
    return "unknown"


def ip_is_allowed(ip, allowed_networks):
    if not allowed_networks:
        return True
    try:
        address = ipaddress.ip_address(ip)
    except ValueError:
        return False
    for value in allowed_networks:
        try:
            network = ipaddress.ip_network(value, strict=False)
        except ValueError:
            logger.error("Neveljaven ADMIN_ALLOWED_IPS vnos je bil prezrt")
            continue
        if address in network:
            return True
    return False


def rate_fingerprint(*parts):
    payload = "\x1f".join(str(part).strip().lower() for part in parts).encode("utf-8", "ignore")
    return hmac.new(settings.SECRET_KEY.encode("utf-8"), payload, hashlib.sha256).hexdigest()


def _get_locked_rate(scope, fingerprint, now):
    from .models import SecurityRateLimit

    try:
        return SecurityRateLimit.objects.select_for_update().get(scope=scope, fingerprint=fingerprint)
    except SecurityRateLimit.DoesNotExist:
        try:
            # Ločena shranjevalna točka omogoči varen ponovni SELECT, če dva
            # procesa prvi zapis ustvarita istočasno.
            with transaction.atomic():
                return SecurityRateLimit.objects.create(
                    scope=scope,
                    fingerprint=fingerprint,
                    window_started_at=now,
                )
        except IntegrityError:
            return SecurityRateLimit.objects.select_for_update().get(scope=scope, fingerprint=fingerprint)


def rate_limit_status(scope, fingerprint, window_seconds):
    now = timezone.now()
    with transaction.atomic():
        record = _get_locked_rate(scope, fingerprint, now)
        if record.blocked_until and record.blocked_until > now:
            return RateLimitResult(False, max(1, int((record.blocked_until - now).total_seconds())))
        if (now - record.window_started_at).total_seconds() >= window_seconds:
            record.window_started_at = now
            record.count = 0
            record.blocked_until = None
            record.save(update_fields=["window_started_at", "count", "blocked_until", "updated_at"])
        return RateLimitResult(True)


def consume_rate_limit(scope, fingerprint, limit, window_seconds, block_seconds):
    now = timezone.now()
    with transaction.atomic():
        record = _get_locked_rate(scope, fingerprint, now)
        if record.blocked_until and record.blocked_until > now:
            return RateLimitResult(False, max(1, int((record.blocked_until - now).total_seconds())))
        if (now - record.window_started_at).total_seconds() >= window_seconds:
            record.window_started_at = now
            record.count = 0
            record.blocked_until = None
        record.count += 1
        if record.count > limit:
            record.blocked_until = now + timedelta(seconds=block_seconds)
            allowed = False
            retry_after = block_seconds
        else:
            allowed = True
            retry_after = 0
        record.save(update_fields=["window_started_at", "count", "blocked_until", "updated_at"])
        return RateLimitResult(allowed, retry_after)


def clear_rate_limit(scope, fingerprint):
    from .models import SecurityRateLimit

    SecurityRateLimit.objects.filter(scope=scope, fingerprint=fingerprint).delete()


def make_contact_form_token():
    return signing.dumps(
        {"purpose": "contact", "issued": time.time()},
        salt="home.contact-form",
        compress=True,
    )


def validate_contact_form_token(token):
    try:
        payload = signing.loads(
            token,
            salt="home.contact-form",
            max_age=settings.CONTACT_FORM_MAX_SECONDS,
        )
        issued = float(payload["issued"])
    except (signing.BadSignature, signing.SignatureExpired, KeyError, TypeError, ValueError):
        return False
    age = time.time() - issued
    return settings.CONTACT_FORM_MIN_SECONDS <= age <= settings.CONTACT_FORM_MAX_SECONDS
