package main

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"math"
	mathrand "math/rand/v2"
	"net/http"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/jackc/pgx/v5"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
	"go.opentelemetry.io/otel/trace"
)

type api struct {
	store      store
	metrics    *metrics
	logger     *slog.Logger
	version    string
	chaosRate  float64
	migrated   *atomic.Bool
	prometheus prometheus.Gatherer
}

func (a *api) routes() http.Handler {
	r := chi.NewRouter()
	r.Use(a.versionHeader)
	r.Use(a.observeRequest)
	r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
		writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
	})
	r.Get("/readyz", a.ready)
	r.Handle("/metrics", promhttp.HandlerFor(a.prometheus, promhttp.HandlerOpts{}))
	r.Route("/api/v1", func(r chi.Router) {
		r.Use(a.chaos)
		r.Get("/buoys", a.listBuoys)
		r.Post("/buoys", a.createBuoy)
		r.Get("/buoys/{id}", a.getBuoy)
		r.Delete("/buoys/{id}", a.deleteBuoy)
		r.Get("/readings", a.listReadings)
		r.Post("/readings", a.createReading)
		r.Get("/readings/aggregate", a.aggregateReadings)
	})
	return otelhttp.NewHandler(r, "http.request")
}

func (a *api) versionHeader(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Nereus-Version", a.version)
		next.ServeHTTP(w, r)
	})
}

type responseRecorder struct {
	http.ResponseWriter
	status int
}

func (w *responseRecorder) WriteHeader(status int) {
	w.status = status
	w.ResponseWriter.WriteHeader(status)
}

func (a *api) observeRequest(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		started := time.Now()
		recorder := &responseRecorder{ResponseWriter: w, status: http.StatusOK}
		next.ServeHTTP(recorder, r)
		path := chi.RouteContext(r.Context()).RoutePattern()
		if path == "" {
			path = "unmatched"
		}
		status := strconv.Itoa(recorder.status)
		a.metrics.requests.WithLabelValues(r.Method, path, status, a.version).Inc()
		a.metrics.duration.WithLabelValues(r.Method, path, a.version).Observe(time.Since(started).Seconds())
		span := trace.SpanFromContext(r.Context()).SpanContext()
		a.logger.InfoContext(r.Context(), "request", "method", r.Method, "path", path, "status", recorder.status, "duration_ms", time.Since(started).Milliseconds(), "trace_id", span.TraceID().String())
	})
}

func (a *api) chaos(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if a.chaosRate > 0 && mathrand.Float64() < a.chaosRate {
			writeError(w, http.StatusInternalServerError, "injected failure")
			return
		}
		next.ServeHTTP(w, r)
	})
}

func (a *api) ready(w http.ResponseWriter, r *http.Request) {
	ctx, cancel := contextWithTimeout(r, 2*time.Second)
	defer cancel()
	if !a.migrated.Load() || a.store.Ping(ctx) != nil {
		writeError(w, http.StatusServiceUnavailable, "database unavailable")
		return
	}
	writeJSON(w, http.StatusOK, map[string]string{"status": "ready"})
}

func (a *api) listBuoys(w http.ResponseWriter, r *http.Request) {
	items, err := a.store.ListBuoys(r.Context())
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	if items == nil {
		items = []buoy{}
	}
	a.metrics.active.Set(float64(len(items)))
	writeJSON(w, http.StatusOK, items)
}

func (a *api) createBuoy(w http.ResponseWriter, r *http.Request) {
	var input buoy
	if !decodeJSON(w, r, &input) {
		return
	}
	input.Name = strings.TrimSpace(input.Name)
	if input.Name == "" || len(input.Name) > 120 || input.Latitude < -90 || input.Latitude > 90 || input.Longitude < -180 || input.Longitude > 180 {
		writeError(w, http.StatusBadRequest, "invalid buoy")
		return
	}
	id, err := newUUID()
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	input.ID = id
	created, err := a.store.CreateBuoy(r.Context(), input)
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	a.metrics.active.Inc()
	writeJSON(w, http.StatusCreated, created)
}

func (a *api) getBuoy(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if !validUUID(id) {
		writeError(w, http.StatusBadRequest, "invalid buoy ID")
		return
	}
	item, err := a.store.GetBuoy(r.Context(), id)
	if errors.Is(err, pgx.ErrNoRows) {
		writeError(w, http.StatusNotFound, "buoy not found")
		return
	}
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	writeJSON(w, http.StatusOK, item)
}

