package entity

import (
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/pkgv2/bizerr"
	"github.com/tidbcloud/serverless-service/internal/config"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
)

// AWSEndpointService maps to JSON column `aws_endpoint_service`.
type AWSEndpointService struct {
	EndpointServiceName   string            `json:"endpointServiceName"`
	EndpointServiceRegion string            `json:"endpointServiceRegion,omitempty"`
	EndpointState         string            `json:"endpointState,omitempty"`
	EndpointID            string            `json:"endpointID,omitempty"`
	Domains               []string          `json:"domains,omitempty"`
	EndpointAzIdToName    map[string]string `json:"endpointAzIdToName,omitempty"`
}

// AlicloudEndpointService maps to JSON column `alicloud_endpoint_service`.
type AlicloudEndpointService struct {
	EndpointServiceName string   `json:"endpointServiceName"`
	EndpointID          string   `json:"endpointID,omitempty"`
	EndpointState       string   `json:"endpointState,omitempty"`
	ConnectionState     string   `json:"connectionState,omitempty"`
	Domain              string   `json:"domain,omitempty"`
	EndpointAzs         []string `json:"endpointAzs,omitempty"`
}

// AWSResourceConfiguration maps to JSON column `aws_resource_configuration`.
type AWSResourceConfiguration struct {
	ARN     string   `json:"arn"`
	Domains []string `json:"domains,omitempty"`
}

// AmazonMSKProvisioned maps to JSON column `amazon_msk_provisioned`.
type AmazonMSKProvisioned struct {
	MSKArn                string            `json:"mskArn"`
	MSKVPCConnectionArn   string            `json:"mskVpcConnectionArn,omitempty"`
	MSKVPCConnectionState string            `json:"mskVpcConnectionState,omitempty"`
	BootstrapBroker       string            `json:"bootstrapBroker,omitempty"`
	EndpointAzIdToName    map[string]string `json:"endpointAzIdToName,omitempty"`
}

type PrivateLinkConnection struct {
	ID                          string                              `gorm:"column:id;primaryKey;type:varchar(255);not null"`
	ClusterID                   string                              `gorm:"column:cluster_id;type:varchar(255);not null;index:idx_cluster_id"`
	TenantID                    string                              `gorm:"column:tenant_id;type:varchar(255);not null"`
	CloudProvider               string                              `gorm:"column:cloud_provider;type:varchar(255);not null"`
	Region                      string                              `gorm:"column:region;type:varchar(255);not null"`
	Kubernetes                  string                              `gorm:"column:kubernetes;type:varchar(255);not null"`
	DisplayName                 string                              `gorm:"column:display_name;type:varchar(255);not null"`
	State                       constant.PrivateLinkConnectionState `gorm:"column:state;type:varchar(255);not null;index:idx_state"`
	Message                     string                              `gorm:"column:message;type:text;default:null"`
	CreatedBy                   string                              `gorm:"column:created_by;type:varchar(255);not null"`
	CreatedAt                   time.Time                           `gorm:"column:created_at;type:timestamp;not null;autoCreateTime;index:idx_create_at"`
	UpdatedAt                   *time.Time                          `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
	Type                        constant.PrivateLinkConnectionType  `gorm:"column:type;type:varchar(255);not null;index:idx_type"`
	AWSEndpointServiceName      string                              `gorm:"column:aws_endpoint_service_name;type:varchar(255);default:null"`
	AlicloudEndpointServiceName string                              `gorm:"column:alicloud_endpoint_service_name;type:varchar(255);default:null"`
	AWSResourceConfigurationARN string                              `gorm:"column:aws_resource_configuration_arn;type:varchar(255);default:null"`
	AmazonMSKProvisionedArn     string                              `gorm:"column:amazon_msk_provisioned_arn;type:varchar(255);default:null"`
	AWSEndpointService          *AWSEndpointService                 `gorm:"column:aws_endpoint_service;type:json;serializer:json;default:null"`
	AlicloudEndpointService     *AlicloudEndpointService            `gorm:"column:alicloud_endpoint_service;type:json;serializer:json;default:null"`
	AWSResourceConfiguration    *AWSResourceConfiguration           `gorm:"column:aws_resource_configuration;type:json;serializer:json;default:null"`
	AmazonMSKProvisioned        *AmazonMSKProvisioned               `gorm:"column:amazon_msk_provisioned;type:json;serializer:json;default:null"`
	ClusterStatusId             uint64                              `gorm:"column:cluster_status_id;type:bigint;null;default:null"`
}

// TableName returns the DB table name.
func (p *PrivateLinkConnection) TableName() string {
	return "private_link_connections"
}

type PrivateLinkConnectionLink struct {
	ClusterID               string     `gorm:"column:cluster_id;type:varchar(255);not null"`
	PrivateLinkConnectionID string     `gorm:"column:private_link_connection_id;type:varchar(255);not null;index:idx_private_link_connection_id"`
	BizID                   string     `gorm:"column:biz_id;type:varchar(255);not null;uniqueIndex"`
	CreatedAt               time.Time  `gorm:"column:created_at;type:timestamp;not null;autoCreateTime"`
	UpdatedAt               *time.Time `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
}

