package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	apppkg "github.com/jan/fm-rds-tx/internal/app"
	"github.com/jan/fm-rds-tx/internal/audio"
	cfgpkg "github.com/jan/fm-rds-tx/internal/config"
	ctrlpkg "github.com/jan/fm-rds-tx/internal/control"
	drypkg "github.com/jan/fm-rds-tx/internal/dryrun"
	"github.com/jan/fm-rds-tx/internal/ingest"
	"github.com/jan/fm-rds-tx/internal/ingest/adapters/icecast"
	ingestfactory "github.com/jan/fm-rds-tx/internal/ingest/factory"
	"github.com/jan/fm-rds-tx/internal/license"
	"github.com/jan/fm-rds-tx/internal/platform"
	"github.com/jan/fm-rds-tx/internal/platform/plutosdr"
	"github.com/jan/fm-rds-tx/internal/platform/soapysdr"
)

func main() {
	configPath := flag.String("config", "", "path to JSON config")
	printConfig := flag.Bool("print-config", false, "print effective config and exit")
	dryRun := flag.Bool("dry-run", false, "run no-hardware dry-run output")
	dryOutput := flag.String("dry-output", "-", "dry-run output path or - for stdout")
	simulate := flag.Bool("simulate-tx", false, "run simulated Soapy/backend transmit path")
	simulateOutput := flag.String("simulate-output", "", "simulated transmit output file")
	simulateDuration := flag.Duration("simulate-duration", 500*time.Millisecond, "simulated transmit duration")
	txMode := flag.Bool("tx", false, "start real TX mode (requires hardware + build tags)")
	txAutoStart := flag.Bool("tx-auto-start", false, "auto-start TX on launch")
	licenseKey := flag.String("license", "", "fm-rds-tx license key (omit for evaluation mode with jingle)")
	listDevices := flag.Bool("list-devices", false, "enumerate SoapySDR devices and exit")
	audioStdin := flag.Bool("audio-stdin", false, "read S16LE stereo PCM audio from stdin")
	audioRate := flag.Int("audio-rate", 44100, "sample rate of stdin audio input (Hz)")
	audioHTTP := flag.Bool("audio-http", false, "enable HTTP audio ingest via /audio/stream")
	flag.Parse()

	if *listDevices {
		devices, err := soapysdr.Enumerate()
		if err != nil {
			log.Fatalf("enumerate: %v", err)
		}
		if len(devices) == 0 {
			fmt.Println("no SoapySDR devices found")
			return
		}
		for i, dev := range devices {
			fmt.Printf("device %d:\n", i)
			for k, v := range dev {
				fmt.Printf("  %s = %s\n", k, v)
			}
		}
		return
	}

	cfg, err := cfgpkg.Load(*configPath)
	if err != nil {
		log.Fatalf("load config: %v", err)
	}

	if *printConfig {
		preemph := "off"
		if cfg.FM.PreEmphasisTauUS > 0 {
			preemph = fmt.Sprintf("%.0fus", cfg.FM.PreEmphasisTauUS)
		}
		fmt.Printf("backend=%s freq=%.1fMHz stereo=%t rds=%t preemph=%s limiter=%t fmmod=%t deviation=+-%.0fHz compositeRate=%dHz deviceRate=%.0fHz listen=%s pluto=%t soapy=%t\n",
			cfg.Backend.Kind, cfg.FM.FrequencyMHz, cfg.FM.StereoEnabled, cfg.RDS.Enabled,
			preemph, cfg.FM.LimiterEnabled, cfg.FM.FMModulationEnabled, cfg.FM.MaxDeviationHz,
			cfg.FM.CompositeRateHz, cfg.EffectiveDeviceRate(), cfg.Control.ListenAddress,
			plutosdr.Available(), soapysdr.Available())
		return
	}

	if *dryRun {
		frame := drypkg.Generate(cfg)
		if err := drypkg.WriteJSON(*dryOutput, frame); err != nil {
			log.Fatalf("dry-run: %v", err)
		}
		if *dryOutput != "" && *dryOutput != "-" {
			fmt.Fprintf(os.Stderr, "dry run frame written to %s\n", *dryOutput)
		}
		return
	}

	if *simulate {
		summary, err := apppkg.RunSimulatedTransmit(cfg, *simulateOutput, *simulateDuration)
		if err != nil {
			log.Fatalf("simulate-tx: %v", err)
		}
		fmt.Println(summary)
		return
	}

	if *txMode {
		driver := selectDriver(cfg)
		if driver == nil {
			log.Fatal("no hardware driver available - build with -tags pluto (or -tags soapy)")
		}
		runTXMode(cfg, *configPath, driver, *txAutoStart, *audioStdin, *audioRate, *audioHTTP, *licenseKey)
		return
	}

	srv := ctrlpkg.NewServer(cfg)
	configureControlPlanePersistence(srv, *configPath, nil)
	server := ctrlpkg.NewHTTPServer(cfg, srv.Handler())
	log.Printf("fm-rds-tx listening on %s (TX default: off, use --tx for hardware)", server.Addr)
	log.Fatal(server.ListenAndServe())
}

