package regionalservice

import (
	"context"
	stderrors "errors"
	"fmt"

	awssdk "github.com/aws/aws-sdk-go-v2/aws"
	awsconfig "github.com/aws/aws-sdk-go-v2/config"
	ec2api "github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/smithy-go"

	v1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"

	"github.com/tidbcloud/observability/pkg/common/configs"
	"github.com/tidbcloud/observability/pkg/common/constants"
	o11yerrors "github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/common/structs"
)

type byocRouteClient interface {
	DescribeRouteTables(ctx context.Context, input *ec2api.DescribeRouteTablesInput, optFns ...func(*ec2api.Options)) (*ec2api.DescribeRouteTablesOutput, error)
	CreateRoute(ctx context.Context, input *ec2api.CreateRouteInput, optFns ...func(*ec2api.Options)) (*ec2api.CreateRouteOutput, error)
	DeleteRoute(ctx context.Context, input *ec2api.DeleteRouteInput, optFns ...func(*ec2api.Options)) (*ec2api.DeleteRouteOutput, error)
}

func ensureBYOCVPCPeeringRoutesFromRequest(ctx context.Context, req message.CreateClusterReq) error {
	if req.BYOCVPCPeering == nil {
		return nil
	}
	serviceInfo := configs.GetServiceInfo()
	if serviceInfo.Vendor != string(constants.AWS) {
		return nil
	}

	cfg, err := awsconfig.LoadDefaultConfig(ctx)
	if err != nil {
		return o11yerrors.Wrap(o11yerrors.AWSClientError, "failed to load AWS config for BYOC VPC peering routes", err)
	}
	cfg.Region = serviceInfo.Region
	return ensureBYOCVPCPeeringRoutes(ctx, req, serviceInfo.BYOCVPCPeering, ec2api.NewFromConfig(cfg))
}

func ensureBYOCVPCPeeringRoutes(ctx context.Context, req message.CreateClusterReq, cfg structs.BYOCVPCPeeringConfig, cli byocRouteClient) error {
	if req.BYOCVPCPeering == nil {
		return nil
	}
	peeringID := req.BYOCVPCPeering.PeeringConnectionID
	dataplaneCIDR := req.BYOCVPCPeering.DataplaneVPCCIDR
	if peeringID == "" || dataplaneCIDR == "" {
		return o11yerrors.New(o11yerrors.ParameterInvalid, "byocVpcPeering requires peeringConnectionID and dataplaneVpcCIDR")
	}
	if len(cfg.RouteTableIDs) == 0 {
		return o11yerrors.New(o11yerrors.ConfigInvalid, "byocVpcPeering routeTableIDs is empty")
	}

	routeTables, err := cli.DescribeRouteTables(ctx, &ec2api.DescribeRouteTablesInput{RouteTableIds: cfg.RouteTableIDs})
	if err != nil {
		return o11yerrors.Wrap(o11yerrors.AWSClientError, "failed to describe o11y route tables for BYOC VPC peering", err)
	}

	seen := make(map[string]struct{}, len(routeTables.RouteTables))
	for _, routeTable := range routeTables.RouteTables {
		routeTableID := awssdk.ToString(routeTable.RouteTableId)
		seen[routeTableID] = struct{}{}
		found := false
		for _, route := range routeTable.Routes {
			if awssdk.ToString(route.DestinationCidrBlock) != dataplaneCIDR {
				continue
			}
			found = true
			if awssdk.ToString(route.VpcPeeringConnectionId) != peeringID {
				return o11yerrors.NewErrorf(o11yerrors.AWSClientError, "route table %s route %s already targets %s", routeTableID, dataplaneCIDR, awssdk.ToString(route.VpcPeeringConnectionId))
			}
		}
		if found {
			continue
		}
		if _, err := cli.CreateRoute(ctx, &ec2api.CreateRouteInput{
			RouteTableId:           awssdk.String(routeTableID),
			DestinationCidrBlock:   awssdk.String(dataplaneCIDR),
			VpcPeeringConnectionId: awssdk.String(peeringID),
		}); err != nil {
			return o11yerrors.Wrap(o11yerrors.AWSClientError, fmt.Sprintf("failed to create BYOC VPC peering route in route table %s", routeTableID), err)
		}
	}

	for _, routeTableID := range cfg.RouteTableIDs {
		if _, ok := seen[routeTableID]; !ok {
			return o11yerrors.NewErrorf(o11yerrors.ConfigInvalid, "o11y route table %s not found", routeTableID)
		}
	}
	return nil
}

