package cli

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/kardianos/service"
	"github.com/spf13/cobra"

	"github.com/Control-D-Inc/ctrld"
)

// Start implements the logic from cmdStart.Run
func (sc *ServiceCommand) Start(cmd *cobra.Command, args []string) error {
	logger := mainLog.Load()
	logger.Debug().Msg("Service start command started")

	firewallModeFlagChanged = cmd.Flags().Changed("firewall-mode")
	checkStrFlagEmpty(cmd, cdUidFlagName)
	checkStrFlagEmpty(cmd, cdOrgFlagName)
	validateCdAndNextDNSFlags()

	svcConfig := sc.createServiceConfig()
	osArgs := os.Args[2:]
	osArgs = filterEmptyStrings(osArgs)
	if os.Args[1] == "service" {
		osArgs = os.Args[3:]
	}
	setDependencies(svcConfig)
	svcConfig.Arguments = append([]string{"run"}, osArgs...)

	// Validate --intercept-mode early, before installing the service.
	// Without this, a typo like "--intercept-mode fds" would install the service,
	// the child process would Fatal() on the invalid value, and the parent would
	// then uninstall — confusing and destructive.
	if interceptMode != "" && !validInterceptMode(interceptMode) {
		logger.Fatal().Msgf("invalid --intercept-mode value %q: must be 'off', 'dns', or 'hard'", interceptMode)
	}
	if firewallModeFlagChanged && !validFirewallMode(firewallMode) {
		logger.Fatal().Msgf("invalid --firewall-mode value %q: must be 'off' or 'on'", firewallMode)
	}

	// Initialize service manager with proper configuration
	s, p, err := sc.initializeServiceManagerWithServiceConfig(svcConfig)
	if err != nil {
		logger.Error().Err(err).Msg("Failed to initialize service manager")
		return err
	}

	p.cfg = &cfg
	p.preRun()

	status, err := s.Status()
	isCtrldRunning := status == service.StatusRunning
	isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled)

	// Get current running iface, if any.
	var currentIface *ifaceResponse

	// Handle "ctrld start --intercept-mode dns|hard" on an existing
	// service BEFORE the pin check. Adding intercept mode is an enhancement, not
	// deactivation, so it doesn't require the deactivation pin. We modify the
	// plist/registry directly and restart the service via the OS service manager.
	osArgsEarly := os.Args[2:]
	if os.Args[1] == "service" {
		osArgsEarly = os.Args[3:]
	}
	osArgsEarly = filterEmptyStrings(osArgsEarly)
	interceptOnly := onlyInterceptFlags(osArgsEarly)
	svcExists := serviceConfigFileExists()
	logger.Debug().Msgf("intercept upgrade check: args=%v interceptOnly=%v svcConfigExists=%v interceptMode=%q", osArgsEarly, interceptOnly, svcExists, interceptMode)
	if interceptOnly && svcExists {
		// Remove any existing intercept flags before applying the new value.
		_ = removeServiceFlag("--intercept-mode")

		if interceptMode == "off" {
			// "off" = remove intercept mode entirely (just the removal above).
			logger.Notice().Msg("Existing service detected — removing --intercept-mode from service arguments")
		} else {
			// Add the new mode value.
			logger.Notice().Msgf("Existing service detected — appending --intercept-mode %s to service arguments", interceptMode)
			if err := appendServiceFlag("--intercept-mode"); err != nil {
				logger.Fatal().Err(err).Msg("failed to append intercept flag to service arguments")
			}
			if err := appendServiceFlag(interceptMode); err != nil {
				logger.Fatal().Err(err).Msg("failed to append intercept mode value to service arguments")
			}
		}

		// Stop the service if running (bypasses ctrld pin — this is an
		// enhancement, not deactivation). Then fall through to the normal
		// startOnly path which handles start, self-check, and reporting.
		if isCtrldRunning {
			logger.Notice().Msg("Stopping service for intercept mode upgrade")
			_ = s.Stop()
			isCtrldRunning = false
		}
		startOnly = true
		isCtrldInstalled = true
		// Fall through to startOnly path below.
	}

	// If pin code was set, do not allow running start command.
	if isCtrldRunning {
		if err := checkDeactivationPin(s, nil); isCheckDeactivationPinErr(err) {
			logger.Error().Msg("Deactivation pin check failed")
			os.Exit(deactivationPinInvalidExitCode)
		}
		currentIface = runningIface(s)
		logger.Debug().Msgf("Current interface on start: %v", currentIface)
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	reportSetDnsOk := func(sockDir string) {
		if cc := newSocketControlClient(ctx, s, sockDir); cc != nil {
			if resp, _ := cc.post(ifacePath, nil); resp != nil && resp.StatusCode == http.StatusOK {
				if iface == autoIface {
					iface = defaultIfaceName()
				}
				res := &ifaceResponse{}
				if err := json.NewDecoder(resp.Body).Decode(res); err != nil {
					logger.Warn().Err(err).Msg("Failed to get iface info")
					return
				}
				if res.OK {
					// In intercept mode, show intercept-specific status instead of
					// per-interface DNS messages (which are irrelevant).
					if res.InterceptMode != "" {
						switch res.InterceptMode {
						case "hard":
							logger.Notice().Msg("DNS hard intercept mode active — all DNS traffic intercepted, no VPN split routing")
						default:
							logger.Notice().Msg("DNS intercept mode active — all DNS traffic intercepted via OS packet filter")
						}
					} else {
						name := res.Name
						if iff, err := net.InterfaceByName(name); err == nil {
							_, _ = patchNetIfaceName(iff)
							name = iff.Name
						}
						ifaceLogger := logger.With().Str("iface", name)
						ifaceLogger.Debug().Msg("Setting DNS successfully")
						if res.All {
							// Log that DNS is set for other interfaces.
							withEachPhysicalInterfaces(
								name,
								"set DNS",
								func(i *net.Interface) error { return nil },
							)
						}
					}
				}
			}
		}
	}

	// No config path, generating config in HOME directory.
	noConfigStart := isNoConfigStart(cmd)
	writeDefaultConfig := !noConfigStart && configBase64 == ""

	logServerStarted := make(chan struct{})
	stopLogCh := make(chan struct{})
	ud, err := userHomeDir()
	sockDir := ud
	var logServerSocketPath string
	if err != nil {
		logger.Warn().Err(err).Msg("Failed to get user home directory")
		logger.Warn().Msg("Log server did not start")
		close(logServerStarted)
	} else {
		setWorkingDirectory(svcConfig, ud)
		if configPath == "" && writeDefaultConfig {
			defaultConfigFile = filepath.Join(ud, defaultConfigFile)
		}
		svcConfig.Arguments = append(svcConfig.Arguments, "--homedir="+ud)
		if d, err := socketDir(); err == nil {
			sockDir = d
		}
		logServerSocketPath = filepath.Join(sockDir, ctrldLogUnixSock)
		_ = os.Remove(logServerSocketPath)
		go func() {
			defer os.Remove(logServerSocketPath)

			close(logServerStarted)

			// Start HTTP log server
			if err := httpLogServer(logServerSocketPath, stopLogCh); err != nil && err != http.ErrServerClosed {
				logger.Warn().Err(err).Msg("Failed to serve HTTP log server")
				return
			}
		}()
	}
	<-logServerStarted

	if !startOnly {
		startOnly = len(osArgs) == 0
	}
	// If user run "ctrld start" and ctrld is already installed, starting existing service.
	if startOnly && isCtrldInstalled {
		tryReadingConfigWithNotice(false, true)
		if err := v.Unmarshal(&cfg); err != nil {
			logger.Fatal().Msgf("Failed to unmarshal config: %v", err)
		}

		// if already running, dont restart
		if isCtrldRunning {
			logger.Notice().Msg("Service is already running")
			return nil
		}

		initInteractiveLogging()
		tasks := []task{
			{func() error {
				// Save current DNS so we can restore later.
				withEachPhysicalInterfaces("", "saveCurrentStaticDNS", func(i *net.Interface) error {
					if err := saveCurrentStaticDNS(i); !errors.Is(err, errSaveCurrentStaticDNSNotSupported) && err != nil {
						return err
					}
					return nil
				})
				return nil
			}, false, "Save current DNS"},
			{func() error {
				return ConfigureWindowsServiceFailureActions(ctrldServiceName)
			}, false, "Configure service failure actions"},
			{s.Start, true, "Start"},
			{noticeWritingControlDConfig, false, "Notice writing ControlD config"},
		}
		logger.Notice().Msg("Starting existing ctrld service")
		if doTasks(tasks) {
			logger.Notice().Msg("Service started")
			sockDir, err := socketDir()
			if err != nil {
				logger.Warn().Err(err).Msg("Failed to get socket directory")
				os.Exit(1)
			}
			reportSetDnsOk(sockDir)
			// Verify service registration after successful start.
			if err := verifyServiceRegistration(); err != nil {
				logger.Warn().Err(err).Msg("Service registry verification failed")
			}
		} else {
			logger.Error().Err(err).Msg("Failed to start existing ctrld service")
			os.Exit(1)
		}
		return nil
	}

	if cdUID != "" {
		_ = doValidateCdRemoteConfig(cdUID, true)
	} else if uid := cdUIDFromProvToken(); uid != "" {
		cdUID = uid
		logger.Debug().Msg("Using uid from provision token")
		removeOrgFlagsFromArgs(svcConfig)
		// Pass --cd flag to "ctrld run" command, so the provision token takes no effect.
		svcConfig.Arguments = append(svcConfig.Arguments, "--cd="+cdUID)
	}
	if cdUID != "" {
		validateCdUpstreamProtocol()
	}

	if configPath != "" {
		v.SetConfigFile(configPath)
	}

	tryReadingConfigWithNotice(writeDefaultConfig, true)

	if err := v.Unmarshal(&cfg); err != nil {
		logger.Fatal().Msgf("Failed to unmarshal config: %v", err)
	}

	initInteractiveLogging()

	if nextdns != "" {
		removeNextDNSFromArgs(svcConfig)
	}

	// Explicitly passing config, so on system where home directory could not be obtained,
	// or sub-process env is different with the parent, we still behave correctly and use
	// the expected config file.
	if configPath == "" {
		svcConfig.Arguments = append(svcConfig.Arguments, "--config="+defaultConfigFile)
	}

	tasks := []task{
		{s.Stop, false, "Stop"},
		{func() error { return doGenerateNextDNSConfig(nextdns) }, true, "Checking config"},
		{func() error { return ensureUninstall(s) }, false, "Ensure uninstall"},
		//resetDnsTask(p, s, isCtrldInstalled, currentIface),
		{func() error {
			// Save current DNS so we can restore later.
			withEachPhysicalInterfaces("", "saveCurrentStaticDNS", func(i *net.Interface) error {
				if err := saveCurrentStaticDNS(i); !errors.Is(err, errSaveCurrentStaticDNSNotSupported) && err != nil {
					return err
				}
				return nil
			})
			return nil
		}, false, "Save current DNS"},
		{s.Install, false, "Install"},
		{func() error {
			return ConfigureWindowsServiceFailureActions(ctrldServiceName)
		}, false, "Configure Windows service failure actions"},
		{s.Start, true, "Start"},
		// Note that startCmd do not actually write ControlD config, but the config file was
		// generated after s.Start, so we notice users here for consistent with nextdns mode.
		{noticeWritingControlDConfig, false, "Notice writing ControlD config"},
	}
	logger.Notice().Msg("Starting service")
	if doTasks(tasks) {
		// add a small delay to ensure the service is started and did not crash
		time.Sleep(1 * time.Second)

		ok, status, err := selfCheckStatus(ctx, s, sockDir)
		switch {
		case ok && status == service.StatusRunning:
			logger.Notice().Msg("Service started")
		default:
			marker := append(bytes.Repeat([]byte("="), 32), '\n')
			// If ctrld service is not running, emitting log obtained from ctrld process.
			if status != service.StatusRunning || ctx.Err() != nil {
				logger.Error().Msg("Ctrld service may not have started due to an error or misconfiguration, service log:")
				_, _ = logger.Write(marker)

				// Wait for log collection to complete
				<-stopLogCh

				// Retrieve logs from HTTP server if available
				if logServerSocketPath != "" {
					hlc := newHTTPLogClient(logServerSocketPath)
					logs, err := hlc.GetLogs()
					if err != nil {
						logger.Warn().Err(err).Msg("Failed to get logs from HTTP log server")
					}
					if len(logs) == 0 {
						logger.Write([]byte("<no log output is obtained from ctrld process>\n"))
					} else {
						logger.Write(logs)
						logger.Write([]byte("\n"))
					}
				} else {
					logger.Write([]byte("<no log output from HTTP log server>\n"))
				}
			}
			// Report any error if occurred.
			if err != nil {
				_, _ = logger.Write(marker)
				msg := fmt.Sprintf("An error occurred while performing test query: %s\n", err)
				logger.Write([]byte(msg))
			}
			// If ctrld service is running but selfCheckStatus failed, it could be related
			// to user's system firewall configuration, notice users about it.
			if status == service.StatusRunning && err == nil {
				_, _ = logger.Write(marker)
				logger.Write([]byte("ctrld service was running, but a DNS query could not be sent to its listener\n"))
				logger.Write([]byte("Please check your system firewall if it is configured to block/intercept/redirect DNS queries\n"))
			}

			_, _ = logger.Write(marker)
			uninstall(p, s)
			os.Exit(1)
		}
		reportSetDnsOk(sockDir)
		// Verify service registration after successful start.
		if err := verifyServiceRegistration(); err != nil {
			logger.Warn().Err(err).Msg("Service registry verification failed")
		}
	}

	logger.Debug().Msg("Service start command completed")
	return nil
}

