package stdinpcm

import (
	"context"
	"encoding/binary"
	"fmt"
	"io"
	"sync"
	"sync/atomic"
	"time"

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

type Source struct {
	id          string
	reader      io.Reader
	sampleRate  int
	channels    int
	chunkFrames int

	chunks chan ingest.PCMChunk
	errs   chan error

	cancel context.CancelFunc
	wg     sync.WaitGroup

	state           atomic.Value // string
	chunksIn        atomic.Uint64
	samplesIn       atomic.Uint64
	discontinuities atomic.Uint64
	lastChunkAtUnix atomic.Int64
	lastError       atomic.Value // string
}

func New(id string, reader io.Reader, sampleRate, channels, chunkFrames int) *Source {
	if id == "" {
		id = "stdin"
	}
	if sampleRate <= 0 {
		sampleRate = 44100
	}
	if channels <= 0 {
		channels = 2
	}
	if chunkFrames <= 0 {
		chunkFrames = 1024
	}

	s := &Source{
		id:          id,
		reader:      reader,
		sampleRate:  sampleRate,
		channels:    channels,
		chunkFrames: chunkFrames,
		chunks:      make(chan ingest.PCMChunk, 8),
		errs:        make(chan error, 4),
	}
	s.state.Store("idle")
	return s
}

func (s *Source) Descriptor() ingest.SourceDescriptor {
	return ingest.SourceDescriptor{
		ID:           s.id,
		Kind:         "stdin-pcm",
		Family:       "raw",
		Transport:    "stdin",
		Codec:        "pcm_s16le",
		Channels:     s.channels,
		SampleRateHz: s.sampleRate,
		Detail:       "S16LE interleaved PCM via stdin",
	}
}

func (s *Source) Start(ctx context.Context) error {
	if s.reader == nil {
		return fmt.Errorf("stdin source reader is nil")
	}
	// BUG-2 fix: recreate channels — readLoop() closes them on exit.
	s.chunks = make(chan ingest.PCMChunk, 8)
	s.errs   = make(chan error, 4)
	runCtx, cancel := context.WithCancel(ctx)
	s.cancel = cancel
	s.state.Store("running")

	s.wg.Add(1)
	go s.readLoop(runCtx)
	return nil
}

func (s *Source) Stop() error {
	if s.cancel != nil {
		s.cancel()
	}
	s.wg.Wait()
	s.state.Store("stopped")
	return nil
}

func (s *Source) Chunks() <-chan ingest.PCMChunk { return s.chunks }
func (s *Source) Errors() <-chan error           { return s.errs }

func (s *Source) Stats() ingest.SourceStats {
	state, _ := s.state.Load().(string)
	last := s.lastChunkAtUnix.Load()
	errStr, _ := s.lastError.Load().(string)
	var lastChunkAt time.Time
	if last > 0 {
		lastChunkAt = time.Unix(0, last)
	}
	return ingest.SourceStats{
		State:           state,
		Connected:       state == "running",
		LastChunkAt:     lastChunkAt,
		ChunksIn:        s.chunksIn.Load(),
		SamplesIn:       s.samplesIn.Load(),
		Discontinuities: s.discontinuities.Load(),
		LastError:       errStr,
	}
}

func (s *Source) readLoop(ctx context.Context) {
	defer s.wg.Done()
	defer close(s.errs)
	defer close(s.chunks)

	frameBytes := s.channels * 2
	buf := make([]byte, s.chunkFrames*frameBytes)
	seq := uint64(0)

	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		n, err := io.ReadAtLeast(s.reader, buf, frameBytes)
		if err != nil {
			if err == io.EOF || err == io.ErrUnexpectedEOF {
				if n > 0 {
					s.emitChunk(buf[:n], seq)
				}
				s.state.Store("stopped")
				return
			}
			wrapped := fmt.Errorf("stdin read: %w", err)
			s.lastError.Store(wrapped.Error())
			s.state.Store("failed")
			select {
			case s.errs <- wrapped:
			default:
			}
			return
		}
		s.emitChunk(buf[:n], seq)
		seq++
	}
}

func (s *Source) emitChunk(data []byte, seq uint64) {
	samples := make([]int32, 0, len(data)/2)
	for i := 0; i+1 < len(data); i += 2 {
		v := int16(binary.LittleEndian.Uint16(data[i : i+2]))
		samples = append(samples, int32(v)<<16)
	}
	chunk := ingest.PCMChunk{
		Samples:      samples,
		Channels:     s.channels,
		SampleRateHz: s.sampleRate,
		Sequence:     seq,
		Timestamp:    time.Now(),
		SourceID:     s.id,
	}
	s.chunksIn.Add(1)
	s.samplesIn.Add(uint64(len(samples)))
	s.lastChunkAtUnix.Store(time.Now().UnixNano())
	select {
	case s.chunks <- chunk:
	default:
		s.discontinuities.Add(1)
	}
}