func deleteBYOCVPCPeeringRoutesFromCluster(ctx context.Context, cluster *v1.Cluster) error {
	if cluster == nil {
		return nil
	}
	serviceInfo := configs.GetServiceInfo()
	if serviceInfo.Vendor != string(constants.AWS) {
		return nil
	}

	peeringID := cluster.Annotations[constants.AnnotationBYOCVPCPeeringID]
	dataplaneCIDR := cluster.Annotations[constants.AnnotationBYOCDataplaneVPCCIDR]
	if peeringID == "" && dataplaneCIDR == "" {
		return nil
	}
	if peeringID == "" || dataplaneCIDR == "" {
		return o11yerrors.New(o11yerrors.ConfigInvalid, "byocVpcPeering cleanup requires peeringConnectionID and dataplaneVpcCIDR")
	}

	cfg, err := awsconfig.LoadDefaultConfig(ctx)
	if err != nil {
		return o11yerrors.Wrap(o11yerrors.AWSClientError, "failed to load AWS config for BYOC VPC peering route cleanup", err)
	}
	cfg.Region = serviceInfo.Region
	return deleteBYOCVPCPeeringRoutes(ctx, peeringID, dataplaneCIDR, serviceInfo.BYOCVPCPeering, ec2api.NewFromConfig(cfg))
}

func deleteBYOCVPCPeeringRoutes(ctx context.Context, peeringID, dataplaneCIDR string, cfg structs.BYOCVPCPeeringConfig, cli byocRouteClient) error {
	if peeringID == "" || dataplaneCIDR == "" {
		return o11yerrors.New(o11yerrors.ConfigInvalid, "byocVpcPeering cleanup requires peeringConnectionID and dataplaneVpcCIDR")
	}
	if len(cfg.RouteTableIDs) == 0 {
		return o11yerrors.New(o11yerrors.ConfigInvalid, "byocVpcPeering routeTableIDs is empty")
	}

	routeTables, err := cli.DescribeRouteTables(ctx, &ec2api.DescribeRouteTablesInput{RouteTableIds: cfg.RouteTableIDs})
	if err != nil {
		return o11yerrors.Wrap(o11yerrors.AWSClientError, "failed to describe o11y route tables for BYOC VPC peering cleanup", err)
	}

	seen := make(map[string]struct{}, len(routeTables.RouteTables))
	for _, routeTable := range routeTables.RouteTables {
		routeTableID := awssdk.ToString(routeTable.RouteTableId)
		seen[routeTableID] = struct{}{}
		for _, route := range routeTable.Routes {
			if awssdk.ToString(route.DestinationCidrBlock) != dataplaneCIDR {
				continue
			}
			if awssdk.ToString(route.VpcPeeringConnectionId) != peeringID {
				return o11yerrors.NewErrorf(o11yerrors.AWSClientError, "route table %s route %s targets unexpected %s", routeTableID, dataplaneCIDR, awssdk.ToString(route.VpcPeeringConnectionId))
			}
			_, err := cli.DeleteRoute(ctx, &ec2api.DeleteRouteInput{
				RouteTableId:         awssdk.String(routeTableID),
				DestinationCidrBlock: awssdk.String(dataplaneCIDR),
			})
			if isRouteNotFound(err) {
				continue
			}
			if err != nil {
				return o11yerrors.Wrap(o11yerrors.AWSClientError, fmt.Sprintf("failed to delete BYOC VPC peering route from route table %s", routeTableID), err)
			}
		}
	}

	for _, routeTableID := range cfg.RouteTableIDs {
		if _, ok := seen[routeTableID]; !ok {
			return o11yerrors.NewErrorf(o11yerrors.ConfigInvalid, "o11y route table %s not found", routeTableID)
		}
	}
	return nil
}

func isRouteNotFound(err error) bool {
	var apiErr smithy.APIError
	return stderrors.As(err, &apiErr) && apiErr.ErrorCode() == "InvalidRoute.NotFound"
}