func selectDriver(cfg cfgpkg.Config) platform.SoapyDriver {
	kind := cfg.Backend.Kind

	if kind == "pluto" || kind == "plutosdr" {
		if plutosdr.Available() {
			return plutosdr.NewPlutoDriver()
		}
		log.Printf("warning: backend=%s but pluto driver not available (%s)", kind, plutosdr.AvailableError())
	}

	if kind == "soapy" || kind == "soapysdr" {
		if soapysdr.Available() {
			return soapysdr.NewNativeDriver()
		}
		log.Printf("warning: backend=%s but soapy driver not available", kind)
	}

	if plutosdr.Available() {
		log.Println("auto-selected: pluto-iio driver")
		return plutosdr.NewPlutoDriver()
	}
	if soapysdr.Available() {
		log.Println("auto-selected: soapy-native driver")
		return soapysdr.NewNativeDriver()
	}

	return nil
}

func runTXMode(cfg cfgpkg.Config, configPath string, driver platform.SoapyDriver, autoStart bool, audioStdin bool, audioRate int, audioHTTP bool, licenseKey string) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	soapyCfg := platform.SoapyConfig{
		Driver:       cfg.Backend.Driver,
		Device:       cfg.Backend.Device,
		CenterFreqHz: cfg.FM.FrequencyMHz * 1e6,
		GainDB:       0,
		DeviceArgs:   map[string]string{},
	}
	if cfg.Backend.URI != "" {
		soapyCfg.DeviceArgs["uri"] = cfg.Backend.URI
	}
	for k, v := range cfg.Backend.DeviceArgs {
		soapyCfg.DeviceArgs[k] = v
	}
	soapyCfg.SampleRateHz = cfg.EffectiveDeviceRate()

	log.Printf("TX: configuring %s freq=%.3fMHz rate=%.0fHz gain=%.1fdB",
		driver.Name(), cfg.FM.FrequencyMHz, soapyCfg.SampleRateHz, soapyCfg.GainDB)

	if err := driver.Configure(ctx, soapyCfg); err != nil {
		log.Fatalf("configure: %v", err)
	}

	caps, err := driver.Capabilities(ctx)
	if err == nil {
		log.Printf("TX: device caps: gain=%.0f..%.0f dB, rate=%.0f..%.0f Hz",
			caps.GainMinDB, caps.GainMaxDB, caps.MinSampleRate, caps.MaxSampleRate)
	}

	engine := apppkg.NewEngine(cfg, driver)

	// License setup.
	licState := license.NewState(licenseKey)
	if licState.Licensed() {
		log.Println("license: valid key — evaluation jingle disabled")
	} else {
		log.Printf("license: no valid key — evaluation jingle every %d minutes", license.JingleIntervalMinutes)
	}
	engine.SetLicenseState(licState, licenseKey)
	engine.ConfigureWatermark(cfg.FM.WatermarkEnabled, licenseKey)
	cfg = applyLegacyAudioFlags(cfg, audioStdin, audioRate, audioHTTP)

	var streamSrc *audio.StreamSource
	var ingestRuntime *ingest.Runtime
	var ingress ctrlpkg.AudioIngress
	if ingestEnabled(cfg.Ingest.Kind) {
		rate := ingestfactory.SampleRateForKind(cfg)
		bufferFrames := rate * 2
		if bufferFrames <= 0 {
			bufferFrames = 1
		}
		streamSrc = audio.NewStreamSource(bufferFrames, rate)
		engine.SetStreamSource(streamSrc)

		source, sourceIngress, err := ingestfactory.BuildSource(ctx, cfg, ingestfactory.Deps{Stdin: os.Stdin})
		if err != nil {
			log.Fatalf("ingest source: %v", err)
		}
		runtimeOpts := []ingest.RuntimeOption{}
		runtimeOpts = append(runtimeOpts, ingest.WithPrebufferMs(cfg.Ingest.PrebufferMs))
		if cfg.Ingest.Icecast.RadioText.Enabled {
			relay := icecast.NewRadioTextRelay(
				icecast.RadioTextOptions{
					Enabled:      true,
					Prefix:       cfg.Ingest.Icecast.RadioText.Prefix,
					MaxLen:       cfg.Ingest.Icecast.RadioText.MaxLen,
					OnlyOnChange: cfg.Ingest.Icecast.RadioText.OnlyOnChange,
				},
				cfg.RDS.RadioText,
				func(rt string) error {
					return engine.UpdateConfig(apppkg.LiveConfigUpdate{RadioText: &rt})
				},
			)
			runtimeOpts = append(runtimeOpts, ingest.WithStreamTitleHandler(func(streamTitle string) {
				if err := relay.HandleStreamTitle(streamTitle); err != nil {
					log.Printf("ingest: failed to forward StreamTitle to RDS RadioText: %v", err)
				}
			}))
			log.Printf(
				"ingest: ICY StreamTitle->RDS enabled (maxLen=%d onlyOnChange=%t prefix=%q)",
				cfg.Ingest.Icecast.RadioText.MaxLen,
				cfg.Ingest.Icecast.RadioText.OnlyOnChange,
				cfg.Ingest.Icecast.RadioText.Prefix,
			)
		}
		ingestRuntime = ingest.NewRuntime(streamSrc, source, runtimeOpts...)
		if err := ingestRuntime.Start(ctx); err != nil {
			log.Fatalf("ingest start: %v", err)
		}
		ingress = sourceIngress
		log.Printf("ingest: kind=%s rate=%dHz buffer=%d frames", cfg.Ingest.Kind, rate, streamSrc.Stats().Capacity)
	}

	srv := ctrlpkg.NewServer(cfg)
	configureControlPlanePersistence(srv, configPath, cancel)
	srv.SetDriver(driver)
	srv.SetTXController(&txBridge{engine: engine})
	if streamSrc != nil {
		srv.SetStreamSource(streamSrc)
	}
	if ingress != nil {
		srv.SetAudioIngress(ingress)
	}
	if ingestRuntime != nil {
		srv.SetIngestRuntime(ingestRuntime)
	}

	if autoStart {
		log.Println("TX: auto-start enabled")
		if err := engine.Start(ctx); err != nil {
			log.Fatalf("engine start: %v", err)
		}
		log.Printf("TX ACTIVE: freq=%.3fMHz rate=%.0fHz", cfg.FM.FrequencyMHz, cfg.EffectiveDeviceRate())
	} else {
		log.Println("TX ready (idle) - POST /tx/start to begin")
	}

	ctrlServer := ctrlpkg.NewHTTPServer(cfg, srv.Handler())
	go func() {
		log.Printf("control plane on %s (read=%s write=%s idle=%s)", ctrlServer.Addr, ctrlServer.ReadTimeout, ctrlServer.WriteTimeout, ctrlServer.IdleTimeout)
		if err := ctrlServer.ListenAndServe(); err != nil {
			log.Printf("http: %v", err)
		}
	}()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	sig := <-sigCh
	log.Printf("received %s, shutting down...", sig)

	_ = engine.Stop(ctx)
	if ingestRuntime != nil {
		_ = ingestRuntime.Stop()
	}
	_ = driver.Close(ctx)
	log.Println("shutdown complete")
}

