//go:build !ios && !android

package cmd

import (
	"context"
	"fmt"
	"runtime"
	"time"

	"github.com/kardianos/service"
	log "github.com/sirupsen/logrus"

	"github.com/spf13/cobra"
	"google.golang.org/grpc"

	"github.com/netbirdio/netbird/client/internal/daemonaddr"
	"github.com/netbirdio/netbird/client/internal/ipcauth"
	"github.com/netbirdio/netbird/client/proto"
	"github.com/netbirdio/netbird/client/server"
	"github.com/netbirdio/netbird/client/system"
	"github.com/netbirdio/netbird/util"
)

func validateJSONSocketFlags() error {
	if serviceCmd.PersistentFlags().Changed("json-socket") && !enableJSONSocket {
		return fmt.Errorf("--json-socket requires --enable-json-socket to configure the daemon JSON gateway")
	}
	return nil
}

// daemonServerOptions installs the transport credentials that expose each
// caller's kernel-authenticated identity to the handlers, which is what lets
// the daemon require root/administrator for privileged operations.
//
// The handshake exchanges no bytes, so older CLI and UI binaries still
// interoperate. Callers on a TCP socket carry no identity at all: the daemon
// keeps serving them, and the privileged operations deny them, so a warning is
// logged to make the loss of functionality visible.
func daemonServerOptions(network string) []grpc.ServerOption {
	if network == "tcp" {
		log.Warnf("daemon is listening on TCP (%s): callers carry no verifiable identity over TCP, "+
			"so privileged operations (SSH root login, SSH auth, enabling the SSH server, management URL changes, "+
			"deregistration) will be denied. Use a unix socket, or npipe:// on Windows", daemonAddr)
		return nil
	}

	creds := ipcauth.NewTransportCredentials()
	if creds == nil {
		log.Warnf("daemon IPC has no peer-identity primitive on %s: privileged operations will be denied", runtime.GOOS)
		return nil
	}

	return []grpc.ServerOption{grpc.Creds(creds)}
}

func (p *program) Start(svc service.Service) error {
	// Start should not block. Do the actual work async.
	log.Info("starting NetBird service") //nolint

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

	// Collect static system and platform information
	system.UpdateStaticInfoAsync()

	// A daemon installed before named-pipe support has the loopback TCP address
	// persisted. Move it to the named pipe so an upgraded daemon can identify
	// its callers instead of silently serving an unauthenticated socket.
	if migrated, ok := daemonaddr.MigrateLegacy(daemonAddr); ok {
		log.Infof("daemon address %q predates named-pipe support, listening on %q so callers can be identified", daemonAddr, migrated)
		daemonAddr = migrated
	}

	network, _, err := parseListenAddress(daemonAddr)
	if err != nil {
		return fmt.Errorf("parse daemon address: %w", err)
	}

	// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
	p.serv = grpc.NewServer(daemonServerOptions(network)...)

	daemonListener, jsonListener, err := listenDaemonSockets()
	if err != nil {
		return err
	}

	go func() {
		// Fatal here rather than inside serve, so serve's deferred listener
		// closes run before the process exits.
		if err := p.serve(daemonListener, jsonListener); err != nil {
			log.Fatalf("failed to %v", err)
		}
	}()
	return nil
}

// listenDaemonSockets opens the daemon control socket and, when it is enabled, the
// JSON gateway socket. The control socket is closed again if the second one fails,
// so a failed start leaves nothing listening. The returned JSON listener is nil
// when the socket is disabled.
func listenDaemonSockets() (*socketListener, *socketListener, error) {
	daemonListener, err := listenOnAddress(daemonAddr)
	if err != nil {
		return nil, nil, fmt.Errorf("listen daemon interface: %w", err)
	}

	if !enableJSONSocket {
		removeStaleUnixSocketForAddress(jsonSocket)
		return daemonListener, nil, nil
	}

	jsonListener, err := listenOnAddress(jsonSocket)
	if err != nil {
		if cerr := daemonListener.Close(); cerr != nil {
			log.Debugf("close daemon listener: %v", cerr)
		}
		return nil, nil, fmt.Errorf("listen daemon JSON interface: %w", err)
	}

	return daemonListener, jsonListener, nil
}

// serve brings up the daemon server on an already-open control socket and blocks
// until it stops. jsonListener is nil when the JSON socket is disabled. A returned
// error means the daemon cannot run at all and the caller is expected to exit; the
// failures it recovers from on its own are logged here.
func (p *program) serve(daemonListener, jsonListener *socketListener) error {
	defer daemonListener.Close()
	if jsonListener != nil {
		defer jsonListener.Close()
	}

	// chmodUnixSocket is a no-op for a nil listener and for a non-unix one.
	if err := daemonListener.chmodUnixSocket("daemon"); err != nil {
		log.Error(err)
		return nil
	}
	if err := jsonListener.chmodUnixSocket("daemon JSON"); err != nil {
		log.Error(err)
		return nil
	}

	serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, captureEnabled, networksDisabled)
	if err := serverInstance.Start(); err != nil {
		return fmt.Errorf("start daemon: %w", err)
	}
	proto.RegisterDaemonServiceServer(p.serv, serverInstance)

	p.serverInstanceMu.Lock()
	p.serverInstance = serverInstance
	p.serverInstanceMu.Unlock()

	if jsonListener == nil {
		log.Debug("daemon JSON socket disabled")
	} else if err := p.startJSONGateway(jsonListener, daemonAddr); err != nil {
		return fmt.Errorf("start daemon JSON server: %w", err)
	}

	log.Printf("started daemon server: %v", daemonListener.address)
	if err := p.serv.Serve(daemonListener.Listener); err != nil {
		log.Errorf("failed to serve daemon requests: %v", err)
	}
	return nil
}

