import json
import time
import logging

from pyramid.view import view_config
from pyramid.response import Response

import sqlalchemy as sa

from ..models.meta import id_to_uuid
from ..models.page_session import PageSession

log = logging.getLogger(__name__)

# In-memory rate limit: {(session_token, product_id_str): timestamp}
_recent_beacons = {}
_RATE_LIMIT_SECONDS = 60
_RATE_LIMIT_MAX_ENTRIES = 1000

# Referrer classification
_SEARCH_DOMAINS = {
    "google", "bing", "yahoo", "duckduckgo", "baidu", "yandex",
    "ecosia", "qwant", "startpage", "brave",
}
_SOCIAL_DOMAINS = {
    "facebook", "twitter", "reddit", "linkedin", "instagram",
    "tiktok", "youtube", "pinterest", "mastodon", "threads",
    "x",
}


# Search engine query parameter names by domain root
_SEARCH_QUERY_PARAMS = {
    "google": "q", "bing": "q", "yahoo": "p", "duckduckgo": "q",
    "baidu": "wd", "yandex": "text", "ecosia": "q", "qwant": "q",
    "startpage": "query", "brave": "q",
}


def classify_referrer(referrer, request_host):
    """Classify a Referer header into (class, domain, query).

    class:  0=direct, 1=search, 2=social, 3=internal, 4=unknown
    domain: hostname string or None
    query:  search engine query string or None
    """
    if not referrer:
        return 0, None, None
    try:
        from urllib.parse import urlparse, parse_qs
        parsed = urlparse(referrer)
        host = (parsed.hostname or "").lower()
    except Exception:
        return 4, None, None

    domain = host or None

    # Internal: same host as request
    if request_host and host == request_host.lower():
        return 3, domain, None

    # Strip www. and extract root domain for classification
    clean = host
    if clean.startswith("www."):
        clean = clean[4:]
    parts = clean.split(".")
    root = parts[-2] if len(parts) >= 2 else clean

    if root in _SEARCH_DOMAINS:
        query = None
        param = _SEARCH_QUERY_PARAMS.get(root, "q")
        try:
            qs = parse_qs(parsed.query)
            vals = qs.get(param, [])
            if vals:
                query = vals[0][:256]
        except Exception:
            pass
        return 1, domain, query

    if root in _SOCIAL_DOMAINS:
        return 2, domain, None

    return 4, domain, None


def classify_device(viewport_width):
    """Classify viewport width into device class.

    0 = mobile (<=768)
    1 = tablet (<=1024)
    2 = desktop (>1024)
    """
    if viewport_width is None:
        return None
    try:
        w = int(viewport_width)
    except (TypeError, ValueError):
        return None
    if w <= 768:
        return 0
    if w <= 1024:
        return 1
    return 2


def _clamp(value, lo, hi):
    """Clamp a numeric value to [lo, hi], returning None if not numeric."""
    if value is None:
        return None
    try:
        v = type(lo)(value)
    except (TypeError, ValueError):
        return None
    if v < lo:
        return lo
    if v > hi:
        return hi
    return v


def _clamp_int(value, lo, hi):
    """Clamp to int range."""
    return _clamp(value, lo, hi)


def _clamp_float(value, lo, hi):
    """Clamp to float range."""
    if value is None:
        return None
    try:
        v = float(value)
    except (TypeError, ValueError):
        return None
    if v < lo:
        return lo
    if v > hi:
        return hi
    return v


def _is_rate_limited(session_token, product_id_str):
    """Check and update rate limit. Returns True if should be rate-limited."""
    global _recent_beacons

    if not session_token or not product_id_str:
        return False

    now = time.time()
    key = (session_token, product_id_str)

    # Evict all entries if dict gets too large
    if len(_recent_beacons) > _RATE_LIMIT_MAX_ENTRIES:
        _recent_beacons = {}

    last_seen = _recent_beacons.get(key)
    if last_seen and (now - last_seen) < _RATE_LIMIT_SECONDS:
        return True

    _recent_beacons[key] = now
    return False


