package aws

import (
	"context"
	"fmt"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/upbound/provider-aws/apis/ec2/v1beta1"
	"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
}

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

func NewVPCPeeringConnection(ctx *task.Context) *VPCPeeringConnection {
	return &VPCPeeringConnection{
		BaseTask: &task.BaseTask{
			Context: ctx,
		},
	}
}

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

	autoAccept := t.autoAcceptO11yPeering()
	forProvider := v1beta1.VPCPeeringConnectionParameters_2{
		PeerOwnerID: ptr.To(t.VPCPeering.Spec.Target.AWS.AccountID),
		PeerVPCID:   ptr.To(t.VPCPeering.Spec.Target.AWS.VpcID),
		Region:      ptr.To(t.VPCPeering.Spec.Region),
		VPCID:       ptr.To(t.VPC.Status.VPC.VPCID),
		AutoAccept:  autoAccept,
		Tags:        common.BuildVPCPeeringTags(t.VPC, t.VPCPeering.Name, consts.ProviderTypeEKS, t.Config.AdditionalCloudTags),
	}
	if autoAccept == nil {
		forProvider.PeerRegion = ptr.To(t.VPCPeering.Spec.Target.AWS.Region)
	}

	peeringConn := &v1beta1.VPCPeeringConnection{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.VPCPeering.Name,
			Labels: common.BuildVPCPeeringLabels(t.Dataplane, t.VPC),
		},
		Spec: v1beta1.VPCPeeringConnectionSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: forProvider,
		},
	}
	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()
}

func (t *VPCPeeringConnection) autoAcceptO11yPeering() *bool {
	if t.VPCPeering.Spec.Target.AWS == nil {
		return nil
	}
	if t.VPCPeering.Labels[common.O11yVPCPeeringManagedByLabel] != common.O11yVPCPeeringManagedByValue {
		return nil
	}
	sourceAccountID := t.VPCPeering.Annotations[v1alpha1.AnnotationKeySourceAccountID]
	if sourceAccountID == "" || sourceAccountID != t.VPCPeering.Spec.Target.AWS.AccountID {
		return nil
	}
	return ptr.To(true)
}

// peeringStatus holds the peering connection status information
type peeringStatus struct {
	connection   *v1beta1.VPCPeeringConnection
	acceptStatus string
	peeringName  string
	peerCIDR     string
}

// getPeeringConnectionStatus retrieves and validates the VPCPeeringConnection status
func (t *VPCPeeringConnection) getPeeringConnectionStatus(ctx context.Context) (*peeringStatus, error) {
	peeringConn := &v1beta1.VPCPeeringConnection{
		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,
		acceptStatus: ptr.Deref(peeringConn.Status.AtProvider.AcceptStatus, ""),
		peeringName:  peeringConn.Name, // Use CR name instead of AWS ID
		peerCIDR:     t.VPCPeering.Spec.Target.AWS.PeerCIDR,
	}, nil
}

// handlePendingAcceptanceState handles peering connection in pending-acceptance state
// Creates routes in blackhole state and triggers reconciliation
func (t *VPCPeeringConnection) handlePendingAcceptanceState(
	ctx context.Context,
	status *peeringStatus,
	routes []v1beta1.Route,
	nodeRouteTableNames []string,
) deploy.Result {
	t.Logger.Info("VPCPeeringConnection is in pending-acceptance state",
		"peeringConnectionID", *status.connection.Status.AtProvider.ID,
		"peeringConnectionName", status.peeringName)

	// Ensure routes are created and status is updated
	updatedRoutes, err := t.ensureRoutesCreatedAndUpdated(ctx, routes, nodeRouteTableNames, status.peerCIDR, status.peeringName)
	if err != nil {
		return deploy.Error(err)
	}

	// Update routes status in VPCPeering CR
	if err := t.updateVPCPeeringRoutesStatus(updatedRoutes); err != nil {
		return deploy.Error(fmt.Errorf("failed to update routes status: %w", err))
	}

	t.Logger.Info("routes configured in blackhole state, waiting for connection acceptance",
		"peeringConnectionName", status.peeringName)

	// Update VPCPeering status
	t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhasePending
	t.VPCPeering.Status.Reason = "VPCPeeringConnection is pending acceptance, routes configured in blackhole state"

	// 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(err)
	}

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

