package dm

import (
	"context"
	"fmt"
	"net"
	"strconv"
	"strings"

	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	agentpb "github.com/tidbcloud/dataflow-service/idl/pbgen/proto/agent"
	commonpb "github.com/tidbcloud/dataflow-service/idl/pbgen/proto/common"
	dmpb "github.com/tidbcloud/dataflow-service/idl/pbgen/proto/dm"
	"github.com/tidbcloud/dataflow-service/internal/config"
	"github.com/tidbcloud/dataflow-service/internal/db/entity"
	"github.com/tidbcloud/dataflow-service/internal/handler/agent"
	"google.golang.org/protobuf/encoding/protojson"
)

var supportedAWSRegions = []*commonpb.Region{
	// we support sources in all aws regions
	// https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/using-regions-availability-zones.html
	{Name: "us-east-2", DisplayName: "Ohio (us-east-2)"},
	{Name: "us-east-1", DisplayName: "N. Virginia (us-east-1)"},
	{Name: "us-west-1", DisplayName: "N. California (us-west-1)"},
	{Name: "us-west-2", DisplayName: "Oregon (us-west-2)"},
	{Name: "af-south-1", DisplayName: "Cape Town (af-south-1)"},
	{Name: "ap-east-1", DisplayName: "Hong Kong (ap-east-1)"},
	{Name: "ap-southeast-3", DisplayName: "Jakarta (ap-southeast-3)"},
	{Name: "ap-south-1", DisplayName: "Mumbai (ap-south-1)"},
	{Name: "ap-northeast-3", DisplayName: "Osaka (ap-northeast-3)"},
	{Name: "ap-northeast-2", DisplayName: "Seoul (ap-northeast-2)"},
	{Name: "ap-southeast-1", DisplayName: "Singapore (ap-southeast-1)"},
	{Name: "ap-southeast-2", DisplayName: "Sydney (ap-southeast-2)"},
	{Name: "ap-northeast-1", DisplayName: "Tokyo (ap-northeast-1)"},
	{Name: "ca-central-1", DisplayName: "Central (ca-central-1)"},
	{Name: "eu-central-1", DisplayName: "Frankfurt (eu-central-1)"},
	{Name: "eu-west-1", DisplayName: "Ireland (eu-west-1)"},
	{Name: "eu-west-2", DisplayName: "London (eu-west-2)"},
	{Name: "eu-south-1", DisplayName: "Milan (eu-south-1)"},
	{Name: "eu-west-3", DisplayName: "Paris (eu-west-3)"},
	{Name: "eu-north-1", DisplayName: "Stockholm (eu-north-1)"},
	{Name: "me-south-1", DisplayName: "Bahrain (me-south-1)"},
	{Name: "me-central-1", DisplayName: "UAE (me-central-1)"},
	{Name: "sa-east-1", DisplayName: "São Paulo (sa-east-1)"},
}

