// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

package handlers

import (
	"net/http"
	"strconv"
	"time"

	"github.com/MeshCore-Beacon/beacon-server/internal/api"
	"github.com/go-chi/chi/v5"
	"github.com/google/uuid"
)

// ObserversRouter mounts all /observers routes onto a subrouter.
//
// GET  /observers                        → listObservers
// GET  /observers/{observerId}           → getObserver
// GET  /observers/{observerId}/telemetry → getObserverTelemetry
// GET  /observers/{observerId}/adverts   → listObserverAdverts
func ObserversRouter(reader api.Reader) http.Handler {
	r := chi.NewRouter()
	r.Get("/", listObservers(reader))
	r.Route("/{observerId}", func(r chi.Router) {
		r.Get("/", getObserver(reader))
		r.Get("/adverts", listObserverAdverts(reader))
		r.Get("/telemetry", getObserverTelemetry(reader))
	})
	return r
}

// listObservers godoc
//
//	@Summary	List observers
//	@Tags		Observers
//	@Produce	json
//	@Param		iata			query		string	false	"Filter by single IATA code (case-insensitive)"
//	@Param		iatas			query		string	false	"Filter by multiple IATA codes, comma-separated e.g. YVR,YYJ"
//	@Param		regionId		query		int		false	"Filter by region ID, expands to member IATAs"
//	@Param		region			query		string	false	"Filter by region slug, expands to member IATAs"
//	@Param		type	query		string	false	"Filter by observer type (e.g. meshcoretomqtt, meshcore-ha)"
//	@Param		broker	query		string	false	"Filter by broker name"
//	@Param		status	query		string	false	"Filter by status (online or offline)"
//	@Param		name	query		string	false	"Partial case-insensitive display name match"
//	@Param		scope	query		string	false	"Filter by transport scope name e.g. %23bc (URL-encoded #bc)"
//	@Param		cursor	query		int		false	"last_seen epoch ms of last item for pagination"
//	@Param		limit	query		int		false	"Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200)
//	@Success	200		{object}	api.Page[api.ObserverSummary]
//	@Failure	400		{object}	handlers.APIError
//	@Failure	500		{object}	handlers.APIError
//	@Router		/observers [get]
func listObservers(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		observerType := r.URL.Query().Get("type")
		broker := r.URL.Query().Get("broker")
		name := r.URL.Query().Get("name")
		status := r.URL.Query().Get("status")
		scope := r.URL.Query().Get("scope")
		var cursor int64
		if cursorParam := r.URL.Query().Get("cursor"); cursorParam != "" {
			c, err := strconv.ParseInt(cursorParam, 10, 64)
			if err != nil {
				respondError(w, http.StatusBadRequest, "cursor must be an integer")
				return
			}
			cursor = c
		}
		limit, err := parseLimit(r, 50)
		if err != nil {
			respondError(w, http.StatusBadRequest, err.Error())
			return
		}
		iatas := parseIATAs(r)
		if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" {
			regionIATAs, err := resolveRegionIATAs(r.Context(), regionIDStr, r.URL.Query().Get("region"), reader)
			if err != nil {
				respondError(w, http.StatusBadRequest, err.Error())
				return
			}
			iatas = append(iatas, regionIATAs...)
		}
		observers, err := reader.ListObservers(r.Context(), iatas, observerType, broker, status, name, scope, cursor, limit)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "failed to get list of observers")
			return
		}
		respond(w, http.StatusOK, observers)
	}
}

// getObserver godoc
//
//	@Summary	Get observer detail
//	@Tags		Observers
//	@Produce	json
//	@Param		observerId	path		string	true	"Observer UUID"
//	@Success	200			{object}	api.Observer
//	@Failure	400			{object}	handlers.APIError
//	@Failure	404			{object}	handlers.APIError
//	@Router		/observers/{observerId} [get]
func getObserver(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		observerID := chi.URLParam(r, "observerId")
		id, err := uuid.Parse(observerID)
		if err != nil {
			respondError(w, http.StatusBadRequest, "failed to parse observer UUID")
			return
		}
		obs, err := reader.GetObserver(r.Context(), id)
		if err != nil {
			respondError(w, http.StatusNotFound, "observer not found")
			return
		}
		respond(w, http.StatusOK, obs)
	}
}

