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

package cache

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

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

const (
	keyIATAs                   = "beacon:iatas"
	keyIATAPrefix              = "beacon:iata:"
	keyIATABorderPrefix        = "beacon:iata:border:"
	keyRegions                 = "beacon:regions"
	keyRegionPrefix            = "beacon:region:"
	keyRegionSlugPrefix        = "beacon:region:slug:"
	keyScopeNames              = "beacon:scope:names"
	keyScopeStats              = "beacon:scope:stats"
	keyScopesByIATAsPrefix     = "beacon:scopes:iatas:"
	keyScopeByNamePrefix       = "beacon:scope:name:"
	keyStatsOverviewPrefix     = "beacon:stats:overview:"
	keyStatsObservationsPrefix = "beacon:stats:observations:"
	keyStatsBreakdownPrefix    = "beacon:stats:breakdown:"
	keyStatsTopNodesPrefix     = "beacon:stats:top-nodes:"
	keyStatsTopObsPrefix       = "beacon:stats:top-observers:"
	keyStatsTopAdvPrefix       = "beacon:stats:top-advertisers:"
	keyStatsClockDriftPrefix   = "beacon:stats:clock-drift:"
	keyStatsTopTalkersPrefix   = "beacon:stats:top-talkers:"
	keyStatsNodeTypes          = "beacon:stats:node-types:"
	keyRadioPresetsPrefix      = "beacon:radio-presets:"
	keyNodePrefix              = "beacon:node:"
	keyNodeNeighborsPrefix     = "beacon:node:neighbors:"
	keyNodesByIDsPrefix        = "beacon:nodes:ids:"
	keyObserverPrefix          = "beacon:observer:"
	keyObserverScopesPrefix    = "beacon:observer:scopes:"
)

// CachedReader wraps an api.Reader with a Redis caching layer.
// It implements api.Reader and is a drop-in replacement for db.Store
// at the wiring point in main.go.
type CachedReader struct {
	inner api.Reader
	c     *Client
	ttl   CacheTTLs
}

// CacheTTLs holds the resolved per-category TTLs for the cache layer.
// All fields should be non-zero — use ResolveTTLs to build this from
// config with fallback to the global TTL and then the default.
type CacheTTLs struct {
	Stats     time.Duration
	Reference time.Duration
	Nodes     time.Duration
	Observers time.Duration
}

// NewCachedReader returns an api.Reader that transparently caches responses
// using the provided Redis client and TTL configuration. inner is the
// underlying db.Store that is called on cache misses.
func NewCachedReader(inner api.Reader, c *Client, ttl CacheTTLs) api.Reader {
	return &CachedReader{
		inner: inner,
		c:     c,
		ttl:   ttl,
	}
}

// InvalidateNode removes the cached entries for a node by UUID.
// Should be called from the ingest path after a node upsert.
func (cr *CachedReader) InvalidateNode(ctx context.Context, nodeID uuid.UUID) {
	id := nodeID.String()
	cr.c.del(ctx, keyNodePrefix+id, keyNodeNeighborsPrefix+id)
}

// InvalidateObserver removes the cached entries for an observer by UUID.
// Should be called from the ingest path after an observer upsert.
func (cr *CachedReader) InvalidateObserver(ctx context.Context, observerID uuid.UUID) {
	id := observerID.String()
	cr.c.del(ctx, keyObserverPrefix+id, keyObserverScopesPrefix+id)
}

// ListIATAs implements [api.Reader].
func (cr *CachedReader) ListIATAs(ctx context.Context) ([]api.IATA, error) {
	return getOrSet(ctx, cr.c, keyIATAs, cr.ttl.Reference, func() ([]api.IATA, error) {
		return cr.inner.ListIATAs(ctx)
	})
}

// GetIATA implements [api.Reader].
func (cr *CachedReader) GetIATA(ctx context.Context, iata string) (*api.IATA, error) {
	return getOrSet(ctx, cr.c, keyIATAPrefix+iata, cr.ttl.Reference, func() (*api.IATA, error) {
		return cr.inner.GetIATA(ctx, iata)
	})
}