// https://cloud.google.com/sql/docs/mysql/locations
var supportedGCPRegions = []*commonpb.Region{
	{Name: "northamerica-northeast1", DisplayName: "Montréal (northamerica-northeast1)"},
	{Name: "northamerica-northeast2", DisplayName: "Toronto (northamerica-northeast2)"},
	{Name: "us-central1", DisplayName: "Iowa (us-central1)"},
	{Name: "us-south1", DisplayName: "Dallas (us-south1)"},
	{Name: "us-east1", DisplayName: "South Carolina (us-east1)"},
	{Name: "us-east4", DisplayName: "Northern Virginia (us-east4)"},
	{Name: "us-east5", DisplayName: "Columbus (us-east5)"},
	{Name: "us-west1", DisplayName: "Oregon (us-west1)"},
	{Name: "us-west2", DisplayName: "Los Angeles (us-west2)"},
	{Name: "us-west3", DisplayName: "Salt Lake City (us-west3)"},
	{Name: "us-west4", DisplayName: "Las Vegas (us-west4)"},
	{Name: "southamerica-east1", DisplayName: "São Paulo (southamerica-east1)"},
	{Name: "southamerica-west1", DisplayName: "Santiago (southamerica-west1)"},
	{Name: "europe-north1", DisplayName: "Finland (europe-north1)"},
	{Name: "europe-central2", DisplayName: "Warsaw (europe-central2)"},
	{Name: "europe-west1", DisplayName: "Belgium (europe-west1)"},
	{Name: "europe-west2", DisplayName: "London (europe-west2)"},
	{Name: "europe-west3", DisplayName: "Frankfurt (europe-west3)"},
	{Name: "europe-west4", DisplayName: "Netherlands (europe-west4)"},
	{Name: "europe-west6", DisplayName: "Zürich (europe-west6)"},
	{Name: "europe-west9", DisplayName: "Paris (europe-west9)"},
	{Name: "europe-southwest1", DisplayName: "Madrid (europe-southwest1)"},
	{Name: "me-west1", DisplayName: "Tel Aviv (me-west1)"},
	{Name: "asia-east1", DisplayName: "Taiwan (asia-east1)"},
	{Name: "asia-east2", DisplayName: "Hong Kong (asia-east2)"},
	{Name: "asia-northeast1", DisplayName: "Tokyo (asia-northeast1)"},
	{Name: "asia-northeast2", DisplayName: "Osaka (asia-northeast2)"},
	{Name: "asia-northeast3", DisplayName: "Seoul (asia-northeast3)"},
	{Name: "asia-south1", DisplayName: "Mumbai (asia-south1)"},
	{Name: "asia-south2", DisplayName: "Delhi (asia-south2)"},
	{Name: "asia-southeast1", DisplayName: "Singapore (asia-southeast1)"},
	{Name: "asia-southeast2", DisplayName: "Jakarta (asia-southeast2)"},
	{Name: "australia-southeast1", DisplayName: "Sydney (australia-southeast1)"},
	{Name: "australia-southeast2", DisplayName: "Melbourne (australia-southeast2)"},
}

// TODO complete azure mysql region list
var supportedAzureRegions = []*commonpb.Region{
	{Name: "eastus", DisplayName: "eastus"},
	{Name: "eastus2", DisplayName: "eastus2"},
	{Name: "southeastasia", DisplayName: "southeastasia"},
	{Name: "japaneast", DisplayName: "japaneast"},
}

var allConnMethods = []*dmpb.ConnMethod{
	{
		Name:        string(entity.ConnMethodDirect),
		DisplayName: entity.ConnMethodDirect.DisplayName(),
	},
	{
		Name:        string(entity.ConnMethodVPCPeering),
		DisplayName: entity.ConnMethodVPCPeering.DisplayName(),
	},
	{
		Name:        string(entity.ConnMethodPrivateLink),
		DisplayName: entity.ConnMethodPrivateLink.DisplayName(),
	},
}

var onlyPublicConnMethods = []*dmpb.ConnMethod{
	{
		Name:        string(entity.ConnMethodDirect),
		DisplayName: entity.ConnMethodDirect.DisplayName(),
	},
}

var publicAndPrivateLinkConnMethods = []*dmpb.ConnMethod{
	{
		Name:        string(entity.ConnMethodDirect),
		DisplayName: entity.ConnMethodDirect.DisplayName(),
	},
	{
		Name:        string(entity.ConnMethodPrivateLink),
		DisplayName: entity.ConnMethodPrivateLink.DisplayName(),
	},
}

var publicAndVPCPeeringConnMethods = []*dmpb.ConnMethod{
	{
		Name:        string(entity.ConnMethodDirect),
		DisplayName: entity.ConnMethodDirect.DisplayName(),
	},
	{
		Name:        string(entity.ConnMethodVPCPeering),
		DisplayName: entity.ConnMethodVPCPeering.DisplayName(),
	},
}

