package server

import (
	"context"
	"errors"
	"fmt"

	"github.com/mem9-ai/drive9/pkg/logger"
	"github.com/mem9-ai/drive9/pkg/meta"
	"github.com/mem9-ai/drive9/pkg/tenant"
	"go.uber.org/zap"
)

func (s *Server) deleteNewestFreePoolTenants(ctx context.Context, poolID, organizationID string, count int, cred tenant.CredentialProvisionRequest, deleteAll bool) (int, error) {
	if count <= 0 && !deleteAll {
		return 0, nil
	}
	remaining := count
	deleted := 0
	for deleteAll || remaining > 0 {
		var native *meta.TenantWithTiDBCloudOrgBinding
		var nativeRows []meta.TenantWithTiDBCloudOrgBinding
		var err error
		if deleteAll {
			nativeRows, err = s.meta.ListFreeTenantPoolBindingsForDelete(ctx, organizationID, true, 1)
		} else {
			nativeRows, err = s.meta.ListTenantPoolFreeSlotsForDelete(ctx, organizationID, true, 1)
		}
		if err != nil {
			return deleted, err
		}
		if len(nativeRows) > 0 {
			native = &nativeRows[0]
		}
		shared, sharedErr := s.meta.GetNewestFreeTenantPoolMembershipForDelete(ctx, poolID)
		if sharedErr != nil && !errors.Is(sharedErr, meta.ErrNotFound) {
			return deleted, sharedErr
		}
		if native == nil && shared == nil {
			if deleteAll {
				return deleted, nil
			}
			return deleted, fmt.Errorf("not enough free tenants to delete")
		}
		useShared := shared != nil && (native == nil || shared.Membership.CreatedAt.After(native.Binding.CreatedAt) ||
			(shared.Membership.CreatedAt.Equal(native.Binding.CreatedAt) && shared.Tenant.ID > native.Tenant.ID))
		if useShared {
			owned, err := s.deleteFreeSharedPoolTenant(ctx, &shared.Tenant)
			if err != nil {
				return deleted, err
			}
			if !owned {
				continue
			}
		} else {
			t := native.Tenant
			if t.StorageNamespaceID != "" {
				if _, err := s.meta.GetStorageNamespace(ctx, t.StorageNamespaceID); err != nil {
					return deleted, err
				}
			}
			var updated bool
			var markErr error
			updated, markErr = s.meta.MarkFreeTenantPoolTenantDeleting(ctx, t.ID, t.Status)
			if markErr != nil {
				return deleted, markErr
			}
			if !updated {
				continue
			}
			if err := s.deprovisionTenantCluster(ctx, &t, cred); err != nil {
				return deleted, err
			}
			if _, err := s.enqueueTenantDeleteJob(ctx, &t); err != nil {
				return deleted, err
			}
			_ = s.meta.RevokeTenantAPIKeys(ctx, t.ID)
		}
		deleted++
		if !deleteAll {
			remaining--
		}
	}
	return deleted, nil
}

func (s *Server) retryUnconfirmedFreePoolTenantDeletes(ctx context.Context, organizationID, poolID string, cred tenant.CredentialProvisionRequest) error {
	for {
		tenantIDs, err := s.meta.ListUnconfirmedFreeTenantPoolDeleteIDs(ctx, organizationID, poolID, 100)
		if err != nil {
			return err
		}
		if len(tenantIDs) == 0 {
			return nil
		}
		for _, tenantID := range tenantIDs {
			t, err := s.meta.GetTenant(ctx, tenantID)
			if errors.Is(err, meta.ErrNotFound) {
				continue
			}
			if err != nil {
				return err
			}
			if t.Status != meta.TenantDeleting {
				continue
			}
			if t.StorageNamespaceID != "" {
				if _, err := s.meta.GetStorageNamespace(ctx, t.StorageNamespaceID); err != nil {
					return err
				}
			}
			updated, err := s.meta.MarkFreeTenantPoolTenantDeleting(ctx, t.ID, meta.TenantDeleting)
			if err != nil {
				return err
			}
			if !updated {
				continue
			}
			if err := s.deprovisionTenantCluster(ctx, t, cred); err != nil {
				return err
			}
			if _, err := s.enqueueTenantDeleteJob(ctx, t); err != nil {
				return err
			}
			_ = s.meta.RevokeTenantAPIKeys(ctx, tenantID)
		}
	}
}