// handleActiveState handles peering connection in active state
// Ensures routes are active and updates final status
func (t *VPCPeeringConnection) handleActiveState(
	ctx context.Context,
	status *peeringStatus,
	routes []v1beta1.Route,
	nodeRouteTableNames []string,
) deploy.Result {
	t.Logger.Info("VPCPeeringConnection is active",
		"peeringConnectionID", *status.connection.Status.AtProvider.ID,
		"peeringConnectionName", status.peeringName)

	created, routesReady, err := t.arePeeringRoutesReady(routes, nodeRouteTableNames)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to check routes status: %w", err))
	}

	if !routesReady {
		t.Logger.Info("routes not yet ready, ensuring they are configured",
			"destination", status.peerCIDR,
			"peeringConnectionName", status.peeringName,
			"created", created)

		// If routes don't exist, create them
		if !created {
			updatedRoutes, err := t.ensureRoutesCreatedAndUpdated(ctx, routes, nodeRouteTableNames, status.peerCIDR, status.peeringName)
			if err != nil {
				return deploy.Error(err)
			}
			routes = updatedRoutes
		}

		// Update routes status regardless of whether they were just created or already existed
		if err := t.updateVPCPeeringRoutesStatus(routes); err != nil {
			return deploy.Error(fmt.Errorf("failed to update routes status: %w", err))
		}

		// Requeue to verify routes become active
		return deploy.Done().WithRequeuAfter(common.RetryInterval)
	}

	// All routes are ready and active, update status one final time
	if err := t.updateVPCPeeringRoutesStatus(routes); err != nil {
		t.Logger.Error(err, "failed to update VPCPeering routes status",
			"peeringConnectionName", status.peeringName)
		return deploy.Error(err)
	}

	t.VPCPeering.Status.Phase = v1alpha1.VPCPeeringPhaseActive
	t.VPCPeering.Status.Reason = "VPCPeeringConnection is active and routes are configured"
	t.Logger.Info("VPCPeering is now active with routes configured",
		"peeringConnectionName", status.peeringName)
	return deploy.Done()
}

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

	nodeRouteTableNames, err := common.GetNodeRouteTableNames(t.Internet, common.AWSNodeRouteTableFilter)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to get node route tables: %w", err))
	}
	routes, err := t.listPeeringRoutes(ctx)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to list peering routes: %w", err))
	}

	switch status.acceptStatus {
	case "pending-acceptance":
		return t.handlePendingAcceptanceState(ctx, status, routes, nodeRouteTableNames)
	case "active":
		return t.handleActiveState(ctx, status, routes, nodeRouteTableNames)
	default:
		return deploy.Error(fmt.Errorf("VPCPeeringConnection is in unexpected state: %s", status.acceptStatus))
	}
}

func (t *VPCPeeringConnection) Delete(ctx context.Context) deploy.Result {
	// get then delete
	peeringConn := &v1beta1.VPCPeeringConnection{
		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)
	}

	// Critical: Delete all Route CRs first before deleting the VPCPeeringConnection
	// This prevents resource leakage if route deletion fails
	if err := t.deletePeeringRoutes(ctx); err != nil {
		// Don't delete connection if routes can't be deleted
		return deploy.Error(fmt.Errorf("failed to delete peering routes, aborting connection deletion: %w", err))
	}

	// Only delete the VPCPeeringConnection CR after routes are successfully deleted
	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 := &v1beta1.VPCPeeringConnection{
		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"))
}

// buildPeeringRouteLabels creates labels for VPC Peering Route CRs.
// These labels are used to query and manage Route CRs associated with this VPC Peering.
func (t *VPCPeeringConnection) buildPeeringRouteLabels() map[string]string {
	return common.BuildPeeringRouteLabels(t.VPCPeering.Name)
}

// 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.
//
// The route will be in blackhole state if the peering connection is in pending-acceptance state,
// and will automatically become active when the peering connection is accepted.
func (t *VPCPeeringConnection) buildPeeringRoute(
	routeTableName string,
	peerCIDR string,
	pcxName string,
	labels map[string]string,
) *v1beta1.Route {
	name := common.GenVPCPeeringRouteName(t.VPCPeering.Name, routeTableName)

	return &v1beta1.Route{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
		Spec: v1beta1.RouteSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
			ForProvider: v1beta1.RouteParameters_2{
				Region:                    ptr.To(t.VPCPeering.Spec.Region),
				DestinationCidrBlock:      ptr.To(peerCIDR),
				VPCPeeringConnectionIDRef: &xpv1.Reference{Name: pcxName},
				RouteTableIDRef:           &xpv1.Reference{Name: routeTableName},
			},
		},
	}
}

// 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) ([]v1beta1.Route, error) {
	routeList := &v1beta1.RouteList{}
	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 arePeeringRoutesCreated(routes []v1beta1.Route, nodeRouteTableNames []string) bool {
	expectedCount := len(nodeRouteTableNames)
	actualCount := len(routes)
	return actualCount == expectedCount && expectedCount != 0
}

