"""
Real-visitor tracking.  POST /api/visit/  (sent by static/js/visit.js)

The script only reports after a human signal (scroll, tap, click, key) and counts
time only while the tab is visible. The server also drops known bot user agents,
caps time per page at 30 minutes and rate-limits each IP.
"""
import json
import re

from django.core.cache import cache
from django.http import HttpResponse
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

from .models import ClickEvent, VisitorLog

BOT_RE = re.compile(r'bot|crawl|spider|slurp|preview|fetch|scan|monitor|headless|phantom|selenium|puppeteer|'
                    r'playwright|lighthouse|pagespeed|gtmetrix|curl|wget|python|httpclient|java/|go-http|axios|'
                    r'node-fetch|facebookexternalhit|whatsapp|telegram|discord|skype|embedly|quora link|'
                    r'pinterest|vkshare|w3c_validator|datanyze|semrush|ahrefs|mj12|dotbot|bytespider|petal', re.I)
ID_RE = re.compile(r'^[a-f0-9]{16,32}$')
SKIP_PREFIXES = ('/admin', '/static', '/media', '/api/', '/account/password')
MAX_SECONDS = 30 * 60
MAX_CLICKS_PER_PING = 30
MAX_CLICKS_PER_VIEW = 300
CLICK_KINDS = {k for k, _ in ClickEvent.KINDS}


def is_bot(user_agent):
    ua = user_agent or ''
    return len(ua) < 20 or bool(BOT_RE.search(ua))


def _ip(request):
    fwd = request.META.get('HTTP_X_FORWARDED_FOR')
    return (fwd.split(',')[0].strip() if fwd else request.META.get('REMOTE_ADDR')) or None


def _device(ua):
    ua = ua.lower()
    if 'ipad' in ua or 'tablet' in ua or ('android' in ua and 'mobile' not in ua):
        return 'tablet'
    return 'mobile' if 'mobi' in ua or 'iphone' in ua else 'desktop'


def _limited(ip):
    key = f'visit-rate:{ip}'
    n = cache.get(key, 0)
    if n >= 400:  # pings per IP per hour
        return True
    cache.set(key, n + 1, 3600)
    return False


@csrf_exempt  # sendBeacon cannot send the CSRF token; it only writes analytics rows
@require_POST
def track(request):
    ok = HttpResponse(status=204)
    ua = request.META.get('HTTP_USER_AGENT', '')
    if is_bot(ua) or request.headers.get('Sec-Purpose', '').startswith('prefetch'):
        return ok
    try:
        data = json.loads(request.body.decode('utf-8')[:16000])
        view_id, visitor_id = str(data['v']), str(data['u'])
        path = str(data.get('p', ''))[:255]
        seconds = max(0, min(int(data.get('s', 0)), MAX_SECONDS))
        scroll = max(0, min(int(data.get('d', 0)), 100))
        title = str(data.get('t', ''))[:200]
        clicks = data.get('c') or []
        if not isinstance(clicks, list):
            clicks = []
    except (ValueError, KeyError, TypeError, UnicodeDecodeError):
        return HttpResponse(status=400)
    if not (ID_RE.match(view_id) and ID_RE.match(visitor_id)) or not path.startswith('/') or path.startswith(SKIP_PREFIXES):
        return HttpResponse(status=400)
    ip = _ip(request)
    if _limited(ip):
        return ok
    now = timezone.now()
    log = VisitorLog.objects.filter(view_id=view_id).first()
    if log is None:
        ref = str(data.get('r', ''))[:255]
        if ref.startswith(request.build_absolute_uri('/')):
            ref = ''  # internal navigation
        utm = data.get('m') if isinstance(data.get('m'), dict) else {}
        log = VisitorLog.objects.create(
            view_id=view_id, visitor_id=visitor_id, is_human=True, ip_address=ip, page_visited=path,
            referrer=ref, device=_device(ua), visit_time=now, last_seen=now, time_spent_seconds=min(seconds, 60),
            user=request.user if request.user.is_authenticated else None, page_title=title, scroll_pct=scroll,
            utm_source=str(utm.get('source', ''))[:60], utm_medium=str(utm.get('medium', ''))[:60],
            utm_campaign=str(utm.get('campaign', ''))[:100])
        _save_clicks(log, clicks, now)
        return ok
    if log.visitor_id != visitor_id:
        return HttpResponse(status=400)
    # Time can only grow, and never faster than the clock (plus a little slack).
    allowed = int((now - log.visit_time).total_seconds()) + 15
    new = min(max(log.time_spent_seconds, seconds), allowed, MAX_SECONDS)
    changes = {'time_spent_seconds': new, 'last_seen': now, 'scroll_pct': max(log.scroll_pct, scroll)}
    if title and not log.page_title:
        changes['page_title'] = title
    VisitorLog.objects.filter(pk=log.pk).update(**changes)
    _save_clicks(log, clicks, now)
    return ok


def _num(value):
    try:
        return max(0, min(int(value), 1000))
    except (TypeError, ValueError):
        return 0


def _save_clicks(log, clicks, now):
    """Store a batch of clicks sent with a ping. Each click: label, href, kind, section, x, y."""
    clicks = [c for c in clicks[:MAX_CLICKS_PER_PING] if isinstance(c, dict)]
    if not clicks:
        return
    room = MAX_CLICKS_PER_VIEW - log.clicks.count()
    rows = []
    for c in clicks[:max(room, 0)]:
        kind = c.get('k') if c.get('k') in CLICK_KINDS else 'other'
        rows.append(ClickEvent(
            view=log, page=log.page_visited, label=' '.join(str(c.get('l', '')).split())[:120],
            href=str(c.get('h', ''))[:300], kind=kind, section=str(c.get('s', ''))[:60],
            x=_num(c.get('x')), y=_num(c.get('y')), created_at=now))
    ClickEvent.objects.bulk_create(rows)
