package alicloud

import (
	"context"
	"fmt"
	"strconv"

	alivpcv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/vpc/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpcpeering/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/vpcpeering/task/common"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
)

type VPCPeeringConnection struct {
	*task.BaseTask
	manager common.RouteManager
}

func (t *VPCPeeringConnection) Name() string {
	return "VPCPeeringConnection"
}

func NewVPCPeeringConnection(ctx *task.Context) *VPCPeeringConnection {
	return &VPCPeeringConnection{
		BaseTask: &task.BaseTask{
			Context: ctx,
		},
		manager: NewAliCloudRouteManager(
			ctx.Local,
			ctx.VPCPeering,
			ctx.ProviderConfigName(),
		),
	}
}

func (t *VPCPeeringConnection) Create(ctx context.Context) deploy.Result {
	if t.VPCPeering.Spec.Target.AliCloud == nil {
		return deploy.Error(fmt.Errorf("spec.target.alicloud is nil"))
	}

	// Convert UID from string to float64 for Crossplane provider compatibility
	// Note: We store UID as string in CRD to avoid JSON precision loss,
	// but the Crossplane provider expects float64
	uidFloat, err := strconv.ParseFloat(t.VPCPeering.Spec.Target.AliCloud.UID, 64)
	if err != nil {
		return deploy.Error(fmt.Errorf("invalid UID format, must be numeric: %w", err))
	}

	peeringConn := &alivpcv1alpha1.VPCPeerConnection{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.VPCPeering.Name,
			Labels: common.BuildVPCPeeringLabels(t.Dataplane, t.VPC),
		},
		Spec: alivpcv1alpha1.VPCPeerConnectionSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.VPCPeerConnectionParameters{
				AcceptingAliUID:   ptr.To(uidFloat),
				AcceptingRegionID: ptr.To(t.VPCPeering.Spec.Target.AliCloud.Region),
				AcceptingVPCID:    ptr.To(t.VPCPeering.Spec.Target.AliCloud.VpcID),
				Region:            ptr.To(t.VPCPeering.Spec.Region),
				VPCID:             ptr.To(t.VPC.Status.VPC.VPCID),
				Tags:              common.BuildVPCPeeringTags(t.VPC, t.VPCPeering.Name, consts.ProviderTypeACK, t.Config.AdditionalCloudTags),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, peeringConn); err != nil {
		return deploy.Error(fmt.Errorf("failed to apply VPCPeeringConnection cr: %w", err))
	}
	if peeringConn.Status.AtProvider.ID != nil {
		t.VPCPeering.Status.ExternalName = *peeringConn.Status.AtProvider.ID
	}

	ready, synced := utils.GetConditions(peeringConn.Status.Conditions)
	if ready && synced {
		t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhaseActive
		t.VPCPeering.Status.Reason = "VPCPeeringConnection is synced and ready"
	} else if !ready && synced {
		t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhasePending
		t.VPCPeering.Status.Reason = "VPCPeeringConnection is synced but not ready"
	}

	return deploy.Done()
}

// peeringStatus holds the peering connection status information
type peeringStatus struct {
	connection  *alivpcv1alpha1.VPCPeerConnection
	status      string
	peeringName string
	peerCIDR    string
}

// getPeeringConnectionStatus retrieves and validates the VPCPeerConnection status
func (t *VPCPeeringConnection) getPeeringConnectionStatus(ctx context.Context) (*peeringStatus, error) {
	peeringConn := &alivpcv1alpha1.VPCPeerConnection{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.VPCPeering.Name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(peeringConn), peeringConn); err != nil {
		return nil, fmt.Errorf("failed to get VPCPeeringConnection CR: %w", err)
	}

	if peeringConn.Status.AtProvider.ID == nil {
		t.Logger.Info("waiting for VPCPeeringConnection ID to be set")
		return nil, fmt.Errorf("VPCPeeringConnection ID is nil")
	}

	return &peeringStatus{
		connection:  peeringConn,
		status:      ptr.Deref(peeringConn.Status.AtProvider.Status, ""),
		peeringName: peeringConn.Name,
		peerCIDR:    t.VPCPeering.Spec.Target.AliCloud.PeerCIDR,
	}, nil
}