// ensureRoutesCreatedAndUpdated ensures routes are created if needed and returns updated route list.
// This method handles the common pattern of:
// 1. Check if routes exist
// 2. Create routes if missing
// 3. Re-query routes after creation
func (t *VPCPeeringConnection) ensureRoutesCreatedAndUpdated(
	ctx context.Context,
	routes []v1beta1.Route,
	nodeRouteTableNames []string,
	peerCIDR string,
	pcxName string,
) ([]v1beta1.Route, error) {
	// Check if routes need to be created
	if !arePeeringRoutesCreated(routes, nodeRouteTableNames) {
		t.Logger.Info("creating routes for VPC peering",
			"destination", peerCIDR,
			"peeringConnectionName", pcxName,
			"expectedRouteCount", len(nodeRouteTableNames),
			"actualRouteCount", len(routes))

		// Create routes
		if err := t.createPeeringRoutes(ctx, peerCIDR, pcxName, nodeRouteTableNames); err != nil {
			return nil, fmt.Errorf("failed to create peering routes: %w", err)
		}

		// Re-query routes after creation
		updatedRoutes, err := t.listPeeringRoutes(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to list peering routes after creation: %w", err)
		}
		routes = updatedRoutes

		t.Logger.Info("routes created successfully",
			"peeringConnectionName", pcxName,
			"routeCount", len(routes))
	}
	return routes, nil
}

// arePeeringRoutesReady checks if all peering routes are ready and active.
// This method verifies:
// 1. The number of routes matches the number of node route tables
// 2. Each Crossplane Route CR is synced and ready
// 3. Each route state is "active" (not "blackhole" or other states)
//
// This is useful for active state where routes should be fully functional.
func (t *VPCPeeringConnection) arePeeringRoutesReady(routes []v1beta1.Route, nodeRouteTableNames []string) (created, ready bool, err error) {
	if !arePeeringRoutesCreated(routes, nodeRouteTableNames) {
		return false, false, nil
	}

	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 true, false, nil
		}

		// Check if the route is in active state (not blackhole)
		if route.Status.AtProvider.State != nil {
			state := *route.Status.AtProvider.State
			if state != "active" {
				t.Logger.Info("route not in active state",
					"routeName", route.Name,
					"state", state)
				return true, false, nil
			}
		}
	}

	return true, true, nil
}

// createPeeringRoutes creates Route CRs for all node route tables.
// The operation is idempotent - if routes already exist, they will be updated.
//
// Routes created while the peering connection is in pending-acceptance state will be in
// blackhole state and will automatically become active when the connection is accepted.
func (t *VPCPeeringConnection) createPeeringRoutes(
	ctx context.Context,
	peerCIDR string,
	pcxName string,
	nodeRouteTableNames []string,
) error {
	labels := t.buildPeeringRouteLabels()

	// Create Route CR for each route table
	for _, rtName := range nodeRouteTableNames {
		route := t.buildPeeringRoute(rtName, peerCIDR, pcxName, labels)

		t.Logger.Info("applying route CR for VPC peering",
			"routeName", route.Name,
			"routeTable", rtName,
			"destinationCIDR", peerCIDR,
			"peeringConnection", pcxName)

		// 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(nodeRouteTableNames),
		"peerCIDR", peerCIDR)

	return nil
}

// deletePeeringRoutes deletes all Route CRs associated with this VPC Peering.
// It queries Route CRs by label and deletes each one using the standard Crossplane deletion process.
func (t *VPCPeeringConnection) deletePeeringRoutes(ctx context.Context) error {
	manager := NewAWSRouteManager(
		t.Local,
		t.VPCPeering,
		t.VPCPeering.Spec.Region,
		t.ProviderConfigName(),
	)
	return common.DeleteAllRoutes(ctx, manager, t.VPCPeering.Name, t.Logger)
}

// updateVPCPeeringRoutesStatus updates the VPCPeering.Status.AWS.Routes field
func (t *VPCPeeringConnection) updateVPCPeeringRoutesStatus(routes []v1beta1.Route) error {
	// Build route status list
	routeStatus := make([]v1alpha1.VPCPeeringRoute, 0, len(routes))
	for _, route := range routes {
		state := "unknown"
		if route.Status.AtProvider.State != nil {
			state = *route.Status.AtProvider.State
		}

		// Safe handling of RouteTableIDRef to prevent nil pointer dereference
		routeTableName := ""
		if route.Spec.ForProvider.RouteTableIDRef != nil {
			routeTableName = route.Spec.ForProvider.RouteTableIDRef.Name
		}

		routeStatus = append(routeStatus, v1alpha1.VPCPeeringRoute{
			Name:           route.Name,
			RouteTableName: routeTableName,
			State:          state,
		})
	}

	// Initialize AWS status if nil
	if t.VPCPeering.Status.AWS == nil {
		t.VPCPeering.Status.AWS = &v1alpha1.AWSVPCPeeringStatus{}
	}

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

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

	return nil
}