// GetIATABorder implements [api.Reader].
func (cr *CachedReader) GetIATABorder(ctx context.Context, iata string) (json.RawMessage, error) {
	return getOrSet(ctx, cr.c, keyIATABorderPrefix+iata, cr.ttl.Reference, func() (json.RawMessage, error) {
		return cr.inner.GetIATABorder(ctx, iata)
	})
}

// ListRegions implements [api.Reader].
func (cr *CachedReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) {
	return getOrSet(ctx, cr.c, keyRegions, cr.ttl.Reference, func() ([]api.RegionSummary, error) {
		return cr.inner.ListRegions(ctx)
	})
}

// GetRegion implements [api.Reader].
func (cr *CachedReader) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) {
	return getOrSet(ctx, cr.c, fmt.Sprintf("%s%d", keyRegionPrefix, regionID), cr.ttl.Reference, func() (*api.Region, error) {
		return cr.inner.GetRegion(ctx, regionID)
	})
}

// GetRegionBySlug implements [api.Reader].
func (cr *CachedReader) GetRegionBySlug(ctx context.Context, slug string) (*api.Region, error) {
	return getOrSet(ctx, cr.c, keyRegionSlugPrefix+slug, cr.ttl.Reference, func() (*api.Region, error) {
		return cr.inner.GetRegionBySlug(ctx, slug)
	})
}

// GetScopeNames implements [api.Reader].
func (cr *CachedReader) GetScopeNames(ctx context.Context) ([]string, error) {
	return getOrSet(ctx, cr.c, keyScopeNames, cr.ttl.Reference, func() ([]string, error) {
		return cr.inner.GetScopeNames(ctx)
	})
}

// GetScopeStats implements [api.Reader].
func (cr *CachedReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
	return getOrSet(ctx, cr.c, keyScopeStats, cr.ttl.Reference, func() ([]api.ScopeStats, error) {
		return cr.inner.GetScopeStats(ctx)
	})
}

// GetScopesByIATAs implements [api.Reader].
func (cr *CachedReader) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
	sorted := make([]string, len(iatas))
	copy(sorted, iatas)
	sort.Strings(sorted)
	key := keyScopesByIATAsPrefix + strings.Join(sorted, ",")
	return getOrSet(ctx, cr.c, key, cr.ttl.Reference, func() ([]api.ScopeSummary, error) {
		return cr.inner.GetScopesByIATAs(ctx, iatas)
	})
}

// GetScopeByName implements [api.Reader].
func (cr *CachedReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
	return getOrSet(ctx, cr.c, keyScopeByNamePrefix+name, cr.ttl.Reference, func() (*api.ScopeDetail, error) {
		return cr.inner.GetScopeByName(ctx, name)
	})
}

// GetStatsOverview implements [api.Reader].
func (cr *CachedReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s", keyStatsOverviewPrefix, segment)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() (*api.StatsOverview, error) {
		return cr.inner.GetStatsOverview(ctx, iatas)
	})
}

// GetStatsObservations implements [api.Reader].
func (cr *CachedReader) GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d", keyStatsObservationsPrefix, segment, since.UnixMilli())
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.ObservationPoint, error) {
		return cr.inner.GetStatsObservations(ctx, iatas, since)
	})
}

// GetStatsPayloadBreakdown implements [api.Reader].
func (cr *CachedReader) GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d", keyStatsBreakdownPrefix, segment, since.UnixMilli())
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.PayloadBreakdownItem, error) {
		return cr.inner.GetStatsPayloadBreakdown(ctx, iatas, since)
	})
}

// GetStatsTopNodes implements [api.Reader].
func (cr *CachedReader) GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d", keyStatsTopNodesPrefix, segment, limit)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.TopNode, error) {
		return cr.inner.GetStatsTopNodes(ctx, iatas, limit)
	})
}