// createStartCommands creates the start command and its alias
func createStartCommands(sc *ServiceCommand) (*cobra.Command, *cobra.Command) {
	// Start command
	startCmd := &cobra.Command{
		Use:   "start",
		Short: "Install and start the ctrld service",
		Long: `Install and start the ctrld service

NOTE: running "ctrld start" without any arguments will start already installed ctrld service.`,
		Args: func(cmd *cobra.Command, args []string) error {
			args = filterEmptyStrings(args)
			if len(args) > 0 {
				return fmt.Errorf("'ctrld start' doesn't accept positional arguments\n" +
					"Use flags instead (e.g. --cd, --iface) or see 'ctrld start --help' for all options")
			}
			return nil
		},
		PreRun: func(cmd *cobra.Command, args []string) {
			checkHasElevatedPrivilege()
		},
		RunE: sc.Start,
	}
	// Keep these flags in sync with runCmd above, except for "-d"/"--nextdns".
	startCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file")
	startCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config")
	startCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port")
	startCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint")
	startCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint")
	startCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy")
	startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file")
	startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items")
	startCmd.Flags().StringVarP(&cdUID, cdUidFlagName, "", "", "Control D resolver uid")
	startCmd.Flags().StringVarP(&cdOrg, cdOrgFlagName, "", "", "Control D provision token")
	startCmd.Flags().StringVarP(&customHostname, customHostnameFlagName, "", "", "Custom hostname passed to ControlD API")
	startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain")
	_ = startCmd.Flags().MarkHidden("dev")
	startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
	startCmd.Flags().StringVarP(&nextdns, nextdnsFlagName, "", "", "NextDNS resolver id")
	startCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
	startCmd.Flags().BoolVarP(&skipSelfChecks, "skip_self_checks", "", false, `Skip self checks after installing ctrld service`)
	startCmd.Flags().BoolVarP(&startOnly, "start_only", "", false, "Do not install new service")
	_ = startCmd.Flags().MarkHidden("start_only")
	startCmd.Flags().BoolVarP(&rfc1918, "rfc1918", "", false, "Listen on RFC1918 addresses when 127.0.0.1 is the only listener")
	startCmd.Flags().StringVarP(&interceptMode, "intercept-mode", "", "", "OS-level DNS interception mode: 'dns' (with VPN split routing) or 'hard' (all DNS through ctrld, no VPN split routing)")
	startCmd.Flags().StringVarP(&firewallMode, "firewall-mode", "", "off", "DNS-resolved IP allowlist: 'on' blocks connections to IPs not resolved by ctrld, 'off' allows all")

	// Start command alias
	startCmdAlias := &cobra.Command{
		PreRun: func(cmd *cobra.Command, args []string) {
			checkHasElevatedPrivilege()
		},
		Use:   "start",
		Short: "Quick start service and configure DNS on interface",
		Long: `Quick start service and configure DNS on interface

NOTE: running "ctrld start" without any arguments will start already installed ctrld service.`,
		Args: func(cmd *cobra.Command, args []string) error {
			args = filterEmptyStrings(args)
			if len(args) > 0 {
				return fmt.Errorf("'ctrld start' doesn't accept positional arguments\n" +
					"Use flags instead (e.g. --cd, --iface) or see 'ctrld start --help' for all options")
			}
			return nil
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(os.Args) == 2 {
				startOnly = true
			}
			if !cmd.Flags().Changed("iface") {
				os.Args = append(os.Args, "--iface="+ifaceStartStop)
			}
			iface = ifaceStartStop
			return startCmd.RunE(cmd, args)
		},
	}
	startCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", autoIface, `Update DNS setting for iface, "auto" means the default interface gateway`)
	startCmdAlias.Flags().AddFlagSet(startCmd.Flags())

	return startCmd, startCmdAlias
}