func configureControlPlanePersistence(srv *ctrlpkg.Server, configPath string, cancel context.CancelFunc) {
	if strings.TrimSpace(configPath) == "" {
		return
	}
	srv.SetConfigSaver(func(next cfgpkg.Config) error {
		return cfgpkg.Save(configPath, next)
	})
	srv.SetHardReload(func() {
		// BUG-5 fix: cancel the app context instead of os.Exit(0).
		// os.Exit skips all defers, Flush/Stop calls, and driver cleanup.
		// Cancelling ctx lets the normal shutdown sequence run: engine.Stop,
		// ingestRuntime.Stop, driver.Close — then the process exits naturally.
		// The supervisor (systemd etc.) will restart the process as intended.
		log.Printf("control: hard reload — cancelling app context for clean restart")
		if cancel != nil {
			cancel()
		}
	})
}

func ingestEnabled(kind string) bool {
	normalized := strings.ToLower(strings.TrimSpace(kind))
	return normalized != "" && normalized != "none"
}

func applyLegacyAudioFlags(cfg cfgpkg.Config, audioStdin bool, audioRate int, audioHTTP bool) cfgpkg.Config {
	if audioRate > 0 {
		cfg.Ingest.Stdin.SampleRateHz = audioRate
		cfg.Ingest.HTTPRaw.SampleRateHz = audioRate
	}
	if audioStdin && audioHTTP {
		log.Printf("audio: both --audio-stdin and --audio-http set; using ingest kind=stdin")
	}
	if audioStdin {
		cfg.Ingest.Kind = "stdin"
	}
	if audioHTTP && !audioStdin {
		cfg.Ingest.Kind = "http-raw"
	}
	return cfg
}