func (s *Server) markFreeSharedPoolTenantDeletingForCleanup(ctx context.Context, t *meta.Tenant) (bool, error) {
	if t.StorageNamespaceID == "" {
		return s.meta.MarkFreeSharedTenantPoolTenantDeleting(ctx, t.ID, t.Status)
	}
	ns, err := s.meta.GetStorageNamespace(ctx, t.StorageNamespaceID)
	if err != nil {
		return false, err
	}
	return s.meta.MarkFreeSharedTenantPoolTenantDeletingWithDeleteJob(ctx, t.ID, t.Status,
		delayedTenantDeleteJob(t, ns))
}

// rollbackFreeSharedPoolTenantDelete undoes the deleting mark of a shared
// pool member whose purge failed before destructive work began. It fails
// closed unless both the placement and free membership still exist; once
// either has been detached, the tenant stays deleting and the delayed cleanup
// job finishes it rather than reactivating a half-deleted tenant.
func (s *Server) rollbackFreeSharedPoolTenantDelete(ctx context.Context, t *meta.Tenant) {
	if t.Status == meta.TenantDeleting {
		logger.Info(ctx, "admin_tenant_pool_shared_delete_rollback_skipped",
			zap.String("tenant_id", t.ID),
			zap.String("reason", "delete was already in progress before this attempt"))
		return
	}
	rollbackCtx := backgroundWithTrace(ctx)
	fsID, err := s.meta.ResolveFsID(rollbackCtx, t.ID)
	if err != nil {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_failed",
			zap.String("tenant_id", t.ID), zap.Error(err))
		return
	}
	if _, err := s.meta.GetTenantPlacement(rollbackCtx, fsID); errors.Is(err, meta.ErrNotFound) {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_skipped",
			zap.String("tenant_id", t.ID),
			zap.String("reason", "placement already detached; leaving tenant deleting for the cleanup job"))
		return
	} else if err != nil {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_failed",
			zap.String("tenant_id", t.ID), zap.Error(err))
		return
	}
	if _, err := s.meta.GetTenantPoolMembership(rollbackCtx, t.ID); errors.Is(err, meta.ErrNotFound) {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_skipped",
			zap.String("tenant_id", t.ID),
			zap.String("reason", "membership already detached; leaving tenant deleting for the cleanup job"))
		return
	} else if err != nil {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_failed",
			zap.String("tenant_id", t.ID), zap.Error(err))
		return
	}
	if t.StorageNamespaceID == "" {
		if _, err := s.meta.UpdateTenantStatusIf(rollbackCtx, t.ID, meta.TenantDeleting, t.Status); err != nil {
			logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_failed",
				zap.String("tenant_id", t.ID), zap.Error(err))
		}
		return
	}
	rolledBack, err := s.meta.RollbackTenantDeletingWithDeleteJob(rollbackCtx, t.ID, t.StorageNamespaceID, t.Status)
	if err != nil {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_failed",
			zap.String("tenant_id", t.ID), zap.Error(err))
		return
	}
	if !rolledBack {
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_skipped",
			zap.String("tenant_id", t.ID),
			zap.String("reason", "delete no longer rollbackable"))
	}
}