// GetStatsNodeTypes implements [api.Reader].
func (cr *CachedReader) GetStatsNodeTypes(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s", keyStatsNodeTypes, segment)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.NodeTypeCount, error) {
		return cr.inner.GetStatsNodeTypes(ctx, iatas)
	})
}

// GetStatsTopObservers implements [api.Reader].
func (cr *CachedReader) GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d:%d", keyStatsTopObsPrefix, segment, since.UnixMilli(), limit)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.TopObserver, error) {
		return cr.inner.GetStatsTopObservers(ctx, iatas, since, limit)
	})
}

// GetStatsTopAdvertisers implements [api.Reader].
func (cr *CachedReader) GetStatsTopAdvertisers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopAdvertiser, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d:%d", keyStatsTopAdvPrefix, segment, since.UnixMilli(), limit)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.TopAdvertiser, error) {
		return cr.inner.GetStatsTopAdvertisers(ctx, iatas, since, limit)
	})
}

// GetStatsClockDrift implements [api.Reader].
func (cr *CachedReader) GetStatsClockDrift(ctx context.Context, iatas []string, limit int32) ([]api.ClockDriftEntry, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d", keyStatsClockDriftPrefix, segment, limit)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.ClockDriftEntry, error) {
		return cr.inner.GetStatsClockDrift(ctx, iatas, limit)
	})
}

// GetStatsTopTalkers implements [api.Reader].
func (cr *CachedReader) GetStatsTopTalkers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopTalker, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%d:%d", keyStatsTopTalkersPrefix, segment, since.UnixMilli(), limit)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.TopTalker, error) {
		return cr.inner.GetStatsTopTalkers(ctx, iatas, since, limit)
	})
}

// GetRadioPresets implements [api.Reader].
func (cr *CachedReader) GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) {
	segment := "all"
	if len(iatas) > 0 {
		sorted := append([]string(nil), iatas...)
		sort.Strings(sorted)
		segment = strings.Join(sorted, ",")
	}
	key := fmt.Sprintf("%s%s:%s", keyRadioPresetsPrefix, preset, segment)
	return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() ([]api.RadioPreset, error) {
		return cr.inner.GetRadioPresets(ctx, preset, iatas)
	})
}

// GetNode implements [api.Reader].
func (cr *CachedReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) {
	return getOrSet(ctx, cr.c, keyNodePrefix+nodeID.String(), cr.ttl.Nodes, func() (*api.Node, error) {
		return cr.inner.GetNode(ctx, nodeID)
	})
}

// GetNodeNeighbors implements [api.Reader].
func (cr *CachedReader) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) {
	return getOrSet(ctx, cr.c, keyNodeNeighborsPrefix+nodeID.String(), cr.ttl.Nodes, func() ([]api.NodeNeighbor, error) {
		return cr.inner.GetNodeNeighbors(ctx, nodeID)
	})
}

// GetNodesByIDs implements [api.Reader].
func (cr *CachedReader) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
	strs := make([]string, len(ids))
	for i, id := range ids {
		strs[i] = id.String()
	}
	sort.Strings(strs)
	key := keyNodesByIDsPrefix + strings.Join(strs, ",")
	return getOrSet(ctx, cr.c, key, cr.ttl.Nodes, func() (map[uuid.UUID]*api.ResolvedNode, error) {
		return cr.inner.GetNodesByIDs(ctx, ids)
	})
}

// GetObserver implements [api.Reader].
func (cr *CachedReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) {
	return getOrSet(ctx, cr.c, keyObserverPrefix+observerID.String(), cr.ttl.Observers, func() (*api.Observer, error) {
		return cr.inner.GetObserver(ctx, observerID)
	})
}

// GetObserverScopes implements [api.Reader].
func (cr *CachedReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) {
	return getOrSet(ctx, cr.c, keyObserverScopesPrefix+observerID.String(), cr.ttl.Observers, func() ([]string, error) {
		return cr.inner.GetObserverScopes(ctx, observerID)
	})
}