func (t *VPCPeeringConnection) handleAcceptingState(
	ctx context.Context,
	status *peeringStatus,
) deploy.Result {
	t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhasePending
	t.VPCPeering.Status.Reason = "VPCPeeringConnection is pending acceptance, waiting for Activated state before adding routes"

	// Trigger reconciliation to monitor state transition
	// Otherwise, the crossplane CR may not be reconciled because it's synced and ready, but not yet active.
	if err := common.TriggerReconciliation(ctx, status.connection, t.Local, client.MergeFrom(status.connection.DeepCopy())); err != nil {
		return deploy.Error(fmt.Errorf("failed to trigger reconciliation: %w", err))
	}

	return deploy.Done().WithRequeuAfter(common.RetryInterval)
}

func (t *VPCPeeringConnection) handleActivatedState(
	ctx context.Context,
	status *peeringStatus,
) deploy.Result {
	// Get node route table names from Internet CR
	nodeRouteTables, err := common.GetNodeRouteTableNames(t.Internet, common.AliCloudNodeRouteTableFilter)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to get node route tables: %w", err))
	}

	// Check if routes are created
	routes, err := t.listPeeringRoutes(ctx)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to list peering routes: %w", err))
	}
	routesCreated := len(routes) == len(nodeRouteTables) && len(routes) > 0
	if !routesCreated {
		if err = t.createPeeringRoutes(ctx, status, nodeRouteTables); err != nil {
			return deploy.Error(fmt.Errorf("failed to create peering routes: %w", err))
		}
		// Re-fetch routes after creation
		routes, err = t.listPeeringRoutes(ctx)
		if err != nil {
			return deploy.Error(fmt.Errorf("failed to list peering routes after creation: %w", err))
		}
	}

	t.updateVPCPeeringRoutesStatus(routes)

	routesReady := t.arePeeringRoutesReady(routes)
	if !routesCreated || !routesReady {
		t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhasePending
		t.VPCPeering.Status.Reason = "VPCPeeringConnection is activated, but routes are not ready"
		// Requeue to verify routes become active
		return deploy.Done().WithRequeuAfter(common.RetryInterval)
	}

	t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhaseActive
	t.VPCPeering.Status.Reason = "VPCPeeringConnection is activated and routes are configured"
	return deploy.Done()
}

func (t *VPCPeeringConnection) PostCreate(ctx context.Context) deploy.Result {
	// Get and validate peering connection status
	status, err := t.getPeeringConnectionStatus(ctx)
	if err != nil {
		return deploy.Error(err)
	}

	// Handle different connection states
	switch status.status {
	case "Accepting":
		return t.handleAcceptingState(ctx, status)
	case "Activated":
		return t.handleActivatedState(ctx, status)
	default:
		// Handle other states (rejected, failed, deleted-by-peer, expired, etc.)
		return deploy.Error(fmt.Errorf("VPCPeeringConnection is in unexpected state: %s", status.status))
	}
}

func (t *VPCPeeringConnection) Delete(ctx context.Context) deploy.Result {
	// get then delete
	peeringConn := &alivpcv1alpha1.VPCPeerConnection{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.VPCPeering.Name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(peeringConn), peeringConn); err != nil {
		if errors.IsNotFound(err) {
			// Connection already deleted, ensure routes are cleaned up
			if err := t.deletePeeringRoutes(ctx); err != nil {
				// Return error to trigger retry, ensuring orphaned routes are cleaned up
				return deploy.Error(fmt.Errorf("connection deleted but routes cleanup failed: %w", err))
			}
			return deploy.Done()
		}
		return deploy.Error(err)
	}

	// Delete all Route CRs
	if err := t.deletePeeringRoutes(ctx); err != nil {
		return deploy.Error(fmt.Errorf("failed to delete peering routes: %w", err))
	}

	// Delete the VPCPeeringConnection CR
	if err := controllers.DeleteUpboundResource(ctx, t.Local, peeringConn); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *VPCPeeringConnection) PostDelete(ctx context.Context) deploy.Result {
	obj := &alivpcv1alpha1.VPCPeerConnection{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.VPCPeering.Name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
		if errors.IsNotFound(err) {
			return deploy.Done()
		}
		return deploy.Error(err)
	}
	return deploy.Error(fmt.Errorf("VPCPeeringConnection CR is not deleted"))
}

