package service

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// preemptStalePrivilegedTiDB self-heals a privileged TiDB pod that is stuck
// activated for a keyspace whose cluster has already been deleted. The
// canonical bug it fixes:
//
//  1. Cluster X runs reset_password (or delete_cloud_admin); the V1 path
//     activates privileged TiDB for keyspace=X and schedules a 30s-delayed
//     deactivation goroutine via ScheduleDeactivation.
//  2. Cluster X is then deleted (its TiDB pods are torn down).
//  3. The deactivation goroutine never fires -- typical reasons: the
//     cluster-agent process crashed/restarted before its 30s timer, or the
//     3-attempt retry inside runDeactivationWithRetry exhausted.
//  4. privileged TiDB stays "activated for keyspace=X" forever. Any later
//     request for a different keyspace polls until deadline and then fails;
//     V0 fallback is useless during cluster create (no platform_admin
//     credential yet).
//
// Detection is fact-based, not time-based: if no TiDB pods labelled with the
// activated keyspace exist in the namespace, the cluster is gone and we are
// safe to force-deactivate the pod (operator recreates it as standby ~14s
// later). We deliberately do NOT preempt activations bound to systemKeyspace
// or to our own clusterID.
//
// Returns true iff a force-deactivation was issued; the caller should expect
// the pod to be recreated and continue with the existing wait-for-available
// loop instead of activating immediately.
func (s *AgentService) preemptStalePrivilegedTiDB(
	ctx context.Context,
	httpClient http.Client,
	tidbAddr, namespace, clusterID string,
) (bool, error) {
	status, err := s.fetchPrivilegedTiDBStatus(ctx, httpClient, tidbAddr)
	if err != nil {
		return false, fmt.Errorf("fetch privileged TiDB status for stale check: %w", err)
	}
	if status.State != "activated" || status.KeyspaceName == "" {
		return false, nil // standby; nothing to preempt
	}
	if status.KeyspaceName == clusterID || status.KeyspaceName == systemKeyspace {
		return false, nil // ours, or system pool; never preempt
	}

	stale, err := s.isActivatedKeyspaceStale(ctx, namespace, status.KeyspaceName)
	if err != nil {
		return false, fmt.Errorf("check stale activation: %w", err)
	}
	if !stale {
		return false, nil // legitimate use by another live cluster
	}

	tilog.WithContextS(ctx).With(
		"stale_keyspace", status.KeyspaceName,
		"my_cluster_id", clusterID,
		"namespace", namespace,
	).Warnf("preempting privileged TiDB stuck on deleted cluster; force-deactivating")

	// Empty UID => legacy "delete whichever Running pod is found" mode. We
	// explicitly want this here: the original activator that should have
	// deactivated is gone, so there is no "expected UID" to honor.
	if err := s.deactivatePrivilegedTiDB(namespace, ""); err != nil {
		return false, fmt.Errorf("preempt stale privileged TiDB: %w", err)
	}
	return true, nil
}

// startupRecoveryTimeout caps the total time spent in RecoverPrivilegedTiDBOnStartup
// to prevent blocking agent startup on network or TLS issues.
const startupRecoveryTimeout = 15 * time.Second