var awsSupportedSources = []*dmpb.SupportedSource{
	{
		Name:        string(entity.DBTypeMySQL),
		DisplayName: entity.DBTypeMySQL.DisplayName(),
		// no region info for self-hosted mysql
		ConnMethods: allConnMethods,
	},
	{
		Name:        string(entity.DBTypeAWSAuroraMySQL),
		DisplayName: entity.DBTypeAWSAuroraMySQL.DisplayName(),
		Regions:     supportedAWSRegions,
		ConnMethods: allConnMethods,
	},
	{
		Name:        string(entity.DBTypeAWSRDSMySQL),
		DisplayName: entity.DBTypeAWSRDSMySQL.DisplayName(),
		Regions:     supportedAWSRegions,
		ConnMethods: allConnMethods,
	},
	// temporarily comment the gcp cloud sql for aws
	/*
		{
			Name:        string(entity.DBTypeGoogleCloudMySQL),
			DisplayName: entity.DBTypeGoogleCloudMySQL.DisplayName(),
			Regions:     supportedGCPRegions,
			ConnMethods: onlyPublicConnMethods,
		},
	*/
}

var gcpSupportedSources = []*dmpb.SupportedSource{
	{
		Name:        string(entity.DBTypeMySQL),
		DisplayName: entity.DBTypeMySQL.DisplayName(),
		// no region info for self-hosted mysql
		ConnMethods: publicAndVPCPeeringConnMethods,
	},
	{
		Name:        string(entity.DBTypeAWSAuroraMySQL),
		DisplayName: entity.DBTypeAWSAuroraMySQL.DisplayName(),
		Regions:     supportedAWSRegions,
		ConnMethods: onlyPublicConnMethods,
	},
	{
		Name:        string(entity.DBTypeAWSRDSMySQL),
		DisplayName: entity.DBTypeAWSRDSMySQL.DisplayName(),
		Regions:     supportedAWSRegions,
		ConnMethods: onlyPublicConnMethods,
	},
	{
		Name:        string(entity.DBTypeGoogleCloudMySQL),
		DisplayName: entity.DBTypeGoogleCloudMySQL.DisplayName(),
		Regions:     supportedGCPRegions,
		ConnMethods: publicAndVPCPeeringConnMethods,
	},
}

var azureSupportedSources = []*dmpb.SupportedSource{
	{
		Name:        string(entity.DBTypeMySQL),
		DisplayName: entity.DBTypeMySQL.DisplayName(),
		// no region info for self-hosted mysql
		ConnMethods: onlyPublicConnMethods,
	},
	{
		Name:        string(entity.DBTypeAzureFlexibleMySQL),
		DisplayName: entity.DBTypeAzureFlexibleMySQL.DisplayName(),
		Regions:     supportedAzureRegions,
		ConnMethods: publicAndPrivateLinkConnMethods,
	},
	// {
	// 	Name:        string(entity.DBTypeAWSAuroraMySQL),
	// 	DisplayName: entity.DBTypeAWSAuroraMySQL.DisplayName(),
	// 	Regions:     supportedAWSRegions,
	// 	ConnMethods: onlyPublicConnMethods,
	// },
	// {
	// 	Name:        string(entity.DBTypeAWSRDSMySQL),
	// 	DisplayName: entity.DBTypeAWSRDSMySQL.DisplayName(),
	// 	Regions:     supportedAWSRegions,
	// 	ConnMethods: onlyPublicConnMethods,
	// },
	// {
	// 	Name:        string(entity.DBTypeGoogleCloudMySQL),
	// 	DisplayName: entity.DBTypeGoogleCloudMySQL.DisplayName(),
	// 	Regions:     supportedGCPRegions,
	// 	ConnMethods: onlyPublicConnMethods,
	// },
}

// price of private link endpoint.
// 0.01 per az, we create endpoint on 2 az
// https://github.com/tidbcloud/cluster-tier-config/blob/main/nightly/sku-dm.yaml (dev)
const privateLinkPrice float64 = 0.02

