"""
hit-counter — minimal per-slug visit counter, with basic abuse protection
and a Prometheus /metrics endpoint for Grafana.

Throttling (in-memory, fine at this scale):
  1. Per (ip, slug) cooldown — same visitor re-hitting the same article
     within COOLDOWN_SECONDS doesn't increment the count again.
  2. Per-ip global rate limit — caps total requests/minute from one IP.

State resets on container restart — acceptable for this use case.
"""
import time
from collections import defaultdict
from pathlib import Path
import sqlite3

from fastapi import FastAPI, Request, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware
from prometheus_client import Gauge, generate_latest, CONTENT_TYPE_LATEST

DB_PATH = Path("/data/hits.db")
DB_PATH.parent.mkdir(parents=True, exist_ok=True)

COOLDOWN_SECONDS = 30 * 60      # one counted hit per (ip, slug) per 30 min
RATE_LIMIT_WINDOW = 60          # seconds
RATE_LIMIT_MAX_REQUESTS = 20    # requests per ip per window, across all endpoints

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://carloselugo.com"],
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)

# (ip, slug) -> last counted timestamp
_last_hit: dict[tuple[str, str], float] = {}
# ip -> list of recent request timestamps
_request_log: dict[str, list[float]] = defaultdict(list)

# Prometheus gauge, one series per article slug.
# Gauge (not Counter) because we set it directly from the SQLite total
# on every /metrics scrape — Prometheus itself builds the time series
# from repeated scrapes, so we don't need to track history ourselves.
article_hits_gauge = Gauge("article_hits_total", "Total counted views per article", ["slug"])


def get_client_ip(request: Request) -> str:
    # Caddy sets X-Forwarded-For when reverse-proxying.
    forwarded = request.headers.get("x-forwarded-for")
    if forwarded:
        return forwarded.split(",")[0].strip()
    return request.client.host if request.client else "unknown"


def enforce_rate_limit(ip: str):
    now = time.time()
    log = _request_log[ip]
    while log and now - log[0] > RATE_LIMIT_WINDOW:
        log.pop(0)
    if len(log) >= RATE_LIMIT_MAX_REQUESTS:
        raise HTTPException(status_code=429, detail="Too many requests")
    log.append(now)


def get_db():
    conn = sqlite3.connect(DB_PATH)
    conn.execute(
        "CREATE TABLE IF NOT EXISTS hits (slug TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0)"
    )
    return conn


@app.post("/hit/{slug}")
def register_hit(slug: str, request: Request):
    ip = get_client_ip(request)
    enforce_rate_limit(ip)

    now = time.time()
    key = (ip, slug)
    should_count = now - _last_hit.get(key, 0) > COOLDOWN_SECONDS

    conn = get_db()
    if should_count:
        conn.execute(
            "INSERT INTO hits (slug, count) VALUES (?, 1) "
            "ON CONFLICT(slug) DO UPDATE SET count = count + 1",
            (slug,),
        )
        conn.commit()
        _last_hit[key] = now

    total = conn.execute("SELECT count FROM hits WHERE slug = ?", (slug,)).fetchone()
    conn.close()

    count = total[0] if total else 0
    article_hits_gauge.labels(slug=slug).set(count)
    return {"slug": slug, "count": count, "counted": should_count}


@app.get("/hits/{slug}")
def get_hits(slug: str, request: Request):
    enforce_rate_limit(get_client_ip(request))
    conn = get_db()
    row = conn.execute("SELECT count FROM hits WHERE slug = ?", (slug,)).fetchone()
    conn.close()
    return {"slug": slug, "count": row[0] if row else 0}


@app.get("/metrics")
def metrics():
    # Refresh every gauge from the source of truth (SQLite) before
    # Prometheus scrapes, so /metrics is correct even right after a restart.
    conn = get_db()
    rows = conn.execute("SELECT slug, count FROM hits").fetchall()
    conn.close()
    for slug, count in rows:
        article_hits_gauge.labels(slug=slug).set(count)
    return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)