type txBridge struct{ engine *apppkg.Engine }

func (b *txBridge) StartTX() error { return b.engine.Start(context.Background()) }
func (b *txBridge) StopTX() error  { return b.engine.Stop(context.Background()) }
func (b *txBridge) TXStats() map[string]any {
	s := b.engine.Stats()
	return map[string]any{
		"runtimeStateDurationSeconds": s.RuntimeStateDurationSeconds,
		"state":                       s.State,
		"chunksProduced":              s.ChunksProduced,
		"totalSamples":                s.TotalSamples,
		"underruns":                   s.Underruns,
		"lateBuffers":                 s.LateBuffers,
		"lastError":                   s.LastError,
		"uptimeSeconds":               s.UptimeSeconds,
		"maxCycleMs":                  s.MaxCycleMs,
		"maxGenerateMs":               s.MaxGenerateMs,
		"maxUpsampleMs":               s.MaxUpsampleMs,
		"maxWriteMs":                  s.MaxWriteMs,
		"queue":                       s.Queue,
		"runtimeIndicator":            s.RuntimeIndicator,
		"runtimeAlert":                s.RuntimeAlert,
		"appliedFrequencyMHz":         s.AppliedFrequencyMHz,
		"activePS":                    s.ActivePS,
		"activeRadioText":             s.ActiveRadioText,
		"measurement":                 s.Measurement,
		"degradedTransitions":         s.DegradedTransitions,
		"mutedTransitions":            s.MutedTransitions,
		"faultedTransitions":          s.FaultedTransitions,
		"faultCount":                  s.FaultCount,
		"faultHistory":                s.FaultHistory,
		"transitionHistory":           s.TransitionHistory,
		"lastFault":                   s.LastFault,
	}
}
func (b *txBridge) UpdateConfig(lp ctrlpkg.LivePatch) error {
	return b.engine.UpdateConfig(apppkg.LiveConfigUpdate{
		FrequencyMHz:            lp.FrequencyMHz,
		OutputDrive:             lp.OutputDrive,
		StereoEnabled:           lp.StereoEnabled,
		StereoMode:              lp.StereoMode,
		PilotLevel:              lp.PilotLevel,
		RDSInjection:            lp.RDSInjection,
		RDSEnabled:              lp.RDSEnabled,
		LimiterEnabled:          lp.LimiterEnabled,
		LimiterCeiling:          lp.LimiterCeiling,
		PS:                      lp.PS,
		RadioText:               lp.RadioText,
		TA:                      lp.TA,
		TP:                      lp.TP,
		ToneLeftHz:              lp.ToneLeftHz,
		ToneRightHz:             lp.ToneRightHz,
		ToneAmplitude:           lp.ToneAmplitude,
		AudioGain:               lp.AudioGain,
		CompositeClipperEnabled: lp.CompositeClipperEnabled,
	})
}

func (b *txBridge) ResetFault() error {
	return b.engine.ResetFault()
}