// TableName returns the DB table name.
func (l *PrivateLinkConnectionLink) TableName() string {
	return "private_link_connection_links"
}

type ListPrivateLinkConnectionFilter struct {
	ClusterID                   string
	TenantID                    string
	Region                      string
	Kubernetes                  string
	States                      []constant.PrivateLinkConnectionState
	Offset                      *int64
	Limit                       *int32
	OrderBy                     string
	Type                        constant.PrivateLinkConnectionType
	StatesNotIn                 []constant.PrivateLinkConnectionState
	AWSEndpointServiceName      string
	AlicloudEndpointServiceName string
	AmazonMSKProvisionedArn     string
	IncludeSharedMSK            bool
}

func (p *PrivateLinkConnection) Validate(cluster *tidbmgmtservicev1.Cluster) bizerr.BizError {
	if p.DisplayName == "" || len(p.DisplayName) > 100 {
		return bizerr.New(bizerr.ErrCodeParams, "display name must be between 1 and 100 characters")
	}
	switch p.Type {
	case constant.PrivateLinkConnectionAWSEndpointService:
		if cluster.CloudProvider != config.ProviderAWS {
			return bizerr.New(bizerr.ErrCodeParams,
				"aws endpoint service private link connection can only be created for aws cluster")
		}
	case constant.PrivateLinkConnectionAlicloudEndpointService:
		if cluster.CloudProvider != config.ProviderAliCloud {
			return bizerr.New(bizerr.ErrCodeParams,
				"alicloud endpoint service private link connection can only be created for alicloud cluster")
		}
	case constant.PrivateLinkConnectionAmazonMSKProvisioned:
		if cluster.CloudProvider != config.ProviderAWS {
			return bizerr.New(bizerr.ErrCodeParams,
				"amazon msk provisioned private link connection can only be created for aws cluster")
		}
	}
	return nil
}

func (p *PrivateLinkConnection) FillWithCluster(cluster *tidbmgmtservicev1.Cluster) {
	p.ClusterID = strconv.FormatUint(cluster.ClusterId, 10)
	p.CloudProvider = cluster.CloudProvider
	p.TenantID = strconv.FormatUint(cluster.OrganizationId, 10)
	p.Region = cluster.Region
	p.Kubernetes = cluster.GetServerlessCluster().GetCluster().GetKubernetes()
}

func (p *PrivateLinkConnection) SyncAWSEndpointService(endpoint *regionalserverlessv1.AWSVpcEndpoint) {
	p.AWSEndpointService.EndpointState = endpoint.State.String()
	switch endpoint.State {
	case regionalserverlessv1.AWSVpcEndpointState_PENDING_ACCEPTANCE,
		regionalserverlessv1.AWSVpcEndpointState_REJECTED,
		regionalserverlessv1.AWSVpcEndpointState_PENDING:
		p.State = constant.PrivateLinkConnectionStatePendingAcceptance
	case regionalserverlessv1.AWSVpcEndpointState_EXPIRED:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = "The AWS VPC Endpoint has expired, please create a new private link connection."
	case regionalserverlessv1.AWSVpcEndpointState_FAILED:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = endpoint.FailureMessage
	case regionalserverlessv1.AWSVpcEndpointState_DELETED:
		p.State = constant.PrivateLinkConnectionStateDeleted
	case regionalserverlessv1.AWSVpcEndpointState_DELETING:
		p.State = constant.PrivateLinkConnectionStateDeleting
	case regionalserverlessv1.AWSVpcEndpointState_AVAILABLE:
		p.State = constant.PrivateLinkConnectionStateAvailable
		p.AWSEndpointService.Domains = endpoint.Domains
		p.AWSEndpointService.EndpointAzIdToName = endpoint.AvailableZones
	default:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = "Unknown VPC Endpoint state, please contact us for support."
	}
}