func (p *program) Stop(srv service.Service) error {
	p.serverInstanceMu.Lock()
	if p.serverInstance != nil {
		in := new(proto.DownRequest)
		_, err := p.serverInstance.Down(p.ctx, in)
		if err != nil {
			log.Errorf("failed to stop daemon: %v", err)
		}
	}
	p.serverInstanceMu.Unlock()

	p.cancel()

	p.jsonServMu.Lock()
	jsonServ, jsonClient := p.jsonServ, p.jsonClient
	p.jsonServMu.Unlock()
	if jsonClient != nil {
		if err := jsonClient.Close(); err != nil {
			log.Debugf("close daemon JSON gateway client: %v", err)
		}
	}
	if jsonServ != nil {
		shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 2*time.Second)
		if err := jsonServ.Shutdown(shutdownCtx); err != nil {
			log.Errorf("failed to stop daemon JSON server gracefully: %v", err)
			if err := jsonServ.Close(); err != nil {
				log.Errorf("failed to close daemon JSON server: %v", err)
			}
		}
		shutdownCancel()
	}

	if p.serv != nil {
		p.serv.Stop()
	}

	time.Sleep(time.Second * 2)
	log.Info("stopped NetBird service") //nolint
	return nil
}

// Common setup for service control commands
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc, consoleLog bool) (service.Service, error) {
	// rootCmd env vars are already applied by PersistentPreRunE.
	SetFlagsFromEnvVars(serviceCmd)

	cmd.SetOut(cmd.OutOrStdout())

	if err := handleRebrand(cmd); err != nil {
		return nil, err
	}

	if consoleLog {
		if err := util.InitLog(logLevel, util.LogConsole); err != nil {
			return nil, fmt.Errorf("init log: %w", err)
		}
	} else {
		if err := util.InitLog(logLevel, logFiles...); err != nil {
			return nil, fmt.Errorf("init log: %w", err)
		}
	}

	cfg, err := newSVCConfig()
	if err != nil {
		return nil, fmt.Errorf("create service config: %w", err)
	}

	s, err := newSVC(newProgram(ctx, cancel), cfg)
	if err != nil {
		return nil, err
	}

	return s, nil
}

var runCmd = &cobra.Command{
	Use:   "run",
	Short: "runs NetBird as service",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(cmd.Context())

		SetupCloseHandler(ctx, cancel)
		SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))

		s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
		if err != nil {
			return err
		}
		if err := validateJSONSocketFlags(); err != nil {
			return err
		}

		return s.Run()
	},
}

var startCmd = &cobra.Command{
	Use:   "start",
	Short: "starts NetBird service",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(cmd.Context())
		s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
		if err != nil {
			return err
		}
		if err := validateJSONSocketFlags(); err != nil {
			return err
		}

		if err := s.Start(); err != nil {
			return fmt.Errorf("start service: %w", err)
		}
		cmd.Println("NetBird service has been started")
		return nil
	},
}

var stopCmd = &cobra.Command{
	Use:   "stop",
	Short: "stops NetBird service",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(cmd.Context())
		s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
		if err != nil {
			return err
		}

		if err := s.Stop(); err != nil {
			return fmt.Errorf("stop service: %w", err)
		}
		cmd.Println("NetBird service has been stopped")
		return nil
	},
}

var restartCmd = &cobra.Command{
	Use:   "restart",
	Short: "restarts NetBird service",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(cmd.Context())
		s, err := setupServiceControlCommand(cmd, ctx, cancel, false)
		if err != nil {
			return err
		}
		if err := validateJSONSocketFlags(); err != nil {
			return err
		}

		if err := s.Restart(); err != nil {
			return fmt.Errorf("restart service: %w", err)
		}
		cmd.Println("NetBird service has been restarted")
		return nil
	},
}

var svcStatusCmd = &cobra.Command{
	Use:   "status",
	Short: "shows NetBird service status",
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx, cancel := context.WithCancel(cmd.Context())
		s, err := setupServiceControlCommand(cmd, ctx, cancel, true)
		if err != nil {
			return err
		}

		status, err := s.Status()
		if err != nil {
			return fmt.Errorf("get service status: %w", err)
		}

		var statusText string
		switch status {
		case service.StatusRunning:
			statusText = "Running"
		case service.StatusStopped:
			statusText = "Stopped"
		case service.StatusUnknown:
			statusText = "Unknown"
		default:
			statusText = fmt.Sprintf("Unknown (%d)", status)
		}

		cmd.Printf("NetBird service status: %s\n", statusText)
		return nil
	},
}
