# retoor <retoor@molodetz.nl>

from __future__ import annotations

import asyncio
import hashlib
import logging
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from typing import Optional
from urllib.parse import urlparse

from devplacepy.config import SECRET_KEY
from devplacepy.database import db, get_setting
from devplacepy.utils import client_ip, get_current_user

from .common import RETENTION_DAYS, hour_bucket, now_utc

logger = logging.getLogger(__name__)

EXCLUDED_PREFIXES = (
    "/static",
    "/avatar",
    "/openai",
    "/devii/ws",
    "/xmlrpc",
    "/dbapi",
    "/admin/statistics/data",
    "/admin/ai-usage/data",
    "/admin/services/data",
)

FLUSH_SECONDS = 30

_view_buffer: dict[tuple[str, str, str], dict[str, int]] = defaultdict(
    lambda: {"views": 0, "member_views": 0, "guest_views": 0}
)
_unique_buffer: list[tuple[str, str, str, Optional[str]]] = []
_cached_daily_salt = ""
_flush_task: Optional[asyncio.Task] = None


def _tracking_enabled() -> bool:
    return get_setting("statistics_tracking_enabled", "1") == "1"


def _daily_salt() -> str:
    global _cached_daily_salt
    day = now_utc().date().isoformat()
    if not _cached_daily_salt or not _cached_daily_salt.startswith(day):
        _cached_daily_salt = f"{day}:{SECRET_KEY}"
    return _cached_daily_salt


def _visitor_hash(request) -> str:
    ip = client_ip(request, default="") or ""
    ua = (request.headers.get("user-agent") or "")[:120]
    raw = f"{_daily_salt()}:{ip}:{ua}"
    return hashlib.sha256(raw.encode("utf-8")).hexdigest()


def _page_group(request) -> str:
    route = request.scope.get("route")
    path = getattr(route, "path", None)
    if path:
        return path
    segments = [segment for segment in request.url.path.split("/") if segment]
    if not segments:
        return "/"
    return f"/{segments[0]}"


def _referrer_group(request) -> str:
    raw = request.headers.get("referer") or request.headers.get("referrer") or ""
    if not raw:
        return "direct"
    try:
        host = (urlparse(raw).hostname or "").lower()
    except ValueError:
        return "other"
    if not host:
        return "direct"
    request_host = (request.url.hostname or "").lower()
    if host == request_host:
        return "internal"
    return host[:120]


def should_track(request, status_code: int) -> bool:
    if not _tracking_enabled():
        return False
    if request.method != "GET":
        return False
    if status_code >= 400:
        return False
    path = request.url.path
    for prefix in EXCLUDED_PREFIXES:
        if path.startswith(prefix):
            return False
    accept = (request.headers.get("accept") or "").lower()
    if "application/json" in accept and "text/html" not in accept:
        return False
    return True


def track_visit(request, status_code: int) -> None:
    if not should_track(request, status_code):
        return
    bucket = hour_bucket(now_utc())
    page_group = _page_group(request)
    referrer_group = _referrer_group(request)
    key = (bucket, page_group, referrer_group)
    slot = _view_buffer[key]
    slot["views"] += 1
    user = get_current_user(request)
    if user:
        slot["member_views"] += 1
        member_uid = user["uid"]
    else:
        slot["guest_views"] += 1
        member_uid = None
    visitor_hash = _visitor_hash(request)
    _unique_buffer.append((bucket, visitor_hash, page_group, member_uid))


def _flush_views() -> None:
    if "visit_stats_hourly" not in db.tables:
        return
    if not _view_buffer:
        return
    pending = dict(_view_buffer)
    _view_buffer.clear()
    with db:
        for (bucket, page_group, referrer_group), counts in pending.items():
            db.query(
                "INSERT INTO visit_stats_hourly "
                "(bucket_start, page_group, referrer_group, views, member_views, guest_views) "
                "VALUES (:bucket, :page, :ref, :views, :members, :guests) "
                "ON CONFLICT(bucket_start, page_group, referrer_group) DO UPDATE SET "
                "views = views + excluded.views, "
                "member_views = member_views + excluded.member_views, "
                "guest_views = guest_views + excluded.guest_views",
                bucket=bucket,
                page=page_group,
                ref=referrer_group,
                views=counts["views"],
                members=counts["member_views"],
                guests=counts["guest_views"],
            )


def _flush_uniques() -> None:
    if "visit_unique_slots" not in db.tables:
        return
    if not _unique_buffer:
        return
    pending = list(_unique_buffer)
    _unique_buffer.clear()
    with db:
        for bucket, visitor_hash, page_group, member_uid in pending:
            db.query(
                "INSERT OR IGNORE INTO visit_unique_slots "
                "(bucket_start, visitor_hash, page_group, user_uid) "
                "VALUES (:bucket, :hash, :page, :uid)",
                bucket=bucket,
                hash=visitor_hash,
                page=page_group,
                uid=member_uid,
            )


def _prune_old() -> None:
    cutoff = (now_utc() - timedelta(days=RETENTION_DAYS)).isoformat(timespec="seconds")
    with db:
        if "visit_stats_hourly" in db.tables:
            db.query("DELETE FROM visit_stats_hourly WHERE bucket_start < :cutoff", cutoff=cutoff)
        if "visit_unique_slots" in db.tables:
            db.query("DELETE FROM visit_unique_slots WHERE bucket_start < :cutoff", cutoff=cutoff)


def flush_visits(*, prune: bool = False) -> None:
    try:
        _flush_views()
        _flush_uniques()
        if prune:
            _prune_old()
    except Exception:
        logger.exception("visit statistics flush failed")


async def _flush_loop() -> None:
    ticks = 0
    while True:
        await asyncio.sleep(FLUSH_SECONDS)
        ticks += 1
        flush_visits(prune=ticks % 120 == 0)


def start_visit_flusher() -> None:
    global _flush_task
    if _flush_task is not None:
        return
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        return
    _flush_task = loop.create_task(_flush_loop())