func (p *PrivateLinkConnection) SyncAliCloudEndpointService(endpoint *regionalserverlessv1.AliCloudVpcEndpoint) {
	p.AlicloudEndpointService.EndpointState = endpoint.State.String()
	p.AlicloudEndpointService.ConnectionState = endpoint.ConnectionState.String()
	switch endpoint.State {
	case regionalserverlessv1.AliCloudVpcEndpointState_PENDING,
		regionalserverlessv1.AliCloudVpcEndpointState_CREATING:
		p.State = constant.PrivateLinkConnectionStateCreating
	case regionalserverlessv1.AliCloudVpcEndpointState_ACTIVE:
		switch endpoint.ConnectionState {
		case regionalserverlessv1.AliCloudVpcEndpointConnectionState_CONNECTED:
			p.State = constant.PrivateLinkConnectionStateAvailable
			p.AlicloudEndpointService.Domain = endpoint.Domain
		case regionalserverlessv1.AliCloudVpcEndpointConnectionState_CONNECTING,
			regionalserverlessv1.AliCloudVpcEndpointConnectionState_PENDING,
			regionalserverlessv1.AliCloudVpcEndpointConnectionState_DISCONNECTED,
			regionalserverlessv1.AliCloudVpcEndpointConnectionState_DISCONNECTING:
			p.State = constant.PrivateLinkConnectionStatePendingAcceptance
		case regionalserverlessv1.AliCloudVpcEndpointConnectionState_DELETING:
			p.State = constant.PrivateLinkConnectionStateDeleting
		default:
			p.State = constant.PrivateLinkConnectionStateFailed
			p.Message = "Unknown VPC Endpoint connection state, please contact us for support."
		}
	case regionalserverlessv1.AliCloudVpcEndpointState_DELETING:
		p.State = constant.PrivateLinkConnectionStateDeleting
	default:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = "Unknown VPC Endpoint state, please contact us for support."
	}
}

func (p *PrivateLinkConnection) SyncAmazonMSKProvisioned(connection *regionalserverlessv1.MSKVpcConnection) {
	p.AmazonMSKProvisioned.MSKVPCConnectionState = connection.State.String()
	switch connection.State {
	case regionalserverlessv1.MSKVpcConnectionState_CREATING:
		p.State = constant.PrivateLinkConnectionStateCreating
	case regionalserverlessv1.MSKVpcConnectionState_AVAILABLE:
		p.State = constant.PrivateLinkConnectionStateAvailable
		p.AmazonMSKProvisioned.EndpointAzIdToName = connection.AvailableZones
	case regionalserverlessv1.MSKVpcConnectionState_DELETING:
		p.State = constant.PrivateLinkConnectionStateDeleting
	case regionalserverlessv1.MSKVpcConnectionState_FAILED:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = "MSK VPC connection failed, please contact support."
	case regionalserverlessv1.MSKVpcConnectionState_REJECTED,
		regionalserverlessv1.MSKVpcConnectionState_REJECTING:
		p.State = constant.PrivateLinkConnectionStatePendingAcceptance
	case regionalserverlessv1.MSKVpcConnectionState_INACTIVE,
		regionalserverlessv1.MSKVpcConnectionState_DEACTIVATING:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = fmt.Sprintf(
			"MSK VPC connection is in %s state, please check the status of your MSK cluster",
			connection.State.String(),
		)
	default:
		p.State = constant.PrivateLinkConnectionStateFailed
		p.Message = "Unknown MSK VPC connection state, please contact support."
	}
}

