package cmd

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"strings"
	"sync"
	"sync/atomic"
	"syscall"
	"time"

	"github.com/SurgeDM/Surge/internal/config"
	"github.com/SurgeDM/Surge/internal/core"
	"github.com/SurgeDM/Surge/internal/download"
	"github.com/SurgeDM/Surge/internal/engine/events"
	"github.com/SurgeDM/Surge/internal/engine/state"
	"github.com/SurgeDM/Surge/internal/engine/types"
	"github.com/SurgeDM/Surge/internal/processing"
	"github.com/SurgeDM/Surge/internal/tui"
	"github.com/SurgeDM/Surge/internal/utils"

	tea "charm.land/bubbletea/v2"
	"github.com/google/uuid"
	"github.com/spf13/cobra"
)

// Version information - set via ldflags during build
var (
	Version   = "dev"
	Commit    = "unknown"
	BuildTime = "unknown"
)

func init() {
	if info, ok := debug.ReadBuildInfo(); ok {
		// Override with build info if ldflags didn't inject a version.
		if (Version == "dev" || Version == "") && info.Main.Version != "" && info.Main.Version != "(devel)" {
			Version = strings.TrimPrefix(info.Main.Version, "v")
		}

		if Commit == "" || Commit == "unknown" {
			if rev := buildInfoSetting(info.Settings, "vcs.revision"); rev != "" {
				Commit = rev
			}
		}
	}
}

func buildInfoSetting(settings []debug.BuildSetting, key string) string {
	for _, setting := range settings {
		if setting.Key == key {
			return strings.TrimSpace(setting.Value)
		}
	}
	return ""
}

// activeDownloads tracks in-flight downloads for headless/server exit logic.
var activeDownloads int32

// pendingEnqueue tracks the number of pending batch enqueues to avoid premature exit
var pendingEnqueue int32

var (
	globalHost  string
	globalToken string
)

// Globals for Unified Backend
var (
	GlobalPool              *download.WorkerPool
	GlobalProgressCh        chan any
	GlobalService           core.DownloadService
	GlobalLifecycleCleanup  func()
	serverProgram           *tea.Program
	startupIntegrityMessage string
	globalSettings          *config.Settings
	GlobalLifecycle         *processing.LifecycleManager
	globalLifecycleMu       sync.Mutex
	globalEnqueueCtx        context.Context
	globalEnqueueCancel     context.CancelFunc
	globalEnqueueMu         sync.Mutex
)

func buildPoolIsNameActive(getAll func() []types.DownloadConfig) processing.IsNameActiveFunc {
	if getAll == nil {
		return nil
	}

	return func(dir, name string) bool {
		dir = utils.EnsureAbsPath(strings.TrimSpace(dir))
		name = strings.TrimSpace(name)
		if dir == "" || name == "" {
			return false
		}

		for _, cfg := range getAll() {
			existingName := strings.TrimSpace(cfg.Filename)
			existingDir := strings.TrimSpace(cfg.OutputPath)
			if cfg.DestPath != "" {
				existingDir = filepath.Dir(cfg.DestPath)
				if existingName == "" {
					existingName = filepath.Base(cfg.DestPath)
				}
			}
			if cfg.State != nil {
				if stateName := strings.TrimSpace(cfg.State.GetFilename()); stateName != "" {
					existingName = stateName
				}
				if stateDestPath := strings.TrimSpace(cfg.State.GetDestPath()); stateDestPath != "" {
					existingDir = filepath.Dir(stateDestPath)
					if existingName == "" {
						existingName = filepath.Base(stateDestPath)
					}
				}
			}
			if existingDir == "" || existingName == "" {
				continue
			}
			if utils.EnsureAbsPath(existingDir) == dir && existingName == name {
				return true
			}
		}
		return false
	}
}