// SupportedSources returns sources we support.
func SupportedSources(vendor string) []*dmpb.SupportedSource {
	// clone it to avoid overwrite incorrectly.
	vd := entity.Cloud(vendor)
	if vd == entity.Cloud("") {
		vd = entity.Aws
	}
	var resp *dmpb.GetSupportedSourcesResp
	if vd == entity.Aws {
		resp = &dmpb.GetSupportedSourcesResp{
			Sources: awsSupportedSources,
		}
	} else if vd == entity.Gcp {
		resp = &dmpb.GetSupportedSourcesResp{
			Sources: gcpSupportedSources,
		}
	} else if vd == entity.Azure {
		resp = &dmpb.GetSupportedSourcesResp{
			Sources: azureSupportedSources,
		}
	} else {
		// should be unreachable
		return []*dmpb.SupportedSource{}
	}

	bytes, _ := protojson.Marshal(resp)
	result := &dmpb.GetSupportedSourcesResp{}
	_ = protojson.Unmarshal(bytes, result)
	return result.Sources
}

var mysqlSystemDBs = []string{
	"information_schema",
	"mysql",
	"performance_schema",
	"sys",
}

func DefaultIgnoreDBs() []string {
	return append([]string{}, mysqlSystemDBs...)
}

func IsMysqlSystemDB(db string) bool {
	for _, ignoreDB := range mysqlSystemDBs {
		if ignoreDB == db {
			return true
		}
	}
	return false
}

// GetPrivateLinkPrice get price of private-link endpoint.
func GetPrivateLinkPrice() float64 {
	return privateLinkPrice
}

// GetNodeRCUPrice get price of node resource of dm job.
func GetNodeRCUPrice(cloud, region string) (float64, error) {
	priceMap := config.Global().DMConfig.RCUPriceMap
	m := priceMap[cloud]
	if m == nil {
		return 0, errors.New("invalid cloud provider " + cloud)
	}
	price, ok := m[region]
	if !ok {
		return 0, errors.New("invalid region " + region)
	}
	return price, nil
}

// GenSourceID generate DM source id from connection profile id.
func GenSourceID(connProfileID entity.ID) string {
	return fmt.Sprintf("source-%d", connProfileID)
}

// UnpackSourceID unpack connection profile id from DM source id generated using GenSourceID.
func UnpackSourceID(id string) (uint64, error) {
	if !strings.HasPrefix(id, "source-") {
		return 0, errors.Errorf("Source ID %s is invalid", id)
	}
	connProfileIdStr := strings.TrimPrefix(id, "source-")
	connProfileId, err := strconv.ParseUint(connProfileIdStr, 10, 64)
	if err != nil {
		return 0, err
	}
	return connProfileId, nil
}

// NormalizeErr normalize err for ui display.
func NormalizeErr(err error, annotateMsg string) error {
	msg := err.Error()
	if strings.Contains(msg, "Access denied for user") {
		return errcode.NewInvalidInputErr(errors.New(
			"Access denied for the input user to connect the database. Please make sure you can log in to the database using given parameters.",
		))
	} else if strings.Contains(msg, "dial tcp") {
		return errcode.NewInvalidInputErr(errors.New(
			"Failed to connect to the database using given host and port. Please make sure you can log in to the database using given parameters.",
		))
	}
	return errcode.NewInvalidInputErr(errors.Annotate(err, annotateMsg))
}

// VpcPeeringHost2IP covert vpc peering host(dns, not ip) to ip addresses.
func VpcPeeringHost2IP(ctx context.Context, agentClient agent.AgentClient, addr string) ([]net.IP, error) {
	resp, err := agentClient.NsLookup(ctx, &agentpb.NsLookupReq{Host: addr})
	if err != nil {
		return nil, err
	}
	var ips []net.IP
	for _, ipStr := range resp.Ips {
		if ip := net.ParseIP(ipStr); ip != nil {
			ips = append(ips, ip)
		}
	}
	if len(ips) == 0 {
		return nil, errors.New("invalid ips from nslookup")
	}
	return ips, nil
}