func (a *api) deleteBuoy(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	if !validUUID(id) {
		writeError(w, http.StatusBadRequest, "invalid buoy ID")
		return
	}
	err := a.store.DeleteBuoy(r.Context(), id)
	if errors.Is(err, pgx.ErrNoRows) {
		writeError(w, http.StatusNotFound, "buoy not found")
		return
	}
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	a.metrics.active.Dec()
	w.WriteHeader(http.StatusNoContent)
}

func (a *api) listReadings(w http.ResponseWriter, r *http.Request) {
	query := r.URL.Query()
	from, ok := optionalTime(w, query.Get("from"))
	if !ok {
		return
	}
	to, ok := optionalTime(w, query.Get("to"))
	if !ok {
		return
	}
	if from != nil && to != nil && from.After(*to) {
		writeError(w, http.StatusBadRequest, "from must not be after to")
		return
	}
	if id := query.Get("buoy_id"); id != "" && !validUUID(id) {
		writeError(w, http.StatusBadRequest, "invalid buoy ID")
		return
	}
	limit, ok := boundedInt(query.Get("limit"), 100, 1, 1000)
	if !ok {
		writeError(w, http.StatusBadRequest, "invalid limit")
		return
	}
	offset, ok := boundedInt(query.Get("offset"), 0, 0, math.MaxInt)
	if !ok {
		writeError(w, http.StatusBadRequest, "invalid offset")
		return
	}
	items, err := a.store.ListReadings(r.Context(), query.Get("buoy_id"), from, to, limit, offset)
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	if items == nil {
		items = []reading{}
	}
	writeJSON(w, http.StatusOK, items)
}

func (a *api) createReading(w http.ResponseWriter, r *http.Request) {
	var input reading
	if !decodeJSON(w, r, &input) {
		return
	}
	if !validUUID(input.BuoyID) || input.WaveHeight < 0 || input.Salinity != nil && *input.Salinity < 0 {
		writeError(w, http.StatusBadRequest, "invalid reading")
		return
	}
	id, err := newUUID()
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	input.ID = id
	created, err := a.store.CreateReading(r.Context(), input)
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	a.metrics.ingested.Inc()
	writeJSON(w, http.StatusCreated, created)
}

func (a *api) aggregateReadings(w http.ResponseWriter, r *http.Request) {
	window, err := time.ParseDuration(r.URL.Query().Get("window"))
	if err != nil || window < time.Minute || window > 24*time.Hour {
		writeError(w, http.StatusBadRequest, "window must be between 1m and 24h")
		return
	}
	items, err := a.store.AggregateReadings(r.Context(), window)
	if err != nil {
		a.internalError(w, r, err)
		return
	}
	if items == nil {
		items = []aggregate{}
	}
	writeJSON(w, http.StatusOK, items)
}

func (a *api) internalError(w http.ResponseWriter, r *http.Request, err error) {
	a.logger.ErrorContext(r.Context(), "request failed", "error", err)
	writeError(w, http.StatusInternalServerError, "internal error")
}
func writeError(w http.ResponseWriter, status int, message string) {
	writeJSON(w, status, map[string]string{"error": message})
}
func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}

func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(dst); err != nil {
		writeError(w, http.StatusBadRequest, "invalid JSON")
		return false
	}
	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		writeError(w, http.StatusBadRequest, "request body must contain one JSON object")
		return false
	}
	return true
}

func optionalTime(w http.ResponseWriter, value string) (*time.Time, bool) {
	if value == "" {
		return nil, true
	}
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		writeError(w, http.StatusBadRequest, "invalid timestamp")
		return nil, false
	}
	return &parsed, true
}
func boundedInt(value string, fallback, min, max int) (int, bool) {
	if value == "" {
		return fallback, true
	}
	parsed, err := strconv.Atoi(value)
	return parsed, err == nil && parsed >= min && parsed <= max
}

func validUUID(value string) bool {
	if len(value) != 36 || value[8] != '-' || value[13] != '-' || value[18] != '-' || value[23] != '-' {
		return false
	}
	compact := strings.ReplaceAll(value, "-", "")
	_, err := hex.DecodeString(compact)
	return err == nil
}

func newUUID() (string, error) {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", fmt.Errorf("generate UUID: %w", err)
	}
	b[6] = (b[6] & 0x0f) | 0x40
	b[8] = (b[8] & 0x3f) | 0x80
	encoded := make([]byte, 36)
	hex.Encode(encoded[0:8], b[0:4])
	encoded[8] = '-'
	hex.Encode(encoded[9:13], b[4:6])
	encoded[13] = '-'
	hex.Encode(encoded[14:18], b[6:8])
	encoded[18] = '-'
	hex.Encode(encoded[19:23], b[8:10])
	encoded[23] = '-'
	hex.Encode(encoded[24:36], b[10:16])
	return string(encoded), nil
}