func newLocalLifecycleManager(service core.DownloadService, getAll func() []types.DownloadConfig) *processing.LifecycleManager {
	var addFunc processing.AddDownloadFunc
	var addWithIDFunc processing.AddDownloadWithIDFunc
	if service != nil {
		addFunc = service.Add
		addWithIDFunc = service.AddWithID
	}

	return processing.NewLifecycleManager(addFunc, addWithIDFunc, buildPoolIsNameActive(getAll))
}

func startLifecycleEventWorker(service core.DownloadService, mgr *processing.LifecycleManager) (func(), error) {
	if service == nil || mgr == nil {
		return nil, nil
	}

	managerStream, managerCleanup, err := service.StreamEvents(context.Background())
	if err != nil {
		return nil, err
	}
	go mgr.StartEventWorker(managerStream)
	return managerCleanup, nil
}

func currentLifecycle() *processing.LifecycleManager {
	globalLifecycleMu.Lock()
	defer globalLifecycleMu.Unlock()
	return GlobalLifecycle
}

func resetGlobalEnqueueContext() {
	globalEnqueueMu.Lock()
	defer globalEnqueueMu.Unlock()
	if globalEnqueueCancel != nil {
		globalEnqueueCancel()
	}
	globalEnqueueCtx, globalEnqueueCancel = context.WithCancel(context.Background())
}

func ensureEnqueueContextLocked() {
	if globalEnqueueCtx == nil || globalEnqueueCancel == nil {
		globalEnqueueCtx, globalEnqueueCancel = context.WithCancel(context.Background())
	}
}

func currentEnqueueContext() context.Context {
	globalEnqueueMu.Lock()
	defer globalEnqueueMu.Unlock()
	ensureEnqueueContextLocked()
	return globalEnqueueCtx
}

func currentEnqueueCancel() context.CancelFunc {
	globalEnqueueMu.Lock()
	defer globalEnqueueMu.Unlock()
	ensureEnqueueContextLocked()
	return globalEnqueueCancel
}

func cancelGlobalEnqueue() {
	globalEnqueueMu.Lock()
	cancel := globalEnqueueCancel
	globalEnqueueMu.Unlock()
	if cancel != nil {
		cancel()
	}
}

func takeLifecycleCleanup() func() {
	globalLifecycleMu.Lock()
	defer globalLifecycleMu.Unlock()
	cleanup := GlobalLifecycleCleanup
	GlobalLifecycleCleanup = nil
	return cleanup
}

func currentPoolConfigs() []types.DownloadConfig {
	if GlobalPool == nil {
		return nil
	}
	return GlobalPool.GetAll()
}

func lifecycleForLocalService(service core.DownloadService) (*processing.LifecycleManager, error) {
	lifecycle := currentLifecycle()
	if service == nil || GlobalService == nil || service != GlobalService {
		return lifecycle, nil
	}
	return ensureLocalLifecycle(GlobalService, currentPoolConfigs)
}

func ensureGlobalLocalServiceAndLifecycle() error {
	if GlobalService == nil {
		localService := core.NewLocalDownloadServiceWithInput(GlobalPool, GlobalProgressCh)
		GlobalService = localService

		lifecycle, err := ensureLocalLifecycle(localService, currentPoolConfigs)
		if err != nil {
			return err
		}

		lifecycle.SetEngineHooks(processing.EngineHooks{
			Pause:               GlobalPool.Pause,
			ExtractPausedConfig: GlobalPool.ExtractPausedConfig,
			GetStatus:           GlobalPool.GetStatus,
			AddConfig:           GlobalPool.Add,
			Cancel:              GlobalPool.Cancel,
			UpdateURL:           GlobalPool.UpdateURL,
			PublishEvent:        localService.Publish,
		})

		localService.SetLifecycleHooks(core.LifecycleHooks{
			Pause:       lifecycle.Pause,
			Resume:      lifecycle.Resume,
			ResumeBatch: lifecycle.ResumeBatch,
			Cancel:      lifecycle.Cancel,
			UpdateURL:   lifecycle.UpdateURL,
		})
	} else {
		_, err := ensureLocalLifecycle(GlobalService, currentPoolConfigs)
		return err
	}
	return nil
}

