package procedure

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"strings"

	"github.com/coredns/caddy/caddyfile"
	eksv1alpha1 "github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// hostRecord is a pair of string denoting ip to hostname mapping.
type hostRecord struct {
	ip   string
	host string
}

const (
	hostsKey           = "hosts"
	fallthroughKey     = "fallthrough"
	fallthroughLineStr = "[" + fallthroughKey + "]"
	corednsCMName      = "coredns-configmap"
	corednsName        = "coredns"
	corefileKey        = "Corefile"

	FrpsInternalSvcName = "frps-internal-service"
)

// parseCorefile is an internal function for corefile parsing
func parseCorefile(cf string) (caddyfile.EncodedCaddyfile, error) {
	res := caddyfile.EncodedCaddyfile{}
	buf, err := caddyfile.ToJSON([]byte(cf))
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(buf, &res)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// updateCorefile is a wrapper for internal logics that updates corefile.
func updateCorefile(cf caddyfile.EncodedCaddyfile, additionalRecords []interface{}, hostset map[string]bool) error {
	for idx, server := range cf {
		hostBlkFound, hostIndex, needFallthrough := false, -1, false
		for i, block := range server.Body {
			if len(block) == 0 || fmt.Sprintf("%v", block[0]) != hostsKey {
				continue
			}
			hostBlkFound, hostIndex = true, i
			break
		}
		var hostBlk, entries, newEntries []interface{}
		if !hostBlkFound {
			hostBlk = []interface{}{hostsKey}
			cf[idx].Body = append(server.Body, hostBlk)
			server = cf[idx]
			hostIndex = len(server.Body) - 1
		} else {
			hostBlk = server.Body[hostIndex]
		}
		// create hostfile lists if not created yet
		if len(hostBlk) < 2 {
			entries = []interface{}{}
			server.Body[hostIndex] = append(hostBlk, entries)
			hostBlk = server.Body[hostIndex]
		} else {
			ents, ok := hostBlk[1].([]interface{})
			if !ok {
				return fmt.Errorf("failed to cast hostfile entries to list")
			}
			entries = ents
		}
		if len(entries) == 0 || fmt.Sprintf("%v", entries[len(entries)-1]) != fallthroughLineStr {
			needFallthrough = true
		}
		// add the mapping to the top of the host file
		newEntries = append([]interface{}{}, additionalRecords...)
		for _, entry := range entries {
			// Deduplicate records
			if record, ok := entry.([]interface{}); ok && len(record) == 2 {
				if host, ok := record[1].(string); ok && hostset[host] {
					continue
				}
			}
			newEntries = append(newEntries, entry)
		}
		if needFallthrough {
			newEntries = append(newEntries, []interface{}{fallthroughKey})
		}
		hostBlk[1] = newEntries
	}
	return nil
}

// insertHostRecord is an internal function for adding DNS record to hostfile in corefile
func insertHostRecord(cf caddyfile.EncodedCaddyfile, records []hostRecord) error {
	newRecords, hostset := make([]interface{}, len(records)), make(map[string]bool)
	for i, record := range records {
		newRecords[i] = []interface{}{record.ip, record.host}
		hostset[record.host] = true
	}
	return updateCorefile(cf, newRecords, hostset)
}

// removeHostRecords is an internal function for removing DNS records from hostfile in corefile
func removeHostRecords(cf caddyfile.EncodedCaddyfile, hosts []string) error {
	hostset := make(map[string]bool)
	for _, host := range hosts {
		hostset[host] = true
	}
	return updateCorefile(cf, nil, hostset)
}

// getCorefileString is an internal function for generating corefile string content
func getCorefileString(cf caddyfile.EncodedCaddyfile) (string, error) {
	buf, err := json.Marshal(&cf)
	if err != nil {
		return "", err
	}
	res, err := caddyfile.FromJSON(buf)
	if err != nil {
		return "", err
	}
	return string(res), nil
}

// addRecordToCorefile is a wrapper for internal logics that adds host record to corefile.
func addRecordToCorefile(corefile string, records []hostRecord) (string, error) {
	cf, err := parseCorefile(corefile)
	if err != nil {
		return "", err
	}
	err = insertHostRecord(cf, records)
	if err != nil {
		return "", err
	}
	return getCorefileString(cf)
}

// removeHostsFromCorefile is a wrapper for internal logics that removes host record from corefile.
func removeHostsFromCorefile(corefile string, hosts []string) (string, error) {
	cf, err := parseCorefile(corefile)
	if err != nil {
		return "", err
	}
	err = removeHostRecords(cf, hosts)
	if err != nil {
		return "", err
	}
	return getCorefileString(cf)
}

// getEKSHost returns the hostname of the EKS cluster.
func getEKSHost(ctx context.Context, cli client.Reader, info ProcedureInfo) (string, error) {
	eks, err := getEKSObject(ctx, cli, info.eksConnection)
	if err != nil {
		return "", err
	}
	if eks.Status.AtProvider.Endpoint == "" {
		return "", fmt.Errorf("eks endpoint hostname is empty in observation")
	}
	parsedURL, err := url.Parse(eks.Status.AtProvider.Endpoint)
	if err != nil {
		return "", err
	}
	return parsedURL.Hostname(), nil
}

// getEKSRecord retrieves the record mapping for the tenant EKS
func getEKSRecord(ctx context.Context, baseCli, infraCli client.Reader, info ProcedureInfo) (hostRecord, error) {
	var (
		res hostRecord
		err error
	)
	res.host, err = getEKSHost(ctx, infraCli, info)
	if err != nil {
		return res, err
	}
	frpEndpoint, err := retrieveFrpEndpoint(ctx, baseCli, infraCli, info)
	if err != nil {
		return res, err
	}
	res.ip = frpEndpoint
	return res, nil
}

type corednsCM struct{}

// retrieveCorefile returns the CoreDNS corefile contents in string
func retrieveCorefile(ctx context.Context, cli client.Reader) (string, *corev1.ConfigMap, error) {
	key := types.NamespacedName{
		Namespace: metav1.NamespaceSystem,
		Name:      corednsName,
	}
	cm := &corev1.ConfigMap{}
	err := cli.Get(ctx, key, cm)
	if err != nil {
		return "", nil, err
	}
	if cm.Data == nil {
		cm.Data = make(map[string]string)
	}
	corefile, exist := cm.Data[corefileKey]
	if !exist {
		return "", nil, fmt.Errorf("corefile content missing for coredns configmap")
	}
	return corefile, cm, nil
}

// retrieveFrpEndpoint returns the EKS endpoint mapped by FRPS
func retrieveFrpEndpoint(ctx context.Context, baseCli, infraCli client.Reader, info ProcedureInfo) (string, error) {
	frps := &eksv1alpha1.Frpserver{}
	frpsName := namer.UniqueIdentifier(info.eksConnection.ObjectMeta)
	err := infraCli.Get(ctx, types.NamespacedName{Name: frpsName}, frps)
	if err != nil {
		return "", err
	}
	if frps.Annotations == nil || frps.Annotations[AnnotationKeyFrpsSvcNs] == "" || frps.Annotations[AnnotationKeyFrpsInternalSvcName] == "" {
		return "", fmt.Errorf("frpserver %s is missing annotations", frpsName)
	}
	svcKey := types.NamespacedName{
		Name:      frps.Annotations[AnnotationKeyFrpsInternalSvcName],
		Namespace: frps.Annotations[AnnotationKeyFrpsSvcNs],
	}
	svc := &corev1.Service{}
	err = baseCli.Get(ctx, svcKey, svc)
	if err != nil {
		return "", err
	}
	if svc.Spec.ClusterIP == "" {
		return "", fmt.Errorf("frps svc ClusterIP is empty")
	}
	return svc.Spec.ClusterIP, nil
}

// Deploy deploys resource for the specified tenant.
func (c *corednsCM) Deploy(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	corefile, cm, err := retrieveCorefile(ctx, baseCli)
	if err != nil {
		return err
	}
	eksRecord, err := getEKSRecord(ctx, baseCli, infraCli, info)
	if err != nil {
		return err
	}
	corefile, err = addRecordToCorefile(corefile, []hostRecord{eksRecord})
	if err != nil {
		return err
	}
	cm.Data[corefileKey] = corefile
	err = baseCli.Update(ctx, cm)
	if err != nil {
		return err
	}

	// Workaround for https://github.com/tidbcloud/eks-provider/issues/123
	// annotate the EKS cluster object
	frpEp, err := retrieveFrpEndpoint(ctx, baseCli, infraCli, info)
	if err != nil {
		return err
	}
	if info.eks.Annotations == nil {
		info.eks.Annotations = make(map[string]string)
	}
	info.eks.Annotations[consts.AnnotationKeyEKSEndpoint] = fmt.Sprintf("https://%s", frpEp)
	err = infraCli.Update(ctx, info.eks)
	if err != nil {
		return err
	}

	return nil
}

// Cleanup cleans up the host record from corefile for the specified tenant.
func (c *corednsCM) Cleanup(ctx context.Context, baseCli, infraCli client.Client, info ProcedureInfo) error {
	corefile, cm, err := retrieveCorefile(ctx, baseCli)
	if err != nil {
		return err
	}
	eksHost, err := getEKSHost(ctx, infraCli, info)
	if err != nil {
		return err
	}
	corefile, err = removeHostsFromCorefile(corefile, []string{eksHost})
	if err != nil {
		return err
	}
	cm.Data[corefileKey] = corefile
	return baseCli.Update(ctx, cm)
}

// Status retrieves the status for the deploying step.
func (c *corednsCM) Status(ctx context.Context, baseCli client.Reader, infraCli client.Client, info ProcedureInfo) (bool, error) {
	corefile, _, err := retrieveCorefile(ctx, baseCli)
	if err != nil {
		return false, err
	}
	eksRecord, err := getEKSRecord(ctx, baseCli, infraCli, info)
	if err != nil {
		return false, err
	}
	return strings.Contains(corefile, fmt.Sprintf("%s %s", eksRecord.ip, eksRecord.host)), nil
}

// Name returns the name of the step.
func (c *corednsCM) Name() string {
	return corednsCMName
}

// NewCoreDNSCM returns the procedure for coredns configmap.
func NewCoreDNSCM() *corednsCM {
	return &corednsCM{}
}

var _ Procedure = &corednsCM{}
