package jumper

import (
	"context"
	"fmt"
	"slices"
	"strings"

	jmpapi "github.com/tidbcloud/devops/api/jump"
	jpent "github.com/tidbcloud/devops/entity/jump"
	"github.com/tidbcloud/devops/pkg/infra/clients/coresvccli"
	"github.com/tidbcloud/devops/pkg/infra/consts"
	"github.com/tidbcloud/devops/pkg/infra/log"
	"github.com/tidbcloud/devops/pkg/service/jump/jumppod/types"

	"tailscale.com/client/tailscale/v2"
)

// ByocJumper knows how to jump to a byoc cluster.
type ByocJumper struct {
	tsAuthKeyTags []string
	tailscale     *tailscale.Client
	coresvc       *coresvccli.CoreSvcCli
}

func NewByocJumper(dep JumperDependency) (Jumper, error) {
	j := &ByocJumper{
		tsAuthKeyTags: dep.Conf.Tailscale.TailscaleAuthKeyTags,
		tailscale:     dep.Tailscale,
		coresvc:       dep.CoreSvc,
	}
	return j, nil
}

func (j *ByocJumper) PrepareJump(ctx context.Context, jp JumpPodForJumper, idest jpent.IDestination, param *types.PrepareJumpParam) (*types.JumpParam, error) {
	dest, ok := idest.(*jpent.DestinationByocCluster)
	if !ok {
		return nil, fmt.Errorf("invalid destination type %s", idest.Type())
	}
	dstHostname := dest.BastionName

	allDevices, err := j.tailscale.Devices().List(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to list devices: %w", err)
	}

	// get the destination device
	dstDeviceIP, err := j.findDeviceIPInTailscale(ctx, dstHostname, allDevices)
	if err != nil {
		return nil, err
	}

	status, err := jp.TailscaleStatus(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get tailscale status: %w", err)
	}

	srcHostname := ""
	srcDeviceIP := ""
	switch status.BackendState {
	case jmpapi.BackendStateNeedsLogin:
		// tailscale up if needed
		srcHostname, err = j.tailscaleUpForJumpPod(ctx, jp)
		if err != nil {
			return nil, err
		}
		log.FromContext(ctx).Info("tailscale up for jump pod", "hostname", srcHostname)
	case jmpapi.BackendStateRunning:
		srcHostname = status.Self.HostName
		if len(status.TailscaleIPs) > 0 {
			srcDeviceIP = status.TailscaleIPs[0]
		}
	default:
		return nil, fmt.Errorf("unsupport tailscale backend state %s", status.BackendState)
	}
	if srcDeviceIP == "" {
		srcDeviceIP, err = j.findDeviceIPInTailscale(ctx, srcHostname, allDevices)
		if err != nil {
			return nil, err
		}
	}

	// update ACL policy
	acl, err := j.tailscale.PolicyFile().Get(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get ACL policy: %w", err)
	}
	acl = setACLEntry(acl, srcHostname, srcDeviceIP, dstHostname, dstDeviceIP)
	etag := strings.Trim(acl.ETag, "\"") // It's a bug in tailscale client, we need to trim the quotes to make `Set` work. Ref:
	err = j.tailscale.PolicyFile().Set(ctx, *acl, etag)
	if err != nil {
		return nil, fmt.Errorf("failed to set ACL policy: %w", err)
	}

	jpParam := &types.JumpParam{
		Method: jpent.JumpMethodTailscale,
		Attr: jpent.JumpAttributes{
			Tailscale: &jpent.JumpTailscaleAttributes{
				User:        "tidbcloud_oncall",
				SrcHostname: srcHostname,
				SrcIP:       srcDeviceIP,
				DstHostname: dstHostname,
				DstIP:       dstDeviceIP,
			},
		},
	}
	return jpParam, nil
}

