package ingest

import (
	"context"
	"sync"
	"sync/atomic"
	"time"

	"github.com/jan/fm-rds-tx/internal/audio"
)

type Runtime struct {
	sink    *audio.StreamSource
	source  Source
	started atomic.Bool
	onTitle func(string)

	ctx    context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup

	mu     sync.RWMutex
	active SourceDescriptor
	stats  RuntimeStats
}

type RuntimeOption func(*Runtime)

func WithStreamTitleHandler(handler func(string)) RuntimeOption {
	return func(r *Runtime) {
		r.onTitle = handler
	}
}

func NewRuntime(sink *audio.StreamSource, src Source, opts ...RuntimeOption) *Runtime {
	r := &Runtime{
		sink:   sink,
		source: src,
		stats: RuntimeStats{
			State: "idle",
		},
	}
	for _, opt := range opts {
		if opt != nil {
			opt(r)
		}
	}
	return r
}

func (r *Runtime) Start(ctx context.Context) error {
	if r.sink == nil {
		r.mu.Lock()
		r.stats.State = "failed"
		r.mu.Unlock()
		return nil
	}
	if r.source == nil {
		r.mu.Lock()
		r.stats.State = "idle"
		r.mu.Unlock()
		return nil
	}
	if !r.started.CompareAndSwap(false, true) {
		return nil
	}

	r.ctx, r.cancel = context.WithCancel(ctx)
	r.mu.Lock()
	r.active = r.source.Descriptor()
	r.stats.State = "starting"
	r.mu.Unlock()
	if err := r.source.Start(r.ctx); err != nil {
		r.started.Store(false)
		r.mu.Lock()
		r.stats.State = "failed"
		r.mu.Unlock()
		return err
	}

	r.wg.Add(1)
	go r.run()
	return nil
}

func (r *Runtime) Stop() error {
	if !r.started.CompareAndSwap(true, false) {
		return nil
	}
	if r.cancel != nil {
		r.cancel()
	}
	if r.source != nil {
		_ = r.source.Stop()
	}
	r.wg.Wait()
	r.mu.Lock()
	r.stats.State = "stopped"
	r.mu.Unlock()
	return nil
}

func (r *Runtime) run() {
	defer r.wg.Done()
	r.mu.Lock()
	r.stats.State = "running"
	r.mu.Unlock()

	ch := r.source.Chunks()
	errCh := r.source.Errors()
	var titleCh <-chan string
	if src, ok := r.source.(StreamTitleSource); ok && r.onTitle != nil {
		titleCh = src.StreamTitleUpdates()
	}
	for {
		select {
		case <-r.ctx.Done():
			return
		case err, ok := <-errCh:
			if !ok {
				errCh = nil
				continue
			}
			if err == nil {
				continue
			}
			r.mu.Lock()
			r.stats.State = "degraded"
			r.mu.Unlock()
		case chunk, ok := <-ch:
			if !ok {
				r.mu.Lock()
				r.stats.State = "stopped"
				r.mu.Unlock()
				return
			}
			r.handleChunk(chunk)
		case title, ok := <-titleCh:
			if !ok {
				titleCh = nil
				continue
			}
			r.onTitle(title)
		}
	}
}

func (r *Runtime) handleChunk(chunk PCMChunk) {
	frames, err := ChunkToFrames(chunk)
	if err != nil {
		r.mu.Lock()
		r.stats.ConvertErrors++
		r.stats.State = "degraded"
		r.mu.Unlock()
		return
	}
	dropped := uint64(0)
	for _, frame := range frames {
		if !r.sink.WriteFrame(frame) {
			dropped++
		}
	}
	r.mu.Lock()
	r.stats.State = "running"
	r.stats.LastChunkAt = time.Now()
	r.stats.DroppedFrames += dropped
	r.stats.WriteBlocked = dropped > 0
	r.mu.Unlock()
}

func (r *Runtime) Stats() Stats {
	r.mu.RLock()
	runtimeStats := r.stats
	active := r.active
	r.mu.RUnlock()

	sourceStats := SourceStats{}
	if r.source != nil {
		sourceStats = r.source.Stats()
	}
	return Stats{
		Active:  active,
		Source:  sourceStats,
		Runtime: runtimeStats,
	}
}
