package offline

import (
	"math"
	"sort"
	"testing"
	"time"

	"github.com/jan/fm-rds-tx/internal/audio"
	cfgpkg "github.com/jan/fm-rds-tx/internal/config"
	"github.com/jan/fm-rds-tx/internal/dsp"
	"github.com/jan/fm-rds-tx/internal/license"
	"github.com/jan/fm-rds-tx/internal/output"
	"github.com/jan/fm-rds-tx/internal/watermark"
)

// noiseSource provides deterministic broadband stereo material so the
// watermark has energy across many bins and the detector has something real to
// correlate against.
type noiseSource struct {
	stateL uint64
	stateR uint64
	amp    float64
}

func newNoiseSource(seed uint64, amp float64) *noiseSource {
	return &noiseSource{stateL: seed, stateR: seed ^ 0x9e3779b97f4a7c15, amp: amp}
}

func (n *noiseSource) nextNorm(state *uint64) float64 {
	*state = *state*6364136223846793005 + 1442695040888963407
	return float64(int32(*state>>33)) / float64(1<<31)
}

func (n *noiseSource) NextFrame() audio.Frame {
	l := n.amp * n.nextNorm(&n.stateL)
	r := n.amp * n.nextNorm(&n.stateR)
	return audio.NewFrame(audio.Sample(l), audio.Sample(r))
}

func newWatermarkE2EGenerator(t *testing.T, key string) *Generator {
	t.Helper()
	cfg := cfgpkg.Default()
	cfg.RDS.Enabled = false
	cfg.FM.CompositeRateHz = 228000
	cfg.FM.StereoEnabled = true
	cfg.FM.OutputDrive = 0.5
	cfg.FM.LimiterEnabled = true
	cfg.FM.LimiterCeiling = 1.0
	cfg.FM.FMModulationEnabled = false // composite output for inspection
	cfg.Audio.ToneAmplitude = 0        // external source only
	cfg.Audio.Gain = 1.0
	cfg.FM.PreEmphasisTauUS = 50

	gen := NewGenerator(cfg)
	if err := gen.SetExternalSource(newNoiseSource(12345, 0.22)); err != nil {
		t.Fatalf("SetExternalSource: %v", err)
	}
	gen.SetLicense(license.NewState(""))
	gen.ConfigureWatermark(true, key)
	return gen
}

func extractCompositeFrame(frame *output.CompositeFrame) []float64 {
	out := make([]float64, len(frame.Samples))
	for i, s := range frame.Samples {
		out[i] = float64(s.I)
	}
	return out
}

func downsampleForWatermarkDetection(composite []float64, rate float64) ([]float64, float64) {
	decimFactor := int(rate / float64(watermark.WMRate))
	if decimFactor < 1 {
		decimFactor = 1
	}
	actualRate := rate / float64(decimFactor)

	lpf := dsp.NewLPF8(5500, rate)
	filtered := make([]float64, len(composite))
	for i, s := range composite {
		filtered[i] = lpf.Process(s)
	}
	out := make([]float64, len(filtered)/decimFactor)
	for i := range out {
		out[i] = filtered[i*decimFactor]
	}
	return out, actualRate
}

func decodeWatermarkFromComposite(t *testing.T, composite []float64, rate float64) ([watermark.RsDataBytes]byte, [watermark.PayloadBits]float64, bool) {
	t.Helper()
	var zero [watermark.RsDataBytes]byte

	down, actualRate := downsampleForWatermarkDetection(composite, rate)
	if math.Abs(actualRate-float64(watermark.WMRate)) > 1e-9 {
		t.Fatalf("unexpected detector rate %.3f Hz after decimation", actualRate)
	}
	if len(down) <= watermark.FFTSize*2 {
		return zero, [watermark.PayloadBits]float64{}, false
	}

	probe := down[watermark.FFTSize:]
	det := watermark.NewSTFTDetector()
	corrs, bestOffset := det.Detect(probe)

	var sumAbs float64
	var strong int
	for _, c := range corrs {
		ac := math.Abs(c)
		sumAbs += ac
		if ac > 1.0 {
			strong++
		}
	}
	t.Logf("watermark detect: bestOffset=%d avg|c|=%.2f strong=%d/%d", bestOffset, sumAbs/float64(watermark.PayloadBits), strong, watermark.PayloadBits)

	var recv [watermark.RsTotalBytes]byte
	byteConfs := make([]float64, watermark.RsTotalBytes)
	for i := 0; i < watermark.PayloadBits; i++ {
		if corrs[i] < 0 {
			recv[i/8] |= 1 << uint(7-(i%8))
		}
		byteConfs[i/8] += math.Abs(corrs[i])
	}

	type bc struct{ idx int; conf float64 }
	ranked := make([]bc, watermark.RsTotalBytes)
	for i := range ranked {
		ranked[i] = bc{i, byteConfs[i]}
	}
	sort.Slice(ranked, func(a, b int) bool { return ranked[a].conf < ranked[b].conf })

	for ne := 0; ne <= watermark.RsCheckBytes; ne++ {
		erasePos := make([]int, ne)
		for i := 0; i < ne; i++ {
			erasePos[i] = ranked[i].idx
		}
		sort.Ints(erasePos)
		payload, ok := watermark.RSDecode(recv, erasePos)
		if ok {
			return payload, corrs, true
		}
	}
	return zero, corrs, false
}

func TestWatermarkE2E(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping long watermark E2E test in -short mode")
	}
	const key = "test-key-e2e"
	const duration = 45 * time.Second

	gen := newWatermarkE2EGenerator(t, key)
	frame := gen.GenerateFrame(duration)
	if frame == nil {
		t.Fatal("GenerateFrame returned nil")
	}
	composite := extractCompositeFrame(frame)
	payload, _, ok := decodeWatermarkFromComposite(t, composite, frame.SampleRateHz)
	if !ok {
		t.Fatal("RS decode failed — watermark not recoverable from generator composite")
	}
	if !watermark.KeyMatchesPayload(key, payload) {
		t.Fatalf("decoded payload mismatch: key=%q payload=%x", key, payload)
	}
}