@view_config(route_name="signals_beacon", request_method="POST", renderer="json")
def beacon_view(request):
    """Receive anonymous page visit signals.

    Expects JSON body with signal data. Returns 204 on success.
    No authentication required — signals are anonymous by design.
    """
    try:
        body = request.json_body
    except (json.JSONDecodeError, ValueError):
        request.response.status_int = 400
        return {"error": "Invalid JSON"}

    if not isinstance(body, dict):
        request.response.status_int = 400
        return {"error": "Invalid payload"}

    # Required fields
    product_id_str = body.get("product_id")
    shop_id_str = body.get("shop_id")

    if not product_id_str or not shop_id_str:
        request.response.status_int = 400
        return {"error": "Missing product_id or shop_id"}

    product_uuid = id_to_uuid(product_id_str)
    shop_uuid = id_to_uuid(shop_id_str)

    if not product_uuid or not shop_uuid:
        request.response.status_int = 400
        return {"error": "Invalid product_id or shop_id"}

    # Rate limit check
    session_token = body.get("session_token", "")
    if _is_rate_limited(session_token, product_id_str):
        # Accept silently but skip insert
        request.response.status_int = 204
        return Response(status=204)

    # Build PageSession row
    ps = PageSession()
    ps.shop_id = shop_uuid
    ps.product_id = product_uuid

    # Presence
    ps.wall_clock_ms = _clamp_int(body.get("wall_clock_ms"), 0, 86_400_000)
    ps.visible_ms = _clamp_int(body.get("visible_ms"), 0, 86_400_000)
    ps.active_ms = _clamp_int(body.get("active_ms"), 0, 86_400_000)

    # Scroll
    ps.scroll_depth_max = _clamp_float(body.get("scroll_depth_max"), 0.0, 1.0)
    ps.scroll_direction_changes = _clamp_int(
        body.get("scroll_direction_changes"), 0, 10_000
    )

    # Media
    ps.media_duration_ms = _clamp_int(body.get("media_duration_ms"), 0, 86_400_000)
    ps.media_play_ms = _clamp_int(body.get("media_play_ms"), 0, 86_400_000)
    ps.media_play_count = _clamp_int(body.get("media_play_count"), 0, 10_000)
    ps.media_pause_count = _clamp_int(body.get("media_pause_count"), 0, 10_000)
    ps.media_seek_count = _clamp_int(body.get("media_seek_count"), 0, 10_000)
    ps.media_seek_back_count = _clamp_int(body.get("media_seek_back_count"), 0, 10_000)
    ps.media_speed = _clamp_float(body.get("media_speed"), 0.1, 16.0)
    ps.media_completed = bool(body.get("media_completed")) if body.get("media_completed") is not None else None
    ps.media_percent_played = _clamp_float(body.get("media_percent_played"), 0.0, 1.0)

    # Context
    referrer = request.referrer or ""
    request_host = request.host
    if ":" in request_host:
        request_host = request_host.split(":")[0]
    ref_class, ref_domain, ref_query = classify_referrer(referrer, request_host)
    ps.referrer_class = ref_class
    ps.referrer_domain = ref_domain[:128] if ref_domain else None
    ps.referrer_query = ref_query[:256] if ref_query else None
    ps.device_class = classify_device(body.get("viewport_width"))
    ps.is_ring_entry = bool(body.get("is_ring_entry")) if body.get("is_ring_entry") is not None else None
    ps.ring_position = _clamp_int(body.get("ring_position"), -32768, 32767)
    ps.is_owner = bool(body.get("is_owner")) if body.get("is_owner") is not None else None

    request.dbsession.add(ps)

    # Increment view_count if visible >= 7 seconds (atomic SQL UPDATE)
    # Skip increment for shop owner/editor traffic
    visible_ms = ps.visible_ms or 0
    if visible_ms >= 7000 and not ps.is_owner:
        from ..models.product import Product
        request.dbsession.execute(
            sa.update(Product)
            .where(Product.id == product_uuid)
            .values(view_count=Product.view_count + 1)
        )

    request.response.status_int = 204
    return Response(status=204)