func publishSystemLog(message string) {
	if GlobalService != nil {
		_ = GlobalService.Publish(events.SystemLogMsg{Message: message})
		return
	}
	fmt.Fprintln(os.Stderr, message)
}

func recordPreflightDownloadError(url, outPath string, err error) {
	if err == nil || strings.TrimSpace(url) == "" {
		return
	}

	filename := strings.TrimSpace(processing.InferFilenameFromURL(url))
	destPath := ""
	if filename != "" && strings.TrimSpace(outPath) != "" {
		destPath = filepath.Join(outPath, filename)
	}

	entry := types.DownloadEntry{
		ID:       uuid.New().String(),
		URL:      url,
		URLHash:  state.URLHash(url),
		DestPath: destPath,
		Filename: filename,
		Status:   "error",
	}
	if addErr := state.AddToMasterList(entry); addErr != nil {
		utils.Debug("Failed to persist preflight download error for %s: %v", url, addErr)
	}
	if GlobalService != nil {
		_ = GlobalService.Publish(events.DownloadErrorMsg{
			DownloadID: entry.ID,
			Filename:   filename,
			DestPath:   destPath,
			Err:        err,
		})
	}
}

func ensureLocalLifecycle(service core.DownloadService, getAll func() []types.DownloadConfig) (*processing.LifecycleManager, error) {
	globalLifecycleMu.Lock()
	defer globalLifecycleMu.Unlock()

	if GlobalLifecycle == nil {
		GlobalLifecycle = newLocalLifecycleManager(service, getAll)
	}
	if GlobalLifecycleCleanup == nil {
		cleanup, err := startLifecycleEventWorker(service, GlobalLifecycle)
		if err != nil {
			return nil, err
		}
		GlobalLifecycleCleanup = cleanup
	}
	return GlobalLifecycle, nil
}

func isExplicitOutputPath(outPath, defaultDir string) bool {
	return utils.EnsureAbsPath(strings.TrimSpace(outPath)) != utils.EnsureAbsPath(strings.TrimSpace(defaultDir))
}

type rootRunOptions struct {
	portFlag     int
	batchFile    string
	outputDir    string
	noResume     bool
	exitWhenDone bool
}

func readRootRunOptions(cmd *cobra.Command) rootRunOptions {
	portFlag, _ := cmd.Flags().GetInt("port")
	batchFile, _ := cmd.Flags().GetString("batch")
	outputDir, _ := cmd.Flags().GetString("output")
	noResume, _ := cmd.Flags().GetBool("no-resume")
	exitWhenDone, _ := cmd.Flags().GetBool("exit-when-done")

	return rootRunOptions{
		portFlag:     portFlag,
		batchFile:    batchFile,
		outputDir:    outputDir,
		noResume:     noResume,
		exitWhenDone: exitWhenDone,
	}
}

func maybeRunRemoteTUI(cmd *cobra.Command, args []string) (bool, error) {
	hostTarget := resolveHostTarget()
	if hostTarget == "" {
		return false, nil
	}

	if len(args) > 0 {
		return false, fmt.Errorf("URLs cannot be passed when using --host. Use 'surge add <url>' after connecting")
	}

	if err := connectAndRunTUI(cmd, hostTarget); err != nil {
		return false, err
	}
	return true, nil
}

func acquireRootInstanceLock() (func(), error) {
	isMaster, err := AcquireLock()
	if err != nil {
		return nil, fmt.Errorf("error acquiring lock: %w", err)
	}

	if !isMaster {
		return nil, fmt.Errorf("surge is already running. Use 'surge add <url>' to add a download to the active instance")
	}

	return func() {
		if err := ReleaseLock(); err != nil {
			utils.Debug("Error releasing lock: %v", err)
		}
	}, nil
}

