// Package license handles fm-rds-tx key validation and jingle injection.
//
// Key format:  FMRTX-<BASE32(HMAC-SHA256(payload, secret)[:10])>
// payload:     "free" for gratis keys, email for paid keys.
//
// Without a valid key the jingle WAV is mixed into the composite output
// every JingleIntervalMinutes minutes. With a valid key the jingle is silent.
package license

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base32"
	"encoding/binary"
	"fmt"
	"math"
	"strings"
	"time"
)

// hmacSecret is the shared secret used to sign and verify keys.
// Change this value in your private fork — keys signed with the old
// secret stop working, forcing a re-issue. Never commit the real secret.
const hmacSecret = "Q7m!xP2#rL9$vN4@tK8%hD3&yF6*zC1+uB5"

// JingleIntervalMinutes is how often the jingle fires when unlicensed.
const JingleIntervalMinutes = 20

// jingleSampleRate and jingleChannels must match the embedded jingle.wav.
// The mixer resamples on-the-fly if the composite rate differs.
const (
	jingleWAVRate     = 44100
	jingleWAVChannels = 2
)

// State holds the runtime license + jingle state for a running generator.
type State struct {
	licensed bool

	active      bool    // jingle currently playing
	pos         int     // playback position in jingleFrames
	nextFire    time.Time
	jingleLevel float64 // composite injection amplitude (0..1)
}

// NewState validates the provided key and returns a ready State.
// If key is empty or invalid, the jingle fires every JingleIntervalMinutes.
func NewState(key string) *State {
	s := &State{
		licensed:    ValidateKey(key),
		jingleLevel: 0.25, // 25% composite injection — loud but not clipping
	}
	if !s.licensed {
		s.nextFire = time.Now().Add(time.Duration(JingleIntervalMinutes) * time.Minute)
	}
	return s
}

// Licensed reports whether a valid key was supplied.
func (s *State) Licensed() bool { return s.licensed }

// Active reports whether the jingle is currently playing.
func (s *State) Active() bool { return s.active }

// NextSample returns the jingle contribution for one composite sample.
// Call once per sample from the DSP loop — it is not thread-safe and must
// be called from the single GenerateFrame goroutine only.
// Returns 0 when licensed, not active, or no jingle loaded.
func (s *State) NextSample(frames []JingleFrame) float64 {
	if s.licensed || len(frames) == 0 {
		return 0
	}
	if !s.active {
		return 0
	}
	f := frames[s.pos%len(frames)]
	jingleMono := float64(f.L+f.R) / 2.0
	s.pos++
	if s.pos >= len(frames) {
		s.active = false
		s.pos = 0
		s.nextFire = time.Now().Add(time.Duration(JingleIntervalMinutes) * time.Minute)
	}
	return s.jingleLevel * jingleMono
}

// Tick checks whether a new jingle playback should start.
// Call once per chunk (not per sample) from GenerateFrame.
// Safe to call from the single DSP goroutine — no locking needed after init.
func (s *State) Tick() {
	if s.licensed || s.active {
		return
	}
	if time.Now().After(s.nextFire) {
		s.active = true
		s.pos = 0
	}
}

// MixComposite is kept for compatibility; prefer Tick()+NextSample() per sample.
func (s *State) MixComposite(composite float64, frames []JingleFrame, _ time.Time) float64 {
	return composite + s.NextSample(frames)
}

// jingleFrame is a normalised stereo frame from the embedded WAV.
type JingleFrame struct{ L, R float32 }

// LoadJingleFrames decodes the embedded WAV bytes into normalised frames
// and resamples them from jingleWAVRate to targetRate using linear interpolation.
func LoadJingleFrames(wavBytes []byte, targetRate float64) ([]JingleFrame, error) {
	raw, err := decodeWAV(wavBytes)
	if err != nil {
		return nil, fmt.Errorf("license: decode jingle WAV: %w", err)
	}
	if targetRate <= 0 || math.Abs(targetRate-float64(jingleWAVRate)) < 1 {
		return raw, nil
	}
	// Linear resample to composite rate.
	ratio := float64(jingleWAVRate) / targetRate
	dstLen := int(float64(len(raw)) / ratio)
	out := make([]JingleFrame, dstLen)
	for i := range out {
		pos := float64(i) * ratio
		idx := int(pos)
		frac := float32(pos - float64(idx))
		if idx+1 < len(raw) {
			a, b := raw[idx], raw[idx+1]
			out[i] = JingleFrame{
				L: a.L*(1-frac) + b.L*frac,
				R: a.R*(1-frac) + b.R*frac,
			}
		} else if idx < len(raw) {
			out[i] = raw[idx]
		}
	}
	return out, nil
}