type PLCAttachDomain struct {
	ID                      string                       `gorm:"column:id;primaryKey;type:varchar(255);not null"`
	ClusterID               string                       `gorm:"column:cluster_id;type:varchar(255);not null"`
	CloudProvider           string                       `gorm:"column:cloud_provider;type:varchar(255);not null"`
	PrivateLinkConnectionID string                       `gorm:"column:private_link_connection_id;type:varchar(255);not null"`
	Type                    constant.PLCAttachDomainType `gorm:"column:type;type:varchar(64);not null"`
	CreatedBy               string                       `gorm:"column:created_by;type:varchar(255);not null"`
	CreatedAt               time.Time                    `gorm:"column:created_at;type:timestamp;not null;autoCreateTime"`
	UpdatedAt               *time.Time                   `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
	UniqueID                string                       `gorm:"column:unique_id;type:varchar(64);not null;uniqueIndex:uk_hu"`
	HostedZone              string                       `gorm:"column:hosted_zone;type:varchar(64);not null;uniqueIndex:uk_hu"`
	HostedZoneID            string                       `gorm:"column:hosted_zone_id;type:varchar(255);default:null"`
	VPCAttached             bool                         `gorm:"column:vpc_attached;type:tinyint(1);not null;default:0"`
	Records                 []*PLCDomainRecord           `gorm:"-:all"`
}

func (p *PLCAttachDomain) TableName() string {
	return "plc_attach_domains"
}

type ListPLCAttachDomainsFilter struct {
	ClusterID                string
	PrivateLinkConnectionID  string
	PrivateLinkConnectionIDs []string
}

type PLCDomainRecord struct {
	AttachDomainID          string                        `gorm:"column:attach_domain_id;type:varchar(255);not null"`
	ClusterID               string                        `gorm:"column:cluster_id;type:varchar(255);not null"`
	CloudProvider           string                        `gorm:"column:cloud_provider;type:varchar(255);not null"`
	PrivateLinkConnectionID string                        `gorm:"column:private_link_connection_id;type:varchar(255);not null;index:idx_plc"`
	Record                  string                        `gorm:"column:record;type:varchar(255);not null"`
	Value                   string                        `gorm:"column:value;type:varchar(255);not null"`
	State                   constant.PLCDomainRecordState `gorm:"column:state;type:varchar(64);not null;index:idx_state"`
	// use create/delete change id to query change status manually when needed
	AWSRoute53CreateChangeID string     `gorm:"column:aws_route53_create_change_id;type:varchar(255);default:null"`
	AWSRoute53DeleteChangeID string     `gorm:"column:aws_route53_delete_change_id;type:varchar(255);default:null"`
	AliDNSRecordID           int64      `gorm:"column:ali_dns_record_id;type:varchar(255);default:null"`
	Message                  string     `gorm:"column:message;type:text;default:null"`
	CreatedAt                time.Time  `gorm:"column:created_at;type:timestamp;not null;autoCreateTime"`
	UpdatedAt                *time.Time `gorm:"column:updated_at;type:timestamp;null;default:null;autoUpdateTime"`
}

func (p *PLCDomainRecord) TableName() string {
	return "plc_domain_records"
}

type ListPLCDomainRecordsFilter struct {
	ClusterID                string
	PrivateLinkConnectionID  string
	AttachDomainID           string
	PrivateLinkConnectionIDs []string
	States                   []constant.PLCDomainRecordState
}

func (p *PLCAttachDomain) Validate(plc *PrivateLinkConnection) bizerr.BizError {
	if plc.State != constant.PrivateLinkConnectionStateAvailable {
		return bizerr.New(bizerr.ErrCodeParams, "private link connection is not in active state")
	}

	switch plc.Type {
	case constant.PrivateLinkConnectionAWSEndpointService:
		if len(plc.AWSEndpointService.EndpointAzIdToName) == 0 {
			return bizerr.New(bizerr.ErrCodeUnknown, "private link connection missing azs info")
		}
		if len(plc.AWSEndpointService.Domains) == 0 {
			return bizerr.New(bizerr.ErrCodeUnknown, "private link connection missing domains info")
		}
	case constant.PrivateLinkConnectionAlicloudEndpointService:
		if p.Type == constant.PLCAttachDomainTypeConfluent {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"can not attach confluent domain to private link connection in `alicloud endpoint service` type",
			)
		}
		if plc.AlicloudEndpointService.Domain == "" {
			return bizerr.New(bizerr.ErrCodeUnknown, "private link connection missing domain info")
		}
	default:
		return bizerr.New(
			bizerr.ErrCodeParams,
			"can not attach domain to private link connection in `%s` type",
			plc.Type,
		)
	}

	switch p.Type {
	case constant.PLCAttachDomainTypeTiDBCloudManaged:
		if strings.Contains(p.UniqueID, ".") || strings.Contains(p.UniqueID, "*") {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"invalid unique id for TiDB Cloud Managed domain, please do not contain '.' or '*'",
			)
		}
	case constant.PLCAttachDomainTypeConfluent:
		// format: domjg5k6lrg.us-east-1
		if strings.Contains(p.UniqueID, "*") || len(strings.Split(p.UniqueID, ".")) != 2 {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"invalid unique id for Confluent domain, please follow the format '<confluent_id>.<region>'",
			)
		}
	}
	return nil
}

func (p *PLCAttachDomain) FillInField(plc *PrivateLinkConnection) {
	p.CloudProvider = plc.CloudProvider
	switch p.Type {
	case constant.PLCAttachDomainTypeTiDBCloudManaged:
		p.HostedZone = fmt.Sprintf("%s.%s", p.CloudProvider, constant.HostedZoneTiDBCloudManaged)
	case constant.PLCAttachDomainTypeConfluent:
		p.HostedZone = fmt.Sprintf("%s.%s.%s", p.UniqueID, p.CloudProvider, constant.HostedZoneConfluent)
	}
	p.ID = GenerateIDWithPrefix(constant.PLCAttachDomainIDPrefix)
	// unique id can only be empty when create plc attach domain for TiDB Cloud Managed type in dry run mode
	if p.UniqueID == "" {
		p.UniqueID = GenerateIDWithPrefix(constant.PLCHostedZonePrefix)
	}
}