func initializeRootLocalRuntime() error {
	if err := initializeGlobalState(); err != nil {
		return err
	}
	resetGlobalEnqueueContext()

	startupIntegrityMessage = runStartupIntegrityCheck()

	if err := ensureGlobalLocalServiceAndLifecycle(); err != nil {
		return fmt.Errorf("error creating lifecycle event stream: %w", err)
	}

	publishStartupWarnings()

	return nil
}

func publishStartupWarnings() {
	if globalSettings == nil || len(globalSettings.StartupWarnings) == 0 {
		return
	}
	for _, warning := range globalSettings.StartupWarnings {
		_ = GlobalService.Publish(events.SystemLogMsg{
			Message: warning,
		})
	}
}

func startRootHTTPServer(opts rootRunOptions) (int, func(), error) {
	port, listener, err := bindServerListener(opts.portFlag)
	if err != nil {
		return 0, nil, err
	}

	saveActivePort(port)
	go startHTTPServer(listener, port, opts.outputDir, GlobalService, "")

	return port, func() {
		removeActivePort()
	}, nil
}

func queueInitialRootDownloads(args []string, opts rootRunOptions) {
	atomic.AddInt32(&pendingEnqueue, 1)
	go func() {
		defer atomic.AddInt32(&pendingEnqueue, -1)
		var urls []string
		urls = append(urls, args...)

		if opts.batchFile != "" {
			fileURLs, err := utils.ReadURLsFromFile(opts.batchFile)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error reading batch file: %v\n", err)
			} else {
				urls = append(urls, fileURLs...)
			}
		}

		if len(urls) > 0 {
			resolvedOutputDir := resolveClientOutputPath(opts.outputDir)
			processDownloads(urls, resolvedOutputDir, 0) // 0 port = internal direct add
		}
	}()
}

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:           "surge [url]...",
	Short:         "Blazing fast TUI download manager built in Go for power users",
	Long:          `Surge is a blazing fast TUI download manager built in Go for power users. Find more info here: https://github.com/SurgeDM/Surge`,
	Version:       Version,
	Args:          cobra.ArbitraryArgs,
	SilenceErrors: true, //errors are printed in main.go this prevents double printing
	SilenceUsage:  true, // prevent usage text from being printed on every error
	PersistentPreRun: func(cmd *cobra.Command, args []string) {
		GlobalProgressCh = make(chan any, 100)
		globalSettings = getSettings()
		GlobalPool = download.NewWorkerPool(GlobalProgressCh, globalSettings.Network.MaxConcurrentDownloads)
	},
	RunE: func(cmd *cobra.Command, args []string) error {
		if ranRemote, err := maybeRunRemoteTUI(cmd, args); err != nil {
			return err
		} else if ranRemote {
			return nil
		}

		releaseLock, err := acquireRootInstanceLock()
		if err != nil {
			return err
		}
		defer releaseLock()

		savePID()
		defer removePID()

		if err := initializeRootLocalRuntime(); err != nil {
			return err
		}

		opts := readRootRunOptions(cmd)
		port, cleanup, err := startRootHTTPServer(opts)
		if err != nil {
			return err
		}
		defer cleanup()

		queueInitialRootDownloads(args, opts)
		return startTUI(port, opts.exitWhenDone, opts.noResume)
	},
}

