package network

import (
	"context"
	"fmt"

	"github.com/samber/lo"
	"github.com/tidbcloud/pkgv2/db"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	endpointrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/endpoint"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/common"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/infra/repoimpl/schema"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

type vpcPeeringImp struct {
	dbProvider db.Provider
}

func NewVpcPeeringRepo(dbProvider db.Provider) endpointrepo.VpcPeeringRepo {
	return &vpcPeeringImp{dbProvider: dbProvider}
}

func (v *vpcPeeringImp) Create(ctx context.Context, en *entity.VpcPeering) bizerr.BizError {
	wdb := v.dbProvider.WriteDB(ctx)
	vpcPeering, err := vpcPeeringEnToModel(en)
	if err != nil {
		return err
	}
	if err := wdb.Create(vpcPeering).Error; err != nil {
		return bizerr.WrapGormError(err, "create vpc peering")
	}
	return nil
}

func (v *vpcPeeringImp) Load(
	ctx context.Context,
	cloudProvider entity.CloudProviderName,
	id uint64,
) (*entity.VpcPeering, bizerr.BizError) {
	rdb := v.dbProvider.ReadDB(ctx)
	var vpcPeering any

	switch cloudProvider {
	case globalcommonv1.CloudProvider_AWS:
		vpcPeering = &schema.AwsVpcPeering{}
		rdb = rdb.Model(&schema.AwsVpcPeering{})
	case globalcommonv1.CloudProvider_GCP:
		vpcPeering = &schema.GcpVpcPeering{}
		rdb = rdb.Model(&schema.GcpVpcPeering{})
	case globalcommonv1.CloudProvider_AZURE:
		// TODO(@yifan): Support Azure in the future.
	}

	if err := rdb.Where("id=?", id).First(vpcPeering).Error; err != nil {
		return nil, bizerr.WrapGormError(err, "load vpc peering")
	}

	return vpcPeeringModelToEn(vpcPeering)
}

func (v *vpcPeeringImp) Delete(ctx context.Context, vpcPeering *entity.VpcPeering) bizerr.BizError {
	if err := metadata.Validate(ctx, &metadata.ExpectRequest[uint64]{
		TenantId:   vpcPeering.GetOrganizationId(),
		ProjectId:  vpcPeering.GetProjectId(),
		ResourceId: fmt.Sprintf("%d", vpcPeering.GetId()),
	}); err != nil {
		return bizerr.WrapError(err, "validate failed")
	}

	wdb := v.dbProvider.WriteDB(ctx)
	if err := wdb.Delete(vpcPeeringEnToModel(vpcPeering)).Error; err != nil {
		return bizerr.WrapGormError(err, "delete vpc peering")
	}
	return nil
}

func (v *vpcPeeringImp) Query(
	ctx context.Context,
	filter *endpointrepo.VpcPeeringFilter,
	pager *globaldedicatedv1.Pager,
	sorter *globaldedicatedv1.Sorter,
) ([]*entity.VpcPeering, bizerr.BizError) {
	if len(filter.CloudProviderNames) == 0 {
		filter.CloudProviderNames = []globalcommonv1.CloudProvider_Name{
			globalcommonv1.CloudProvider_AWS,
			globalcommonv1.CloudProvider_GCP,
			globalcommonv1.CloudProvider_AZURE,
		}
	}

	var sorters []*globaldedicatedv1.Sorter
	if sorter != nil {
		sorters = append(sorters, sorter)
	}
	rdb := v.dbProvider.ReadDB(ctx)
	rdb = buildTxByVpcPeeringFilter(rdb, filter)
	rdb = common.SetOrderByIntoTx(common.SetOffsetAndLimitIntoTx(rdb, pager), sorters)

	var out []*entity.VpcPeering

	for _, p := range filter.CloudProviderNames {
		switch p {
		case globalcommonv1.CloudProvider_AWS:
			var vpcPeeringList []*schema.AwsVpcPeering
			if err := rdb.Model(&schema.AwsVpcPeering{}).Find(&vpcPeeringList).Error; err != nil {
				return nil, bizerr.WrapGormError(err, "find aws vpc peering")
			}
			for _, p := range vpcPeeringList {
				en, err := vpcPeeringModelToEn(p)
				if err != nil {
					return nil, err
				}
				out = append(out, en)
			}

		case globalcommonv1.CloudProvider_GCP:
			var vpcPeeringList []*schema.GcpVpcPeering
			if err := rdb.Model(&schema.GcpVpcPeering{}).Find(&vpcPeeringList).Error; err != nil {
				return nil, bizerr.WrapGormError(err, "find gcp vpc peering")
			}
			for _, p := range vpcPeeringList {
				en, err := vpcPeeringModelToEn(p)
				if err != nil {
					return nil, err
				}
				out = append(out, en)
			}

		case globalcommonv1.CloudProvider_AZURE:
			// TODO(@yifan): Support Azure in the future.

		default:
			return nil, bizerr.New(bizerr.ErrCodeParams, "unknown cloud provider: %s", filter.CloudProviderNames)
		}
	}

	return out, nil
}

func vpcPeeringTypePbToModel(tp globaldedicatedv1.VpcPeering_Type) string {
	switch tp {
	case globaldedicatedv1.VpcPeering_CUSTOMER:
		return "customer"
	case globaldedicatedv1.VpcPeering_DBAAS:
		return "dbaas"
	case globaldedicatedv1.VpcPeering_CUSTOMER_READONLY:
		return "customer_readonly"
	default:
		panic(fmt.Sprintf("unknown peering type: %s", tp))
	}
}

func vpcPeeringEnToModel(en *entity.VpcPeering) (any, bizerr.BizError) {
	switch en.CloudProvider {
	case globalcommonv1.CloudProvider_AWS:
		return &schema.AwsVpcPeering{
			Model: gorm.Model{
				ID: uint(en.GetId()),
			},
			TenantID:         en.GetOrganizationId(),
			ProjectID:        en.GetProjectId(),
			PeeringType:      vpcPeeringTypePbToModel(en.Type),
			Region:           en.GetTidbCloudRegion(),
			PeerRegion:       en.GetCustomerRegion(),
			PeerAwsAccountID: en.Aws.GetCustomerAccountId(),
			PeerAwsVpcID:     en.Aws.GetCustomerVpcId(),
			PeerVpcCidr:      en.GetCustomerCidrBlock(),
		}, nil
	case globalcommonv1.CloudProvider_GCP:
		return &schema.GcpVpcPeering{
			Model: gorm.Model{
				ID: uint(en.GetId()),
			},
			TenantID:           en.GetOrganizationId(),
			ProjectID:          en.GetProjectId(),
			PeeringType:        vpcPeeringTypePbToModel(en.Type),
			Region:             en.GetTidbCloudRegion(),
			PeerRegion:         en.GetCustomerRegion(),
			PeerGcpProjectID:   en.Gcp.GetCustomerProjectId(),
			PeerGcpNetworkName: en.Gcp.GetCustomerNetworkName(),
			PeerVpcCidr:        en.GetCustomerCidrBlock(),
		}, nil
	case globalcommonv1.CloudProvider_AZURE:
		// TODO(@yifan): Support Azure in the future.
		return &schema.AzureVpcPeering{}, nil
	default:
		return nil, bizerr.New(bizerr.ErrCodeParams, "unknown cloud provider: %s", en.CloudProvider)
	}
}

func vpcPeeringTypeModelToPb(tp string) globaldedicatedv1.VpcPeering_Type {
	switch tp {
	case "customer":
		return globaldedicatedv1.VpcPeering_CUSTOMER
	case "dbaas":
		return globaldedicatedv1.VpcPeering_DBAAS
	case "customer_readonly":
		return globaldedicatedv1.VpcPeering_CUSTOMER_READONLY
	default:
		panic(fmt.Sprintf("unknown peering type: %s", tp))
	}
}

func vpcPeeringModelToEn(vpcPeering any) (*entity.VpcPeering, bizerr.BizError) {
	var out *entity.VpcPeering

	switch p := vpcPeering.(type) {
	case *schema.AwsVpcPeering:
		out = &entity.VpcPeering{
			Id:                uint64(p.ID),
			OrganizationId:    p.TenantID,
			ProjectId:         p.ProjectID,
			CreateAt:          timestamppb.New(p.CreatedAt),
			Type:              vpcPeeringTypeModelToPb(p.PeeringType),
			CloudProvider:     globalcommonv1.CloudProvider_AWS,
			CustomerCidrBlock: p.PeerVpcCidr,
			TidbCloudRegion:   p.Region,
			CustomerRegion:    p.PeerRegion,
			Aws: &globaldedicatedv1.VpcPeering_Aws{
				CustomerAccountId: p.PeerAwsAccountID,
				CustomerVpcId:     p.PeerAwsVpcID,
			},
		}
	case *schema.GcpVpcPeering:
		out = &entity.VpcPeering{
			Id:                uint64(p.ID),
			OrganizationId:    p.TenantID,
			ProjectId:         p.ProjectID,
			CreateAt:          timestamppb.New(p.CreatedAt),
			Type:              vpcPeeringTypeModelToPb(p.PeeringType),
			CloudProvider:     globalcommonv1.CloudProvider_GCP,
			CustomerCidrBlock: p.PeerVpcCidr,
			TidbCloudRegion:   p.Region,
			CustomerRegion:    p.PeerRegion,
			Gcp: &globaldedicatedv1.VpcPeering_Gcp{
				CustomerProjectId:   p.PeerGcpProjectID,
				CustomerNetworkName: p.PeerGcpNetworkName,
			},
		}
	case *schema.AzureVpcPeering:
		// TODO(@yifan): Support Azure in the future.

	default:
		return nil, bizerr.New(bizerr.ErrCodeParams, "unknown vpc peering type: %T", vpcPeering)
	}

	return out, nil
}

func buildTxByVpcPeeringFilter(tx *gorm.DB, filter *endpointrepo.VpcPeeringFilter) *gorm.DB {
	if len(filter.RegionNames) != 0 {
		tx = tx.Where(
			"region IN (?)",
			lo.Map(filter.RegionNames, func(i globalcommonv1.Region_Name, _ int) string { return i.String() }),
		)
	}
	if len(filter.IDs) != 0 {
		tx = tx.Where("id IN (?)", filter.IDs)
	}
	if len(filter.ProjectIds) != 0 {
		tx = tx.Where("project_id IN (?)", filter.ProjectIds)
	}
	if len(filter.OrganizationIds) != 0 {
		tx = tx.Where("tenant_id IN (?)", filter.OrganizationIds)
	}
	if len(filter.Types) != 0 {
		tx = tx.Where(
			"peering_type IN (?)",
			lo.Map(
				filter.Types,
				func(i globaldedicatedv1.VpcPeering_Type, _ int) string { return vpcPeeringTypePbToModel(i) },
			),
		)
	}
	return tx
}