// GetObserverTelemetry implements [api.Reader].
func (cr *CachedReader) GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) {
	return cr.inner.GetObserverTelemetry(ctx, observerID, since, until, afterID)
}

// GetObserverTelemetryBucketed implements [api.Reader].
func (cr *CachedReader) GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) {
	return cr.inner.GetObserverTelemetryBucketed(ctx, observerID, since, until, bucketHours)
}

// GetPacket implements [api.Reader].
func (cr *CachedReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) {
	return cr.inner.GetPacket(ctx, packetHash)
}

// GetChannel implements [api.Reader].
func (cr *CachedReader) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
	return cr.inner.GetChannel(ctx, channelID)
}

// GetTraceByTag implements [api.Reader].
func (cr *CachedReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
	return cr.inner.GetTraceByTag(ctx, tag)
}

// GetKnownRoutesByNode implements [api.Reader].
func (cr *CachedReader) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error) {
	return cr.inner.GetKnownRoutesByNode(ctx, iata, nodeID)
}

// GetCrossIATANeighbors implements [api.Reader].
func (cr *CachedReader) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) {
	return cr.inner.GetCrossIATANeighbors(ctx, nodeID, iata)
}

// ListChannels implements [api.Reader].
func (cr *CachedReader) ListChannels(ctx context.Context, limit int32, hash []byte, iatas []string, cursor int64) (api.Page[api.ChannelSummary], error) {
	return cr.inner.ListChannels(ctx, limit, hash, iatas, cursor)
}

// ListChannelMessages implements [api.Reader].
func (cr *CachedReader) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
	return cr.inner.ListChannelMessages(ctx, channelID, since, limit, iatas, scope, cursor)
}

// ListChannelMessagesByHash implements [api.Reader].
func (cr *CachedReader) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
	return cr.inner.ListChannelMessagesByHash(ctx, hash, since, limit, iatas, scope, cursor)
}

// ListMessagesAfterID implements [api.Reader].
func (cr *CachedReader) ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) {
	return cr.inner.ListMessagesAfterID(ctx, afterID, iatas, scope, limit)
}

// ListNodes implements [api.Reader].
func (cr *CachedReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
	return cr.inner.ListNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors)
}

// ListNodeObservations implements [api.Reader].
func (cr *CachedReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) {
	return cr.inner.ListNodeObservations(ctx, nodeID, cursor, limit)
}

// ListObservers implements [api.Reader].
func (cr *CachedReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) {
	return cr.inner.ListObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit)
}

// ListObserverAdverts implements [api.Reader].
func (cr *CachedReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) {
	return cr.inner.ListObserverAdverts(ctx, observerID, cursor, limit)
}

// ListPackets implements [api.Reader].
func (cr *CachedReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
	return cr.inner.ListPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
}

// ListPacketsAfterID implements [api.Reader].
func (cr *CachedReader) ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) {
	return cr.inner.ListPacketsAfterID(ctx, afterObservationID, payloadType, routeType, iatas, scope, limit)
}

// ListKnownRoutes implements [api.Reader].
func (cr *CachedReader) ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error) {
	return cr.inner.ListKnownRoutes(ctx, iata, hopCount, cursor, limit)
}

// SearchKnownRoutes implements [api.Reader].
func (cr *CachedReader) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) {
	return cr.inner.SearchKnownRoutes(ctx, iata, fromHash, toHash)
}

// SearchCrossIATARoutes implements [api.Reader].
func (cr *CachedReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error) {
	return cr.inner.SearchCrossIATARoutes(ctx, fromHash, fromIATA, toHash, toIATA)
}

// ListTraceTags implements [api.Reader].
func (cr *CachedReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
	return cr.inner.ListTraceTags(ctx, iatas, scope, traceType, since, until, cursor, limit)
}
