package web

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"time"

	"pete/internal/storage"
)

// track records one page view plus a privacy-preserving unique-visitor token
// for the given coarse label ("home", a channel slug, …). It is deliberately
// cheap and best-effort: the visitor token is derived synchronously (so it sees
// the live request) but the DB writes are fired in the background so page
// rendering never waits on them.
func (s *Server) track(r *http.Request, label string) {
	token := s.visitorToken(r)
	go func() {
		storage.RecordPageView(label)
		if token != "" {
			storage.RecordVisitor(token)
		}
	}()
}

// visitorToken returns an irreversible per-day token for the requester: a
// SHA-256 of (daily salt || client IP || User-Agent), truncated. Because the
// salt rotates each UTC day and is never stored, tokens cannot be reversed back
// to an IP or linked across days. Returns "" if no client IP can be determined.
func (s *Server) visitorToken(r *http.Request) string {
	ip := clientIP(r)
	if ip == "" {
		return ""
	}
	salt := s.dailySalt()
	h := sha256.New()
	h.Write(salt[:])
	h.Write([]byte(ip))
	h.Write([]byte{0})
	h.Write([]byte(r.UserAgent()))
	return hex.EncodeToString(h.Sum(nil)[:16])
}

// dailySalt returns the current day's salt, generating a fresh random one
// whenever the UTC day rolls over. The previous salt is discarded, which is
// what makes cross-day correlation impossible.
func (s *Server) dailySalt() [16]byte {
	day := time.Now().UTC().Unix() / 86400
	s.metricsMu.Lock()
	defer s.metricsMu.Unlock()
	if day != s.saltDay || isZero(s.salt) {
		if _, err := rand.Read(s.salt[:]); err != nil {
			// crypto/rand should never fail; if it does, fall back to a
			// day-derived constant so we still dedup within the day.
			slog.Error("web: salt generation failed; using weak fallback", "err", err)
			for i := range s.salt {
				s.salt[i] = byte(day >> (uint(i%8) * 8))
			}
		}
		s.saltDay = day
	}
	return s.salt
}

func isZero(b [16]byte) bool {
	for _, x := range b {
		if x != 0 {
			return false
		}
	}
	return true
}

// clientIP extracts the requester's IP, honoring the leftmost X-Forwarded-For
// entry when present (Pete runs behind a reverse proxy) and otherwise falling
// back to RemoteAddr. Only used to derive a one-way hash, never stored.
func clientIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		if first := strings.TrimSpace(strings.Split(xff, ",")[0]); first != "" {
			return first
		}
	}
	host, _, err := net.SplitHostPort(r.RemoteAddr)
	if err != nil {
		return r.RemoteAddr
	}
	return host
}
