package agent

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"os/exec"
	"sync"
	"time"

	jmpapi "github.com/tidbcloud/devops/api/jump"
	"github.com/tidbcloud/devops/pkg/infra/log"
	executil "github.com/tidbcloud/devops/pkg/infra/utils/exec"

	"github.com/go-logr/logr"
	"golang.org/x/sys/unix"
)

const (
	socketPath = "/tmp/tailscaled.sock"
	socketOpt  = "--socket=/tmp/tailscaled.sock"
)

type TailscaleUpOptions struct {
	Hostname string
	AuthKey  string
}

type TailscaleManager struct {
	logger     logr.Logger
	tailscale  string
	tailscaled string
	mu         *sync.Mutex // used to protect tailscale command execution
}

func NewTailscaleManager(logger logr.Logger) *TailscaleManager {
	return &TailscaleManager{
		logger:     logger.WithName("TailscaleManager"),
		mu:         &sync.Mutex{},
		tailscale:  "/usr/bin/tailscale",
		tailscaled: "/usr/sbin/tailscaled",
	}
}

func (m *TailscaleManager) Init() error {
	err := m.StartTailscaled()
	if err != nil {
		return fmt.Errorf("run tailscaled failed: %w", err)
	}

	return nil
}

// StartTailscaled starts the tailscaled as background process.
//
// The start command is referenced from https://github.com/tailscale/tailscale/blob/main/cmd/containerboot/tailscaled.go
func (m *TailscaleManager) StartTailscaled() error {
	err := ensureTunFile()
	if err != nil {
		return fmt.Errorf("ensure tun file failed: %w", err)
	}

	cmd := exec.Command(m.tailscaled, socketOpt, "--state=mem:", "--statedir=/tmp") //nolint:gosec
	cmd.Stdout = log.NewLogrToWriter(m.logger.WithName("Tailscaled").WithName("Stdout"))
	cmd.Stderr = log.NewLogrToWriter(m.logger.WithName("Tailscaled").WithName("Stderr"))

	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("start tailscaled failed: %w", err)
	}

	m.logger.Info("waiting for tailscaled socket")
	startAt := time.Now()
	for {
		if time.Since(startAt) > 60*time.Second {
			return fmt.Errorf("timed out waiting for tailscaled socket")
		}

		_, err := os.Stat(socketPath)
		if errors.Is(err, fs.ErrNotExist) {
			time.Sleep(500 * time.Millisecond)
			continue
		}
		if err != nil {
			return fmt.Errorf("error waiting for tailscaled socket: %w", err)
		}
		break
	}

	m.logger.Info("tailscaled started", "pid", cmd.Process.Pid)
	return nil
}

func (m *TailscaleManager) TailscaleUp(ctx context.Context, opts TailscaleUpOptions) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	args := executil.CommandStructuredArgs{
		Subcmd: []string{socketOpt, "up"},
		Opts:   []string{"--accept-dns=true"},
	}

	if opts.Hostname != "" {
		args.Opts = append(args.Opts, "--hostname", opts.Hostname)
	}
	if opts.AuthKey != "" {
		args.Opts = append(args.Opts, "--authkey", opts.AuthKey)
	}

	cmd := executil.NewCommandBuilder(ctx, m.tailscale, executil.WithStructuredArgs(args))
	output, err := cmd.Run()
	if err != nil {
		return err
	}

	if output.ExitCode != 0 {
		return fmt.Errorf("exec command failed: %s", output.String())
	}

	return nil
}

func (m *TailscaleManager) TailscaleDown(ctx context.Context) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	args := executil.CommandStructuredArgs{
		Subcmd: []string{socketOpt, "logout"},
	}

	cmd := executil.NewCommandBuilder(ctx, m.tailscale, executil.WithStructuredArgs(args))
	output, err := cmd.Run()
	if err != nil {
		return err
	}

	if output.ExitCode != 0 {
		return fmt.Errorf("exec command failed: %s", output.String())
	}

	return nil
}

func (m *TailscaleManager) TailscaleStatus(ctx context.Context) (*jmpapi.TailscaleStatus, error) {
	m.mu.Lock()
	defer m.mu.Unlock()

	args := executil.CommandStructuredArgs{
		Subcmd: []string{socketOpt, "status"},
		Opts:   []string{"--json", "--active"},
	}

	cmd := executil.NewCommandBuilder(ctx, m.tailscale, executil.WithStructuredArgs(args))
	output, err := cmd.Run()
	if err != nil {
		return nil, err
	}

	if output.ExitCode != 0 {
		return nil, fmt.Errorf("exec command failed: %s", output.String())
	}

	status := &jmpapi.TailscaleStatus{}
	err = json.Unmarshal([]byte(output.Stdout), status)
	if err != nil {
		return nil, fmt.Errorf("unmarshal tailscale status failed: %w", err)
	}

	return status, nil
}

// ensureTunFile checks that /dev/net/tun exists, creating it if
// missing.
func ensureTunFile() error {
	// Verify that /dev/net/tun exists, in some container envs it
	// needs to be mknod-ed.

	if _, err := os.Stat("/dev/net"); errors.Is(err, fs.ErrNotExist) {
		if err := os.MkdirAll("/dev/net", 0755); err != nil {
			return err
		}
	}
	if _, err := os.Stat("dev/net/tun"); errors.Is(err, fs.ErrNotExist) {
		dev := unix.Mkdev(10, 200) // tuntap major and minor
		if err := unix.Mknod("dev/net/tun", 0600|unix.S_IFCHR, int(dev)); err != nil {
			return err
		}
	}
	return nil
}