// listObserverAdverts godoc
//
//	@Summary	List advert packets heard by an observer
//	@Tags		Observers
//	@Produce	json
//	@Param		observerId	path		string	true	"Observer UUID"
//	@Param		cursor		query		int		false	"Observation ID of last item for pagination"
//	@Param		limit		query		int		false	"Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200)
//	@Success	200			{object}	api.Page[api.AdvertObservation]
//	@Failure	400			{object}	handlers.APIError
//	@Failure	500			{object}	handlers.APIError
//	@Router		/observers/{observerId}/adverts [get]
func listObserverAdverts(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		observerID, err := uuid.Parse(chi.URLParam(r, "observerId"))
		if err != nil {
			respondError(w, http.StatusBadRequest, "invalid observer ID")
			return
		}
		var cursor int64
		if cursorParam := r.URL.Query().Get("cursor"); cursorParam != "" {
			c, err := strconv.ParseInt(cursorParam, 10, 64)
			if err != nil {
				respondError(w, http.StatusBadRequest, "cursor must be an integer")
				return
			}
			cursor = c
		}
		limit, err := parseLimit(r, 50)
		if err != nil {
			respondError(w, http.StatusBadRequest, err.Error())
			return
		}
		adverts, err := reader.ListObserverAdverts(r.Context(), observerID, cursor, limit)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "internal server error")
			return
		}
		respond(w, http.StatusOK, adverts)
	}
}

// getObserverTelemetry godoc
//
//	@Summary	Get observer telemetry history
//	@Tags		Observers
//	@Produce	json
//	@Param		observerId	path		string	true	"Observer UUID"
//	@Param		range		query		string	false	"Duration window e.g. 24h, 48h, 168h (default 24h)"
//	@Param		afterId		query		int		false	"Return points after this telemetry ID for WS reconnection backfill"
//	@Param		interval	query		string	false	"Bucketing interval: 1h (default), 6h, or 24h"
//	@Success	200			{object}	api.ObserverTelemetry
//	@Failure	400			{object}	handlers.APIError
//	@Failure	500			{object}	handlers.APIError
//	@Router		/observers/{observerId}/telemetry [get]
func getObserverTelemetry(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		observerID, err := uuid.Parse(chi.URLParam(r, "observerId"))
		if err != nil {
			respondError(w, http.StatusBadRequest, "invalid observer ID")
			return
		}
		rangeParam := r.URL.Query().Get("range")
		if rangeParam == "" {
			rangeParam = "24h"
		}
		duration, err := time.ParseDuration(rangeParam)
		if err != nil {
			respondError(w, http.StatusBadRequest, "invalid range, use e.g. 24h, 48h, 168h")
			return
		}
		afterID := int64(0)
		if afterIDParam := r.URL.Query().Get("afterId"); afterIDParam != "" {
			id, err := strconv.ParseInt(afterIDParam, 10, 64)
			if err != nil {
				respondError(w, http.StatusBadRequest, "afterId must be an integer")
				return
			}
			afterID = id
		}
		intervalParam := r.URL.Query().Get("interval")
		if intervalParam == "" {
			intervalParam = "1h"
		}
		var bucketHours int32
		switch intervalParam {
		case "1h":
			bucketHours = 0 // use raw query
		case "6h":
			bucketHours = 6
		case "24h":
			bucketHours = 24
		default:
			respondError(w, http.StatusBadRequest, "invalid interval, use 1h, 6h or 24h")
			return
		}
		since := time.Now().Add(-duration)
		until := time.Time{} // no upper bound
		var telemetry *api.ObserverTelemetry
		if bucketHours == 0 {
			telemetry, err = reader.GetObserverTelemetry(r.Context(), observerID, since, until, afterID)
		} else {
			points, err := reader.GetObserverTelemetryBucketed(r.Context(), observerID, since, until, bucketHours)
			if err == nil {
				telemetry = &api.ObserverTelemetry{Points: points}
			}
		}
		if err != nil {
			respondError(w, http.StatusInternalServerError, "internal server error")
			return
		}
		telemetry.Range = rangeParam
		telemetry.Interval = intervalParam
		respond(w, http.StatusOK, telemetry)
	}
}