// deleteFreeSharedPoolTenant owns the complete free-member delete lifecycle:
// claim the membership, perform the purge/finalize steps, and roll back only
// failures that occur before destructive work begins. Callers only need to
// retry selection when owned=false; they never interpret rollback state.
func (s *Server) deleteFreeSharedPoolTenant(ctx context.Context, t *meta.Tenant) (owned bool, err error) {
	if t == nil {
		return false, meta.ErrNotFound
	}
	// Avoid a mark/rollback write cycle on every deleting-pool reconcile while
	// the physical DB cannot be purged. The readiness check below is repeated
	// after ownership and backend invalidation because this preflight is only a
	// churn guard, not the destructive-operation authorization check.
	preflightFsID, preflightErr := s.meta.ResolveFsID(ctx, t.ID)
	if preflightErr == nil {
		preflightPlacement, placementErr := s.meta.GetTenantPlacement(ctx, preflightFsID)
		if placementErr == nil && preflightPlacement.Status != meta.PlacementStatusDeleting {
			dbPool, dbErr := s.meta.GetSharedDB(ctx, preflightPlacement.DbID)
			if dbErr == nil && !sharedDBConnectionReady(dbPool) {
				return false, fmt.Errorf("shared DB %d is not ready for tenant purge", preflightPlacement.DbID)
			}
		}
	}
	owned, err = s.markFreeSharedPoolTenantDeletingForCleanup(ctx, t)
	if err != nil || !owned {
		return owned, err
	}
	rollbackSafe := t.Status != meta.TenantDeleting
	defer func() {
		if err == nil {
			return
		}
		if rollbackSafe {
			s.rollbackFreeSharedPoolTenantDelete(ctx, t)
			return
		}
		logger.Warn(ctx, "admin_tenant_pool_shared_delete_rollback_skipped",
			zap.String("tenant_id", t.ID),
			zap.String("reason", "shared purge may have started; leaving tenant deleting for cleanup"))
	}()
	if err = s.drainTenantBackend(ctx, t.ID); err != nil {
		return true, err
	}
	fsID, err := s.meta.ResolveFsID(ctx, t.ID)
	if err != nil {
		return true, err
	}
	placement, err := s.meta.GetTenantPlacement(ctx, fsID)
	if errors.Is(err, meta.ErrNotFound) {
		rollbackSafe = false
		return true, fmt.Errorf("shared tenant placement is missing: %w", err)
	}
	if err != nil {
		return true, err
	}
	placementAlreadyDeleting := placement.Status == meta.PlacementStatusDeleting
	if !placementAlreadyDeleting {
		dbPool, err := s.meta.GetSharedDB(ctx, placement.DbID)
		if err != nil {
			return true, err
		}
		if !sharedDBConnectionReady(dbPool) {
			return true, fmt.Errorf("shared DB %d is not ready for tenant purge", placement.DbID)
		}
		// PurgeSharedTenant may have deleted some shared rows before
		// returning an error. From this call onward, restoring the tenant
		// to active is unsafe even while its membership still exists.
		rollbackSafe = false
		if err := s.pool.PurgeSharedTenant(ctx, fsID, placement.DbID); err != nil {
			return true, err
		}
		markDeleted := t.StorageNamespaceID == ""
		if err := s.meta.FinalizeSharedTenantDeleteMetadata(ctx, t.ID, fsID, placement.DbID,
			s.sharedDBReopenRatio, markDeleted); err != nil {
			return true, err
		}
		if markDeleted {
			_ = s.meta.RevokeTenantAPIKeys(ctx, t.ID)
			return true, nil
		}
	}
	rollbackSafe = false
	_ = s.meta.RevokeTenantAPIKeys(ctx, t.ID)
	if placementAlreadyDeleting && t.StorageNamespaceID == "" {
		err = s.meta.FinalizeSharedTenantDeleteMetadata(ctx, t.ID, fsID, placement.DbID,
			s.sharedDBReopenRatio, true)
		return true, err
	}
	if placementAlreadyDeleting && t.StorageNamespaceID != "" {
		_, err = s.meta.HandoffFreeSharedTenantDeleteJob(ctx, t.ID, fsID)
		return true, err
	}
	if _, err = s.enqueueTenantDeleteJob(ctx, t); err != nil {
		return true, err
	}
	return true, err
}