func (j *ByocJumper) FinalizeJump(ctx context.Context, jp JumpPodForJumper, idest jpent.IDestination, attr jpent.JumpAttributes) error {
	_, ok := idest.(*jpent.DestinationByocCluster)
	if !ok {
		return fmt.Errorf("invalid destination type %s", idest.Type())
	}

	// NOTE: we don't need to tailscale down here, because maybe other jumpers need to keep the up.
	// And we use the ephemeral device, so the device will be removed automatically after the jump pod is deleted.

	// update ACL policy
	if attr.Tailscale == nil {
		return fmt.Errorf("tailscale attributes of jump is nil")
	}

	acl, err := j.tailscale.PolicyFile().Get(ctx)
	if err != nil {
		return fmt.Errorf("failed to get ACL policy: %w", err)
	}
	acl = unsetACLEntry(acl, attr.Tailscale.SrcHostname, attr.Tailscale.SrcIP, attr.Tailscale.DstHostname, attr.Tailscale.DstIP)
	etag := strings.Trim(acl.ETag, "\"") // It's a bug in tailscale client, we need to trim the quotes to make `Set` work. Ref: https://github.com/tailscale/tailscale-client-go-v2/issues/27
	err = j.tailscale.PolicyFile().Set(ctx, *acl, etag)
	if err != nil {
		return fmt.Errorf("failed to set ACL policy: %w", err)
	}

	return nil
}

func (j *ByocJumper) RefreshJump(ctx context.Context, jp JumpPodForJumper, idest jpent.IDestination, attr jpent.JumpAttributes, param *types.RefreshJumpParam) (*types.JumpParam, error) {
	return nil, fmt.Errorf("not supported")
}

func (j *ByocJumper) SupportedDestinations() (jpent.DestinationType, []string) {
	typ := jpent.DestinationTypeByoc
	return typ, nil
}

func (j *ByocJumper) tailscaleUpForJumpPod(ctx context.Context, jp JumpPodForJumper) (string, error) {
	// <env>-<jump pod name>, e.g. dev-jump-pod-8d1ce022232333ff9fd54b390fc9f95c
	hostname := fmt.Sprintf("%s-%s", consts.GetEnv(), jp.JumpContext().Namer.JumpPod().Name)

	ckReq := tailscale.CreateKeyRequest{
		ExpirySeconds: 3 * 24 * 60 * 60, // 3 days
		Description:   hostname,
	}
	ckReq.Capabilities.Devices.Create.Ephemeral = true // use ephemeral key
	ckReq.Capabilities.Devices.Create.Tags = j.tsAuthKeyTags
	authKey, err := j.tailscale.Keys().CreateAuthKey(ctx, ckReq)
	if err != nil {
		return "", fmt.Errorf("failed to create auth key: %w", err)
	}

	upReq := jmpapi.TailscaleUpRequest{
		Hostname: hostname,
		AuthKey:  authKey.Key,
	}
	err = jp.TailscaleUp(ctx, upReq)
	if err != nil {
		return "", fmt.Errorf("tailscale up failed: %w", err)
	}

	return hostname, nil
}

func (j *ByocJumper) findDeviceIPInTailscale(ctx context.Context, hostname string, devices []tailscale.Device) (string, error) {
	if devices == nil {
		latestDevices, err := j.tailscale.Devices().List(ctx)
		if err != nil {
			return "", fmt.Errorf("failed to list devices: %w", err)
		}
		devices = latestDevices
	}

	for _, device := range devices {
		if device.Hostname == hostname {
			if device.LastSeen.IsZero() {
				return "", fmt.Errorf("device %s is not online in tailscale", hostname)
			}

			if len(device.Addresses) > 0 {
				return device.Addresses[0], nil // first address is the IPv4
			}
		}
	}

	return "", fmt.Errorf("failed to find device %s in tailscale", hostname)
}

func setACLEntry(acl *tailscale.ACL, srcHostname, srcIP, dstHostname, dstIP string) *tailscale.ACL {
	// TODO: how to delete the hostname in ACL?
	// set hosts
	if acl.Hosts == nil {
		acl.Hosts = make(map[string]string)
	}

	acl.Hosts[srcHostname] = srcIP
	acl.Hosts[dstHostname] = dstIP

	// set acl entry
	dst := fmt.Sprintf("%s:22", dstHostname)
	var entry *tailscale.ACLEntry
	for _, e := range acl.ACLs {
		if e.Source[0] == srcHostname && e.Destination[0] == dst {
			entry = &e
			break
		}
	}

	if entry == nil {
		entry = &tailscale.ACLEntry{
			Action:      "accept",
			Source:      []string{srcHostname},
			Destination: []string{dst},
		}
		acl.ACLs = append(acl.ACLs, *entry)
	}

	return acl
}

func unsetACLEntry(acl *tailscale.ACL, srcHostname, _, dstHostname, _ string) *tailscale.ACL {
	dst := fmt.Sprintf("%s:22", dstHostname)
	acl.ACLs = slices.DeleteFunc(acl.ACLs, func(e tailscale.ACLEntry) bool {
		return e.Source[0] == srcHostname && e.Destination[0] == dst
	})

	return acl
}