// startTUI initializes and runs the TUI program
func startTUI(port int, exitWhenDone bool, noResume bool) error {
	tui.InitializeTUI()
	// Initialize TUI
	// GlobalService and GlobalProgressCh are already initialized in PersistentPreRun or Run

	m := tui.InitialRootModel(port, Version, GlobalService, currentLifecycle(), noResume, Commit)
	m = m.WithEnqueueContext(currentEnqueueContext(), currentEnqueueCancel())

	configureServiceUI(&m)

	m.ServerHost = serverBindHost
	if m.ServerHost == "" {
		m.ServerHost = "127.0.0.1"
	}
	m.IsRemote = false

	p := tea.NewProgram(m)
	serverProgram = p // Save reference for HTTP handler

	// Get event stream from service
	stream, cleanup, err := GlobalService.StreamEvents(context.Background())
	if err != nil {
		_ = executeGlobalShutdown("tui: stream init failed")
		return fmt.Errorf("error getting event stream: %w", err)
	}
	defer cleanup()

	// Background listener for progress events
	go func() {
		for msg := range stream {
			p.Send(msg)
		}
	}()

	if startupIntegrityMessage != "" && GlobalService != nil {
		_ = GlobalService.Publish(events.SystemLogMsg{
			Message: startupIntegrityMessage,
		})
		startupIntegrityMessage = ""
	}

	// Exit-when-done checker for TUI
	if exitWhenDone {
		go func() {
			// Wait a bit for initial downloads to be queued
			time.Sleep(3 * time.Second)
			ticker := time.NewTicker(2 * time.Second)
			defer ticker.Stop()
			for range ticker.C {
				if atomic.LoadInt32(&pendingEnqueue) == 0 && GlobalPool != nil && GlobalPool.ActiveCount() == 0 {
					// Send quit message to TUI
					p.Send(tea.Quit())
					return
				}
			}
		}()
	}

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
	defer signal.Stop(sigChan)

	stopSignalListener := make(chan struct{})
	defer close(stopSignalListener)

	go func() {
		select {
		case sig := <-sigChan:
			_ = executeGlobalShutdown(fmt.Sprintf("tui signal: %s", sig))
			p.Send(tea.Quit())
		case <-stopSignalListener:
			return
		}
	}()

	// Run TUI
	finalModel, err := p.Run()
	if err != nil {
		_ = executeGlobalShutdown("tui: p.Run failed")
		return fmt.Errorf("error running program: %w", err)
	}
	_ = executeGlobalShutdown("tui: program exited")

	// Check if restart was requested (e.g. from settings changed)
	if m, ok := finalModel.(tui.RootModel); ok && m.RestartRequested {
		return performRestart()
	}

	return nil
}

func performRestart() error {
	executable, err := os.Executable()
	if err != nil {
		return fmt.Errorf("could not get executable path: %w", err)
	}

	if runtime.GOOS == "windows" {
		_ = ReleaseLock()
	}

	return utils.Run(executable, os.Args, os.Environ())
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() error {
	return RunService()
}

func init() {
	rootCmd.PersistentFlags().StringVar(&globalHost, "host", "", "Server host to connect/control (or set SURGE_HOST), e.g. 127.0.0.1:1700")
	rootCmd.PersistentFlags().StringVar(&globalToken, "token", "", "Bearer token (or set SURGE_TOKEN)")
	rootCmd.PersistentFlags().BoolVar(&globalInsecureHTTP, "insecure-http", false, "Allow plain HTTP for non-loopback remote targets")
	rootCmd.PersistentFlags().BoolVar(&globalInsecureTLS, "insecure-tls", false, "Skip TLS certificate verification for remote targets")
	rootCmd.PersistentFlags().StringVar(&globalTLSCAFile, "tls-ca-file", "", "PEM bundle to trust for remote HTTPS targets")
	rootCmd.Flags().StringP("batch", "b", "", "File containing URLs to download (one per line)")
	rootCmd.Flags().IntP("port", "p", 0, "Port to listen on (default: 8080 or first available)")
	rootCmd.Flags().StringP("output", "o", "", "Output directory (defaults to current working directory)")
	rootCmd.Flags().Bool("no-resume", false, "Do not auto-resume paused downloads on startup")
	rootCmd.Flags().Bool("exit-when-done", false, "Exit when all downloads complete")
	rootCmd.SetVersionTemplate("Surge v{{.Version}}\n")
	rootCmd.Version = Version
}