// RecoverPrivilegedTiDBOnStartup deactivates a privileged TiDB pod that is
// stuck in activated/SYSTEM state from a previous agent instance whose
// deactivation goroutine was lost on restart.
//
// Safety assumption: cluster-agent must run as a single-replica deployment
// (maxSurge=0 or Recreate strategy). This is already required by the
// in-process mutexPrivilegedTiDBActivation, which provides no cross-pod
// protection. With single-replica deployment, no previous pod can be serving
// requests at the time this runs, so force-deactivating the pod is safe.
//
// Non-fatal: returns after startupRecoveryTimeout or on any unrecoverable error.
func (s *AgentService) RecoverPrivilegedTiDBOnStartup(ctx context.Context) {
	namespace := os.Getenv("NAMESPACE")
	if namespace == "" {
		return
	}
	logger := tilog.WithContextS(ctx).With("namespace", namespace)

	// Bound total startup delay to avoid blocking the gRPC server on network issues.
	recoverCtx, cancel := context.WithTimeout(ctx, startupRecoveryTimeout)
	defer cancel()

	// List services directly so we can distinguish "not found" (expected for
	// deployments without privileged TiDB) from real API or config errors.
	var svcList corev1.ServiceList
	if err := s.dataplaneCli.List(recoverCtx, &svcList,
		client.InNamespace(namespace),
		client.MatchingLabels{PingcapComponentLabelKey: PrivilegedTiDBComponentType}); err != nil {
		logger.Warnf("startup recovery: failed to list privileged TiDB services: %v", err)
		return
	}
	if len(svcList.Items) == 0 {
		return // privileged TiDB is not part of this deployment
	}
	if len(svcList.Items) > 1 {
		logger.Warnf("startup recovery: expected 1 privileged TiDB service, found %d; skipping recovery", len(svcList.Items))
		return
	}
	tidbAddr := svcList.Items[0].Name

	tlsConfig, err := GetTLSConfig(recoverCtx, s.dataplaneCli, namespace, tlsClientSecret)
	if err != nil {
		logger.Warnf("startup recovery: failed to get TLS config: %v", err)
		return
	}
	httpClient := http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig:       tlsConfig,
			TLSHandshakeTimeout:   5 * time.Second,
			ResponseHeaderTimeout: 5 * time.Second,
		},
	}

	status, err := s.fetchPrivilegedTiDBStatus(recoverCtx, httpClient, tidbAddr)
	if err != nil {
		logger.Warnf("startup recovery: failed to fetch privileged TiDB status: %v", err)
		return
	}

	if status.State != "activated" || status.KeyspaceName != systemKeyspace {
		return
	}

	logger.Warnf("startup recovery: privileged TiDB stuck in SYSTEM keyspace from previous instance, force-deactivating")

	// Inline delete instead of deactivatePrivilegedTiDB: that function creates
	// its own context.Background() so it can outlive the caller's request ctx.
	// Here we need the opposite — the delete must be bounded by recoverCtx so
	// the 15s startup cap is actually enforced.
	var podList corev1.PodList
	if err := s.dataplaneCli.List(recoverCtx, &podList,
		client.InNamespace(namespace),
		client.MatchingLabels{PingcapComponentLabelKey: PrivilegedTiDBComponentType}); err != nil {
		logger.Warnf("startup recovery: failed to list privileged TiDB pods: %v", err)
		return
	}
	pod, err := findSingleRunningPod(podList.Items, namespace, PrivilegedTiDBComponentType)
	if errors.Is(err, errNoRunningPods) {
		return // already deactivated; operator will recreate as standby
	}
	if err != nil {
		logger.Warnf("startup recovery: %v", err)
		return
	}
	if err := s.dataplaneCli.Delete(recoverCtx, pod); err != nil {
		logger.Warnf("startup recovery: failed to delete privileged TiDB pod: %v", err)
	}
}

// isActivatedKeyspaceStale returns true if there are no TiDB pods labelled
// with the given keyspace (== clusterID) in the namespace. A zero-pod result
// means the cluster has been deleted (no Pending / Running / Terminating
// pods at all). Any pod -- in any phase -- is treated as evidence that the
// cluster still exists, to avoid misinterpreting transient zero-pod windows
// (rolling updates etc., though PDB normally prevents those).
func (s *AgentService) isActivatedKeyspaceStale(ctx context.Context, namespace, keyspace string) (bool, error) {
	if keyspace == "" || keyspace == systemKeyspace {
		return false, nil
	}
	var pods corev1.PodList
	if err := s.dataplaneCli.List(ctx, &pods,
		client.InNamespace(namespace),
		client.MatchingLabels{
			"pingcap.com/component":      "tidb",
			"tags.tidbcloud.com/cluster": keyspace,
		}); err != nil {
		return false, fmt.Errorf("list TiDB pods for cluster %s in namespace %s: %w", keyspace, namespace, err)
	}
	return len(pods.Items) == 0, nil
}
