package indexer

import (
	"errors"
	"sync"
	"time"

	"oc-discovery/conf"
	"oc-discovery/daemons/node/common"

	pp "github.com/libp2p/go-libp2p/core/peer"
)

// ── defaults ──────────────────────────────────────────────────────────────────

const (
	defaultMaxHBPerMinute   = 5
	defaultMaxPublishPerMin = 10
	defaultMaxGetPerMin     = 50
	strikeThreshold         = 3
	banDuration             = 10 * time.Minute
	behaviorWindowDur       = 60 * time.Second
)

// ── per-node state ────────────────────────────────────────────────────────────

type nodeBehavior struct {
	mu          sync.Mutex
	knownDID    string
	hbTimes     []time.Time
	pubTimes    []time.Time
	getTimes    []time.Time
	strikes     int
	bannedUntil time.Time
}

func (nb *nodeBehavior) isBanned() bool {
	return time.Now().UTC().Before(nb.bannedUntil)
}

func (nb *nodeBehavior) strike(n int) {
	nb.strikes += n
	if nb.strikes >= strikeThreshold {
		nb.bannedUntil = time.Now().Add(banDuration)
	}
}

func pruneWindow(ts []time.Time, dur time.Duration) []time.Time {
	cutoff := time.Now().Add(-dur)
	i := 0
	for i < len(ts) && ts[i].Before(cutoff) {
		i++
	}
	return ts[i:]
}

// recordInWindow appends now to the window slice and returns false (+ adds a
// strike) when the count exceeds max.
func (nb *nodeBehavior) recordInWindow(ts *[]time.Time, max int) bool {
	*ts = pruneWindow(*ts, behaviorWindowDur)
	if len(*ts) >= max {
		nb.strike(1)
		return false
	}
	*ts = append(*ts, time.Now())
	return true
}

// ── NodeBehaviorTracker ───────────────────────────────────────────────────────

// NodeBehaviorTracker is the indexer-side per-node compliance monitor.
// It is entirely local: no state is shared with other indexers.
type NodeBehaviorTracker struct {
	mu    sync.RWMutex
	nodes map[pp.ID]*nodeBehavior

	maxHB  int
	maxPub int
	maxGet int
}

func newNodeBehaviorTracker() *NodeBehaviorTracker {
	cfg := conf.GetConfig()
	return &NodeBehaviorTracker{
		nodes:  make(map[pp.ID]*nodeBehavior),
		maxHB:  common.CfgOr(cfg.MaxHBPerMinute, defaultMaxHBPerMinute),
		maxPub: common.CfgOr(cfg.MaxPublishPerMinute, defaultMaxPublishPerMin),
		maxGet: common.CfgOr(cfg.MaxGetPerMinute, defaultMaxGetPerMin),
	}
}

func (t *NodeBehaviorTracker) get(pid pp.ID) *nodeBehavior {
	t.mu.RLock()
	nb := t.nodes[pid]
	t.mu.RUnlock()
	if nb != nil {
		return nb
	}
	t.mu.Lock()
	defer t.mu.Unlock()
	if nb = t.nodes[pid]; nb == nil {
		nb = &nodeBehavior{}
		t.nodes[pid] = nb
	}
	return nb
}

// IsBanned returns true when the peer is in an active ban period.
func (t *NodeBehaviorTracker) IsBanned(pid pp.ID) bool {
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	return nb.isBanned()
}

// RecordHeartbeat checks heartbeat cadence. Returns an error if the peer is
// flooding (too many heartbeats in the sliding window).
func (t *NodeBehaviorTracker) RecordHeartbeat(pid pp.ID) error {
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.isBanned() {
		return errors.New("peer is banned")
	}
	if !nb.recordInWindow(&nb.hbTimes, t.maxHB) {
		return errors.New("heartbeat flood detected")
	}
	return nil
}

// CheckIdentity verifies that the DID associated with a PeerID never changes.
// A DID change is a strong signal of identity spoofing.
func (t *NodeBehaviorTracker) CheckIdentity(pid pp.ID, did string) error {
	if did == "" {
		return nil
	}
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.knownDID == "" {
		nb.knownDID = did
		return nil
	}
	if nb.knownDID != did {
		nb.strike(2) // identity change is severe
		return errors.New("DID mismatch for peer " + pid.String())
	}
	return nil
}

// RecordBadSignature registers a cryptographic verification failure.
// A single bad signature is worth 2 strikes (near-immediate ban).
func (t *NodeBehaviorTracker) RecordBadSignature(pid pp.ID) {
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	nb.strike(2)
}

// RecordPublish checks publish volume. Returns an error if the peer is
// sending too many publish requests.
func (t *NodeBehaviorTracker) RecordPublish(pid pp.ID) error {
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.isBanned() {
		return errors.New("peer is banned")
	}
	if !nb.recordInWindow(&nb.pubTimes, t.maxPub) {
		return errors.New("publish volume exceeded")
	}
	return nil
}

// RecordGet checks get volume. Returns an error if the peer is enumerating
// the DHT at an abnormal rate.
func (t *NodeBehaviorTracker) RecordGet(pid pp.ID) error {
	nb := t.get(pid)
	nb.mu.Lock()
	defer nb.mu.Unlock()
	if nb.isBanned() {
		return errors.New("peer is banned")
	}
	if !nb.recordInWindow(&nb.getTimes, t.maxGet) {
		return errors.New("get volume exceeded")
	}
	return nil
}

// Cleanup removes the behavior entry for a peer if it is not currently banned.
// Called when the peer is evicted from StreamRecords by the GC.
func (t *NodeBehaviorTracker) Cleanup(pid pp.ID) {
	t.mu.RLock()
	nb := t.nodes[pid]
	t.mu.RUnlock()
	if nb == nil {
		return
	}
	nb.mu.Lock()
	banned := nb.isBanned()
	nb.mu.Unlock()
	if !banned {
		t.mu.Lock()
		delete(t.nodes, pid)
		t.mu.Unlock()
	}
}