// buildPeeringRoute builds a Crossplane Route CR for VPC Peering.
// It creates a route that directs traffic destined for the peer CIDR to the VPC Peering Connection.
func (t *VPCPeeringConnection) buildPeeringRoute(
	routeTableName string,
	peerCIDR string,
	peerConnectionID string,
	region string,
	labels map[string]string,
) *alivpcv1alpha1.RouteEntry {
	name := common.GenVPCPeeringRouteName(t.VPCPeering.Name, routeTableName)

	return &alivpcv1alpha1.RouteEntry{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: alivpcv1alpha1.RouteEntrySpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: alivpcv1alpha1.RouteEntryParameters{
				RouteTableIDRef:      &xpv1.Reference{Name: routeTableName},
				Name:                 ptr.To(name),
				NexthopType:          ptr.To("VpcPeer"),
				NexthopID:            ptr.To(peerConnectionID),
				DestinationCidrblock: ptr.To(peerCIDR),
				Region:               ptr.To(region),
			},
		},
	}
}

// listPeeringRoutes lists all Route CRs associated with this VPC Peering.
// It filters Route CRs by the VPCPeeringNameKey label.
func (t *VPCPeeringConnection) listPeeringRoutes(ctx context.Context) ([]alivpcv1alpha1.RouteEntry, error) {
	routeList := &alivpcv1alpha1.RouteEntryList{}
	labelOpt := client.MatchingLabels{
		v1alpha1.VPCPeeringNameKey: t.VPCPeering.Name,
	}

	if err := t.Local.List(ctx, routeList, labelOpt); err != nil {
		return nil, fmt.Errorf("failed to list peering routes: %w", err)
	}

	return routeList.Items, nil
}

func (t *VPCPeeringConnection) arePeeringRoutesReady(routes []alivpcv1alpha1.RouteEntry) bool {
	for _, route := range routes {
		// Check if the Route CR is synced and ready
		if !controllers.IsResourceReady(route.Status.ResourceStatus) {
			t.Logger.Info("route CR not ready",
				"routeName", route.Name,
				"conditions", route.Status.Conditions)
			return false
		}
	}
	return true
}

// createPeeringRoutes creates Route CRs for all node route tables.
// It retrieves node route table names from the Internet CR and creates a Route CR for each.
// The operation is idempotent - if routes already exist, they will be updated.
//
// Important: This should only be called when the peering connection is in Activated state.
// Unlike AWS, AliCloud does not support adding routes in pending-acceptance (Accepting) state.
func (t *VPCPeeringConnection) createPeeringRoutes(
	ctx context.Context,
	status *peeringStatus,
	nodeRouteTables []string,
) error {
	// Build labels for all Route CRs
	labels := common.BuildPeeringRouteLabels(t.VPCPeering.Name)

	// Create Route CR for each route table
	for _, rtName := range nodeRouteTables {
		route := t.buildPeeringRoute(rtName, status.peerCIDR, t.VPCPeering.Status.ExternalName, t.VPCPeering.Spec.Region, labels)

		// Apply the Route CR (idempotent operation)
		if err := k8s.Apply(ctx, t.Local, route); err != nil {
			return fmt.Errorf("failed to apply route CR %s: %w", route.Name, err)
		}
	}

	t.Logger.Info("successfully applied all route CRs for VPC peering",
		"routeCount", len(nodeRouteTables))
	return nil
}

// deletePeeringRoutes deletes all Route CRs associated with this VPC Peering.
// It uses the routeManager abstraction layer for a unified deletion flow.
func (t *VPCPeeringConnection) deletePeeringRoutes(ctx context.Context) error {
	return common.DeleteAllRoutes(ctx, t.manager, t.VPCPeering.Name, t.Logger)
}

func (t *VPCPeeringConnection) updateVPCPeeringRoutesStatus(routes []alivpcv1alpha1.RouteEntry) {
	routeStatus := make([]v1alpha1.VPCPeeringRoute, 0, len(routes))
	for _, route := range routes {
		var rtName string
		if route.Spec.ForProvider.RouteTableIDRef != nil {
			rtName = route.Spec.ForProvider.RouteTableIDRef.Name
		}

		routeStatus = append(routeStatus, v1alpha1.VPCPeeringRoute{
			Name:           route.Name,
			RouteTableName: rtName,
		})
	}

	// Initialize AliCloud status if nil
	if t.VPCPeering.Status.AliCloud == nil {
		t.VPCPeering.Status.AliCloud = &v1alpha1.AliCloudVPCPeeringStatus{}
	}

	// Update routes status
	t.VPCPeering.Status.AliCloud.Routes = routeStatus

	t.Logger.Info("updated VPC peering routes status",
		"routeCount", len(routeStatus))
}