// decodeWAV parses a minimal PCM WAV (16-bit stereo) into normalised frames.
func decodeWAV(data []byte) ([]JingleFrame, error) {
	if len(data) < 44 {
		return nil, fmt.Errorf("WAV too short")
	}
	if string(data[0:4]) != "RIFF" || string(data[8:12]) != "WAVE" {
		return nil, fmt.Errorf("not a RIFF/WAVE file")
	}
	// Find fmt and data chunks.
	var (
		channels      uint16
		bitsPerSample uint16
		dataStart     int
		dataLen       int
	)
	i := 12
	for i+8 <= len(data) {
		id := string(data[i : i+4])
		chunkSize := int(binary.LittleEndian.Uint32(data[i+4 : i+8]))
		i += 8
		switch id {
		case "fmt ":
			if chunkSize < 16 || i+16 > len(data) {
				return nil, fmt.Errorf("fmt chunk too small")
			}
			if binary.LittleEndian.Uint16(data[i:i+2]) != 1 {
				return nil, fmt.Errorf("only PCM WAV supported")
			}
			channels = binary.LittleEndian.Uint16(data[i+2 : i+4])
			bitsPerSample = binary.LittleEndian.Uint16(data[i+14 : i+16])
		case "data":
			dataStart = i
			dataLen = chunkSize
		}
		i += chunkSize
		if chunkSize%2 != 0 {
			i++
		}
		if dataStart > 0 && channels > 0 {
			break
		}
	}
	if dataStart == 0 || channels == 0 || bitsPerSample != 16 {
		return nil, fmt.Errorf("unsupported WAV format (need 16-bit PCM, got bits=%d ch=%d)", bitsPerSample, channels)
	}
	if dataStart+dataLen > len(data) {
		dataLen = len(data) - dataStart
	}

	step := int(channels) * 2
	frames := make([]JingleFrame, 0, dataLen/step)
	for j := dataStart; j+step <= dataStart+dataLen; j += step {
		l := float32(int16(binary.LittleEndian.Uint16(data[j:j+2]))) / 32768.0
		r := l
		if channels >= 2 {
			r = float32(int16(binary.LittleEndian.Uint16(data[j+2:j+4]))) / 32768.0
		}
		frames = append(frames, JingleFrame{L: l, R: r})
	}
	return frames, nil
}

// --- Key validation ---

// ValidateKey returns true if key is a valid fm-rds-tx license key.
func ValidateKey(key string) bool {
	key = strings.TrimSpace(key)
	if !strings.HasPrefix(key, "FMRTX-") {
		return false
	}
	body := strings.TrimPrefix(key, "FMRTX-")
	// Decode the base32 payload.
	// Format: BASE32(payload_len_byte || payload_bytes || mac_10_bytes)
	padded := body
	if pad := len(padded) % 8; pad != 0 {
		padded += strings.Repeat("=", 8-pad)
	}
	raw, err := base32.StdEncoding.DecodeString(strings.ToUpper(padded))
	if err != nil || len(raw) < 11 {
		return false
	}
	payloadLen := int(raw[0])
	if payloadLen+1+10 > len(raw) {
		return false
	}
	payload := raw[1 : 1+payloadLen]
	mac := raw[1+payloadLen : 1+payloadLen+10]
	expected := computeMAC(payload)
	return hmac.Equal(mac, expected[:10])
}

// GenerateKey generates a signed license key for the given payload string.
// Call this from cmd/keygen — not from the main binary.
func GenerateKey(payload string) string {
	p := []byte(payload)
	raw := make([]byte, 1+len(p)+10)
	raw[0] = byte(len(p))
	copy(raw[1:], p)
	mac := computeMAC(p)
	copy(raw[1+len(p):], mac[:10])
	encoded := base32.StdEncoding.EncodeToString(raw)
	encoded = strings.TrimRight(encoded, "=")
	return "FMRTX-" + encoded
}

func computeMAC(payload []byte) []byte {
	h := hmac.New(sha256.New, []byte(hmacSecret))
	h.Write(payload)
	return h.Sum(nil)
}
