package detect

import (
	"context"
	"strings"
	"time"

	"github.com/wangjia/pangolin/server/internal/scheduler/probe"
)

// NodeSignal is the normalised set of detection signals for a single node
// produced by one Tick.
type NodeSignal struct {
	NodeID string

	// DomesticTotalISPs is the count of distinct domestic (CN) ISPs for which
	// probe data was available this cycle.  Vantages with no data are excluded
	// from both numerator and denominator per spec.
	DomesticTotalISPs int

	// DomesticFailISPs is the count of domestic ISPs judged to be failing.
	// An ISP is failing if ANY vantage from that ISP indicates a failure at
	// the highest available layer (L3 > L2 > L1; see isReportFailed).
	DomesticFailISPs int

	// OverseasHasData is true when at least one non-CN vantage reported this cycle.
	OverseasHasData bool

	// OverseasOK is true when every overseas vantage passed L1 this cycle.
	// Only meaningful when OverseasHasData is true.
	OverseasOK bool

	// TrafficDropPct is the percentage drop in online-connection count computed
	// by comparing the first half vs the second half of the 15-minute
	// GetLoadHistory window.  Zero when insufficient history is available.
	TrafficDropPct float64

	// TrafficBaseline is the online-connection count from the most recent load
	// sample.  Used by the traffic-warning rule's minimum-baseline guard.
	TrafficBaseline int
}

// ProbeSnapshotter lets the Engine read probe snapshots without depending on
// the concrete *probe.Store (facilitates mocking in unit tests).
type ProbeSnapshotter interface {
	SnapshotsByNode(ctx context.Context, nodeID string) (map[string]probe.ProbeSnapshot, error)
}

// computeSignals builds a NodeSignal for nodeID from the current probe
// snapshots and the 15-minute load history.
//
// ISP normalisation: third-party probes (15C) use a "3rd-" prefix on their
// ISP names (e.g. "3rd-ChinaTelecom").  This prefix is stripped before
// grouping so that first-party (15B) and third-party vantages from the same
// ISP are merged, per spec.
func computeSignals(
	ctx context.Context,
	nodeID string,
	snapshots map[string]probe.ProbeSnapshot,
	lc LifecycleService,
) NodeSignal {
	sig := NodeSignal{NodeID: nodeID}

	// domesticISPs: normalised ISP name → whether any vantage from that ISP
	// shows a failure this cycle.
	domesticISPs := make(map[string]bool)
	overseasTotal := 0
	overseasOKCount := 0

	for _, snap := range snapshots {
		v := snap.Vantage
		r := snap.Report

		switch {
		case v.Country == "CN":
			// Domestic vantage. Normalise ISP name and merge failures:
			// any failing vantage from the same ISP marks that ISP as failed.
			isp := normaliseISP(v.ISP)
			failed := isReportFailed(r)
			if prev, seen := domesticISPs[isp]; seen {
				domesticISPs[isp] = prev || failed
			} else {
				domesticISPs[isp] = failed
			}

		case v.Country != "":
			// Overseas vantage (any country other than CN).
			overseasTotal++
			if r.L1.OK {
				overseasOKCount++
			}
		// Empty Country: vantage identity unknown – excluded from both pools.
		}
	}

	sig.DomesticTotalISPs = len(domesticISPs)
	for _, failed := range domesticISPs {
		if failed {
			sig.DomesticFailISPs++
		}
	}
	sig.OverseasHasData = overseasTotal > 0
	sig.OverseasOK = overseasTotal > 0 && overseasOKCount == overseasTotal

	// Traffic drop: compare the older half of the 15-minute window to the
	// newer half.  Zero is returned when there is too little history.
	history, err := lc.GetLoadHistory(ctx, nodeID, 15*time.Minute)
	if err == nil && len(history) > 0 {
		sig.TrafficBaseline = history[len(history)-1].Online
		if len(history) >= 2 {
			sig.TrafficDropPct = trafficDropPct(history)
		}
	}

	return sig
}

// isReportFailed determines whether a single NodeReport indicates a failure
// at the highest available measurement layer.
//
// Priority (highest to lowest):
//   - L3 present → L3.OK is the authoritative verdict.
//     A failing L3 is recorded even when L1/L2 passed (e.g. DPI/SNI reset
//     at the application layer after a successful TCP/TLS handshake).
//   - L3 absent → check L1 then L2.
func isReportFailed(r probe.NodeReport) bool {
	if r.L3 != nil {
		return !r.L3.OK
	}
	if !r.L1.OK {
		return true
	}
	if r.L2 != nil && !r.L2.OK {
		return true
	}
	return false
}

// normaliseISP strips the "3rd-" prefix that AliyunSyntheticAgent (15C) adds
// to distinguish third-party vantages, yielding the canonical ISP name used
// for grouping.
func normaliseISP(isp string) string {
	return strings.TrimPrefix(isp, "3rd-")
}

// trafficDropPct computes the percentage drop between the first and second
// halves of the load-history slice (oldest → newest).
// Returns 0 when the baseline (first-half average) is zero or when the
// second half average is higher (traffic increased).
func trafficDropPct(history []LoadPoint) float64 {
	n := len(history)
	if n < 2 {
		return 0
	}
	half := n / 2
	older := avgOnline(history[:half])
	newer := avgOnline(history[half:])
	if older == 0 {
		return 0
	}
	drop := older - newer
	if drop <= 0 {
		return 0
	}
	return float64(drop) / float64(older) * 100
}

func avgOnline(pts []LoadPoint) int {
	if len(pts) == 0 {
		return 0
	}
	sum := 0
	for _, p := range pts {
		sum += p.Online
	}
	return sum / len(pts)
}
