package srt

import (
	"context"
	"fmt"
	"io"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"time"

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

type Option func(*Source)

func WithConnOpener(opener aoiprxkit.SRTConnOpener) Option {
	return func(s *Source) {
		if opener != nil {
			s.opener = opener
		}
	}
}

type Source struct {
	id  string
	cfg aoiprxkit.SRTConfig

	opener aoiprxkit.SRTConnOpener

	chunks chan ingest.PCMChunk
	errs   chan error

	cancel context.CancelFunc
	wg     sync.WaitGroup

	mu        sync.Mutex
	rx        *aoiprxkit.SRTReceiver
	started   atomic.Bool
	closeOnce sync.Once

	state           atomic.Value // string
	connected       atomic.Bool
	chunksIn        atomic.Uint64
	samplesIn       atomic.Uint64
	overflows       atomic.Uint64
	discontinuities atomic.Uint64
	transportLoss   atomic.Uint64
	reorders        atomic.Uint64
	lastChunkAtUnix atomic.Int64
	lastError       atomic.Value // string
	nextSeq         atomic.Uint64

	seqMu      sync.Mutex
	lastFrame  uint16
	lastHasVal bool
}

func New(id string, cfg aoiprxkit.SRTConfig, opts ...Option) *Source {
	if id == "" {
		id = "srt-main"
	}
	if cfg.Mode == "" {
		cfg.Mode = "listener"
	}
	if cfg.SampleRateHz <= 0 {
		cfg.SampleRateHz = 48000
	}
	if cfg.Channels <= 0 {
		cfg.Channels = 2
	}

	s := &Source{
		id:     id,
		cfg:    cfg,
		chunks: make(chan ingest.PCMChunk, 64),
		errs:   make(chan error, 8),
	}
	for _, opt := range opts {
		if opt != nil {
			opt(s)
		}
	}
	s.state.Store("idle")
	s.lastError.Store("")
	return s
}

func (s *Source) Descriptor() ingest.SourceDescriptor {
	return ingest.SourceDescriptor{
		ID:           s.id,
		Kind:         "srt",
		Family:       "aoip",
		Transport:    "srt",
		Codec:        "pcm_s32le",
		Channels:     s.cfg.Channels,
		SampleRateHz: s.cfg.SampleRateHz,
		Detail:       s.cfg.URL,
		Origin: &ingest.SourceOrigin{
			Kind:     "url",
			Endpoint: redactURL(s.cfg.URL),
			Mode:     strings.TrimSpace(s.cfg.Mode),
		},
	}
}

func (s *Source) Start(ctx context.Context) error {
	if !s.started.CompareAndSwap(false, true) {
		return nil
	}
	// BUG-2 fix: recreate channels and reset closeOnce so Stop+Start works.
	// closeChannels() uses sync.Once — reset it so the new channels can be closed.
	s.chunks    = make(chan ingest.PCMChunk, 64)
	s.errs      = make(chan error, 8)
	s.closeOnce = sync.Once{}

	var (
		rx  *aoiprxkit.SRTReceiver
		err error
	)
	if s.opener != nil {
		rx, err = aoiprxkit.NewSRTReceiverWithOpener(s.cfg, s.opener, s.handleFrame)
	} else {
		rx, err = aoiprxkit.NewSRTReceiver(s.cfg, s.handleFrame)
	}
	if err != nil {
		s.started.Store(false)
		s.connected.Store(false)
		s.state.Store("failed")
		s.setError(err)
		return err
	}

	runCtx, cancel := context.WithCancel(ctx)
	s.cancel = cancel
	s.mu.Lock()
	s.rx = rx
	s.mu.Unlock()
	s.lastError.Store("")
	s.connected.Store(false)
	s.state.Store("connecting")

	if err := rx.Start(runCtx); err != nil {
		s.started.Store(false)
		s.connected.Store(false)
		s.state.Store("failed")
		s.setError(err)
		return err
	}
	s.connected.Store(true)
	s.state.Store("running")

	s.wg.Add(1)
	go func() {
		defer s.wg.Done()
		<-runCtx.Done()
		_ = s.stopReceiver()
		s.connected.Store(false)
		s.closeChannels()
	}()
	return nil
}

func (s *Source) Stop() error {
	if !s.started.CompareAndSwap(true, false) {
		return nil
	}
	if s.cancel != nil {
		s.cancel()
	}
	if err := s.stopReceiver(); err != nil {
		s.setError(err)
		s.state.Store("failed")
	}
	s.wg.Wait()
	s.connected.Store(false)
	state, _ := s.state.Load().(string)
	if state != "failed" {
		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:       s.connected.Load(),
		LastChunkAt:     lastChunkAt,
		ChunksIn:        s.chunksIn.Load(),
		SamplesIn:       s.samplesIn.Load(),
		Overflows:       s.overflows.Load(),
		Discontinuities: s.discontinuities.Load(),
		TransportLoss:   s.transportLoss.Load(),
		Reorders:        s.reorders.Load(),
		LastError:       errStr,
	}
}

func (s *Source) handleFrame(frame aoiprxkit.PCMFrame) {
	if !s.started.Load() {
		return
	}

	discontinuity := false
	s.seqMu.Lock()
	if s.lastHasVal {
		expected := s.lastFrame + 1
		if frame.SequenceNumber != expected {
			discontinuity = true
			delta := int16(frame.SequenceNumber - expected)
			if delta > 0 {
				s.transportLoss.Add(uint64(delta))
			} else {
				s.reorders.Add(1)
			}
		}
	}
	s.lastFrame = frame.SequenceNumber
	s.lastHasVal = true
	s.seqMu.Unlock()

	chunk := ingest.PCMChunk{
		Samples:       append([]int32(nil), frame.Samples...),
		Channels:      frame.Channels,
		SampleRateHz:  frame.SampleRateHz,
		Sequence:      s.nextSeq.Add(1) - 1,
		Timestamp:     frame.ReceivedAt,
		SourceID:      s.id,
		Discontinuity: discontinuity,
	}

	s.chunksIn.Add(1)
	s.samplesIn.Add(uint64(len(chunk.Samples)))
	s.lastChunkAtUnix.Store(time.Now().UnixNano())
	if discontinuity {
		s.discontinuities.Add(1)
	}

	select {
	case s.chunks <- chunk:
	default:
		s.overflows.Add(1)
		s.discontinuities.Add(1)
		s.setError(io.ErrShortBuffer)
		s.emitError(fmt.Errorf("srt chunk buffer overflow"))
	}
}

func (s *Source) stopReceiver() error {
	s.mu.Lock()
	rx := s.rx
	s.rx = nil
	s.mu.Unlock()
	if rx == nil {
		return nil
	}
	return rx.Stop()
}

func (s *Source) closeChannels() {
	s.closeOnce.Do(func() {
		close(s.chunks)
		close(s.errs)
	})
}

func (s *Source) setError(err error) {
	if err == nil {
		return
	}
	s.lastError.Store(err.Error())
	s.emitError(err)
}

func (s *Source) emitError(err error) {
	if err == nil {
		return
	}
	select {
	case s.errs <- err:
	default:
	}
}

func redactURL(raw string) string {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return ""
	}
	u, err := url.Parse(trimmed)
	if err != nil || u.Host == "" {
		return trimmed
	}
	u.User = nil
	u.RawQuery = ""
	u.Fragment = ""
	return u.String()
}
