package classifier

import (
	"math"
)

// ExtractFeatures computes spectral features for a signal slice.
// spectrum is full-band power in dB (length fftSize).
func ExtractFeatures(s SignalInput, spectrum []float64, sampleRate int, fftSize int) Features {
	if fftSize <= 0 {
		fftSize = len(spectrum)
	}
	if len(spectrum) == 0 || s.FirstBin < 0 || s.LastBin < s.FirstBin || s.FirstBin >= len(spectrum) {
		return Features{}
	}
	if s.LastBin >= len(spectrum) {
		s.LastBin = len(spectrum) - 1
	}
	den := fftSize
	if den < 1 {
		den = 1
	}
	binHz := float64(sampleRate) / float64(den)
	// slice
	start := s.FirstBin
	end := s.LastBin
	peakDb := -1e9
	peakIdx := start
	sumLin := 0.0
	geoSum := 0.0
	count := 0
	for i := start; i <= end; i++ {
		db := spectrum[i]
		if db > peakDb {
			peakDb = db
			peakIdx = i
		}
		p := math.Pow(10, db/10.0)
		sumLin += p
		if p > 0 {
			geoSum += math.Log(p)
		}
		count++
	}
	avgLin := 0.0
	if count > 0 {
		avgLin = sumLin / float64(count)
	}
	// Peak-to-avg in dB
	peakToAvg := 0.0
	if avgLin > 0 {
		peakToAvg = 10 * math.Log10(math.Pow(10, peakDb/10.0)/avgLin)
	}
	// Spectral flatness
	flat := 0.0
	if count > 0 && avgLin > 0 {
		geoMean := math.Exp(geoSum / float64(count))
		flat = geoMean / avgLin
	}
	// BW3dB
	bw3 := bwAtThreshold(spectrum, start, end, peakDb-3.0) * binHz
	// BW90 (90% energy)
	bw90 := bwEnergy(spectrum, start, end, 0.90) * binHz
	// Symmetry (power left/right of peak)
	leftSum, rightSum := 0.0, 0.0
	for i := start; i <= end; i++ {
		p := math.Pow(10, spectrum[i]/10.0)
		if i <= peakIdx {
			leftSum += p
		} else {
			rightSum += p
		}
	}
	sym := 0.0
	if leftSum+rightSum > 0 {
		sym = (rightSum - leftSum) / (rightSum + leftSum)
	}
	// Rolloff (dB/kHz) at edges
	leftDb := spectrum[start]
	rightDb := spectrum[end]
	leftHz := math.Max(binHz, float64(peakIdx-start)*binHz)
	rightHz := math.Max(binHz, float64(end-peakIdx)*binHz)
	rollL := (peakDb - leftDb) / (leftHz / 1e3)
	rollR := (peakDb - rightDb) / (rightHz / 1e3)

	return Features{
		BW3dB:        bw3,
		BW90:         bw90,
		SpectralFlat: clamp01(flat),
		PeakToAvg:    peakToAvg,
		Symmetry:     sym,
		RolloffLeft:  rollL,
		RolloffRight: rollR,
	}
}

func bwAtThreshold(spectrum []float64, start, end int, threshDb float64) float64 {
	left := start
	right := end
	for i := start; i <= end; i++ {
		if spectrum[i] >= threshDb {
			left = i
			break
		}
	}
	for i := end; i >= start; i-- {
		if spectrum[i] >= threshDb {
			right = i
			break
		}
	}
	if right < left {
		return float64(end - start + 1)
	}
	return float64(right - left + 1)
}

func bwEnergy(spectrum []float64, start, end int, frac float64) float64 {
	if frac <= 0 {
		return 0
	}
	if frac > 1 {
		frac = 1
	}
	powers := make([]float64, 0, end-start+1)
	sum := 0.0
	for i := start; i <= end; i++ {
		p := math.Pow(10, spectrum[i]/10.0)
		sum += p
		powers = append(powers, p)
	}
	if sum == 0 {
		return float64(end - start + 1)
	}
	// accumulate from center outward
	center := (start + end) / 2
	l := center
	r := center
	acc := powers[center-start]
	for acc/sum < frac && (l > start || r < end) {
		if l > start {
			l--
			acc += powers[l-start]
		}
		if acc/sum >= frac {
			break
		}
		if r < end {
			r++
			acc += powers[r-start]
		}
	}
	return float64(r - l + 1)
}

func clamp01(v float64) float64 {
	if v < 0 {
		return 0
	}
	if v > 1 {
		return 1
	}
	return v
}
