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

package handlers

import (
	"encoding/hex"
	"net/http"
	"strconv"
	"strings"
	"time"

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

// PacketsRouter mounts all /packets routes onto a subrouter.
//
// GET  /packets              → listPackets
// GET  /packets/{packetHash} → getPacket
func PacketsRouter(reader api.Reader) http.Handler {
	r := chi.NewRouter()
	r.Get("/", listPackets(reader))
	r.Get("/backfill", listPacketsBackfill(reader))
	r.Get("/{packetHash}", getPacket(reader))
	return r
}

// listPackets godoc
//
//	@Summary	List packets
//	@Tags		Packets
//	@Produce	json
//	@Param		payloadType		query		int		false	"Filter by payload type integer"
//	@Param		payloadTypes	query		string	false	"Filter by multiple payload types, comma-separated e.g. 2,4"
//	@Param		payloadTypeName	query		string	false	"Filter by payload type name (advert, grp_txt, txt_msg, trace, anon_req)"
//	@Param		routeType		query		int		false	"Filter by route type (0=transport_flood, 1=flood, 2=direct, 3=transport_direct)"
//	@Param		routeTypes		query		string	false	"Filter by multiple route types, comma-separated e.g. 0,1"
//	@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		scope	query		string	false	"Filter by transport scope name e.g. %23bc (URL-encoded #bc)"
//	@Param		scopes	query		string	false	"Filter by multiple transport scope names, comma-separated e.g. %23bc,%23west"
//	@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		since			query		int		false	"Filter by first_heard_at >= since (epoch ms)"
//	@Param		until			query		int		false	"Filter by first_heard_at <= until (epoch ms)"
//	@Param		cursor			query		int		false	"epoch ms of last item for pagination; last_heard_at, or site-local heard_at when iatas is set"
//	@Param		limit			query		int		false	"Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200)
//	@Success	200				{object}	object
//	@Failure	400				{object}	handlers.APIError
//	@Failure	500				{object}	handlers.APIError
//	@Router		/packets [get]
func listPackets(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		payloadTypes, err := parseInt16CSVOrSingle(r, "payloadTypes", "payloadType")
		if err != nil {
			respondError(w, http.StatusBadRequest, "payloadType/payloadTypes must be integers")
			return
		}
		if len(payloadTypes) == 0 {
			if p := r.URL.Query().Get("payloadTypeName"); p != "" {
				payloadTypes = []int16{api.PayloadTypeFromString(p)}
			}
		}
		routeTypes, err := parseInt16CSVOrSingle(r, "routeTypes", "routeType")
		if err != nil {
			respondError(w, http.StatusBadRequest, "routeType/routeTypes must be integers")
			return
		}
		var since, until time.Time
		if p := r.URL.Query().Get("since"); p != "" {
			ms, err := strconv.ParseInt(p, 10, 64)
			if err != nil {
				respondError(w, http.StatusBadRequest, "since must be epoch milliseconds")
				return
			}
			since = time.UnixMilli(ms)
		}
		if p := r.URL.Query().Get("until"); p != "" {
			ms, err := strconv.ParseInt(p, 10, 64)
			if err != nil {
				respondError(w, http.StatusBadRequest, "until must be epoch milliseconds")
				return
			}
			until = time.UnixMilli(ms)
		}
		var cursor int64
		if p := r.URL.Query().Get("cursor"); p != "" {
			c, err := strconv.ParseInt(p, 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...)
		}
		scopes := parseCSVOrSingle(r, "scopes", "scope")
		packets, err := reader.ListPackets(r.Context(), payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "internal server error")
			return
		}
		respond(w, http.StatusOK, packets)
	}
}

// listPacketsBackfill godoc
//
//	@Summary	Backfill packets after a given observation ID
//	@Tags		Packets
//	@Produce	json
//	@Param		afterObservationId	query		int		true	"Return packets with observations after this ID (use last WS event observation ID)"
//	@Param		payloadType			query		int		false	"Filter by payload type integer"
//	@Param		payloadTypeName		query		string	false	"Filter by payload type name"
//	@Param		routeType			query		int		false	"Filter by route type"
//	@Param		iatas				query		string	false	"Filter by IATA code(s), comma-separated"
//	@Param		region				query		string	false	"Filter by region slug"
//	@Param		regionId			query		int		false	"Filter by region ID"
//	@Param		scope				query		string	false	"Filter by transport scope name"
//	@Param		limit				query		int		false	"Max results (default 100); must be positive, values above 200 are clamped" minimum(1) maximum(200)
//	@Success	200					{object}	[]api.PacketSummary
//	@Failure	400					{object}	handlers.APIError
//	@Failure	500					{object}	handlers.APIError
//	@Router		/packets/backfill [get]
func listPacketsBackfill(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		afterIDStr := r.URL.Query().Get("afterObservationId")
		if afterIDStr == "" {
			respondError(w, http.StatusBadRequest, "afterObservationId is required")
			return
		}
		afterID, err := strconv.ParseInt(afterIDStr, 10, 64)
		if err != nil {
			respondError(w, http.StatusBadRequest, "afterObservationId must be an integer")
			return
		}
		limit, err := parseLimit(r, 100)
		if err != nil {
			respondError(w, http.StatusBadRequest, err.Error())
			return
		}
		var payloadType int16 = -1
		if v := r.URL.Query().Get("payloadType"); v != "" {
			t, err := strconv.ParseInt(v, 10, 16)
			if err == nil {
				payloadType = int16(t)
			}
		} else if p := r.URL.Query().Get("payloadTypeName"); p != "" {
			payloadType = api.PayloadTypeFromString(p)
		}
		var routeType int16 = -1
		if v := r.URL.Query().Get("routeType"); v != "" {
			t, err := strconv.ParseInt(v, 10, 16)
			if err == nil {
				routeType = int16(t)
			}
		}
		iatas := parseIATAs(r)
		if regionIDStr := r.URL.Query().Get("regionId"); regionIDStr != "" || r.URL.Query().Get("region") != "" {
			regionIATAs, err := resolveRegionIATAs(r.Context(), r.URL.Query().Get("regionId"), r.URL.Query().Get("region"), reader)
			if err != nil {
				respondError(w, http.StatusBadRequest, err.Error())
				return
			}
			iatas = append(iatas, regionIATAs...)
		}
		scope := r.URL.Query().Get("scope")
		packets, err := reader.ListPacketsAfterID(r.Context(), afterID, payloadType, routeType, iatas, scope, limit)
		if err != nil {
			respondError(w, http.StatusInternalServerError, "internal server error")
			return
		}
		respond(w, http.StatusOK, packets)
	}
}

// getPacket godoc
//
//	@Summary	Get full packet detail. For trace packets (payloadType=9), includes resolvedRoute
//	@Tags		Packets
//	@Produce	json
//	@Param		packetHash	path		string	true	"Packet hash (hex)"
//	@Success	200			{object}	api.Packet
//	@Failure	400			{object}	handlers.APIError
//	@Failure	404			{object}	handlers.APIError
//	@Failure	500			{object}	handlers.APIError
//	@Router		/packets/{packetHash} [get]
func getPacket(reader api.Reader) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		hashHex := strings.ToLower(chi.URLParam(r, "packetHash"))
		hash, err := hex.DecodeString(hashHex)
		if err != nil {
			respondError(w, http.StatusBadRequest, "invalid packet hash")
			return
		}
		packet, err := reader.GetPacket(r.Context(), hash)
		if err != nil {
			respondError(w, http.StatusNotFound, "packet not found")
			return
		}
		respond(w, http.StatusOK, packet)
	}
}
