package service

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	"github.com/go-sql-driver/mysql"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"k8s.io/apimachinery/pkg/types"

	"github.com/tidbcloud/cluster-service-ng/internal/cluster-agent/internal/sqlexec"
)

// privilegedTiDBLifecycle adapts AgentService privileged TiDB lifecycle methods to sqlexec.PrivilegedLifecycle.
type privilegedTiDBLifecycle struct {
	svc *AgentService

	namespace      string
	enableAuditlog bool

	// deactivationWait is the duration to wait before deactivating the privileged TiDB.
	// Zero falls back to AgentService.privilegedTiDBDeactivationWait, which itself
	// defaults to defaultPrivilegedTiDBDeactivationWait when unset.
	deactivationWait time.Duration

	username string
	password string

	// activatedPodUID is the UID of the privileged TiDB pod we activated in Prepare.
	// ScheduleDeactivation passes this UID to deactivatePrivilegedTiDB so that a
	// replacement pod (possibly activated by another concurrent request) is not
	// accidentally deleted. Empty if UID capture failed; in that case deactivation
	// falls back to deleting whichever running pod it finds.
	activatedPodUID types.UID
}

var _ sqlexec.PrivilegedLifecycle = (*privilegedTiDBLifecycle)(nil)

func (l *privilegedTiDBLifecycle) Prepare(ctx context.Context, clusterID string) (string, error) {
	if l == nil || l.svc == nil {
		return "", fmt.Errorf("lifecycle is nil")
	}

	tidbAddr, err := l.svc.getPrivilegedTiDBServiceAddress(ctx, l.namespace)
	if err != nil {
		return "", fmt.Errorf("get privileged TiDB service address: %w", err)
	}
	tilog.WithContextS(ctx).With("cluster_id", clusterID, "namespace", l.namespace, "tidb_addr", tidbAddr).
		Infof("resolved privileged TiDB service address")

	if err := l.svc.prepareForPrivilegedTiDBExecution(ctx, clusterID, l.namespace, tidbAddr, l.enableAuditlog); err != nil {
		return "", fmt.Errorf("prepare for privileged TiDB execution: %w", err)
	}

	// Capture the activated pod's UID so deactivation only deletes this specific pod.
	// Failure here is non-fatal: we fall back to the legacy best-effort deletion.
	if uid, err := l.svc.getPrivilegedTiDBPodUID(ctx, l.namespace); err != nil {
		tilog.WithContextS(ctx).With("cluster_id", clusterID, "namespace", l.namespace).
			Warnf("failed to capture privileged TiDB pod UID, deactivation will fall back to best-effort: %v", err)
	} else {
		l.activatedPodUID = uid
	}

	return tidbAddr, nil
}

func (l *privilegedTiDBLifecycle) WaitReady(ctx context.Context, tidbAddr string) (*sql.DB, error) {
	if l == nil || l.svc == nil {
		return nil, fmt.Errorf("lifecycle is nil")
	}
	return l.svc.waitForTiDBReadyWithAuth(ctx, tidbAddr, l.username, l.password)
}

func (l *privilegedTiDBLifecycle) ScheduleDeactivation(ctx context.Context, clusterID string) {
	if l == nil || l.svc == nil {
		return
	}

	wait := l.deactivationWait
	if wait <= 0 {
		wait = l.svc.privilegedTiDBDeactivationWait
	}
	if wait <= 0 {
		wait = defaultPrivilegedTiDBDeactivationWait
	}

	expectedUID := l.activatedPodUID
	go func() {
		// Wait before deactivating the privileged TiDB,
		// so that the auditlog sidecar can hot reload the config first.
		time.Sleep(wait)
		logger := tilog.WithContextS(ctx).With("cluster_id", clusterID, "pod_uid", string(expectedUID))

		// Retry deactivation up to 3 times -- transient errors (e.g. API server
		// flakiness) can fail the first attempt.
		// NOTE: we do NOT acquire mutexPrivilegedTiDBActivation here, because
		// holding the mutex during deactivation (which may retry for 30s) would
		// block all new reset-password requests. This is especially harmful during
		// cluster initialization when many users are being set up in rapid succession.
		// Safety against racing with a newer activation is provided by the UID
		// check inside deactivatePrivilegedTiDB, not by the mutex.
		for attempt := 1; attempt <= 3; attempt++ {
			err := l.svc.deactivatePrivilegedTiDB(l.namespace, expectedUID)
			if err == nil {
				return
			}
			// deactivatePrivilegedTiDB already treats "no running pods" and UID
			// mismatch as success, so any error here is a genuine failure worth retrying.
			logger.Errorf("failed to deactivate the privileged TiDB (attempt %d/3): %v", attempt, err)
			if errors.Is(err, errNoRunningPods) {
				// Unreachable under current implementation: deactivatePrivilegedTiDB already
				// handles errNoRunningPods internally and returns nil. Kept as defense-in-depth
				// in case that internal handling changes in the future.
				return
			}
			time.Sleep(10 * time.Second)
		}
		logger.Errorf("failed to deactivate the privileged TiDB after 3 attempts")
	}()
}

// waitForTiDBReadyWithAuth waits for the privileged TiDB to be ready by checking MySQL connection
// and returns the established database connection for reuse.
func (s *AgentService) waitForTiDBReadyWithAuth(
	ctx context.Context,
	tidbAddr string,
	username string,
	password string,
) (*sql.DB, error) {
	cfg := mysql.Config{
		User:   username,
		Passwd: password,
		Net:    "tcp",
		Addr:   fmt.Sprintf("%s:4000", tidbAddr),
		Params: map[string]string{
			"charset": "utf8mb4",
		},
		Loc:                  time.UTC,
		Timeout:              5 * time.Second,
		ReadTimeout:          time.Second * 30,
		WriteTimeout:         time.Second * 30,
		AllowNativePasswords: true,
		CheckConnLiveness:    true,
	}

	tilog.WithContextS(ctx).With("tidb_addr", tidbAddr, "username", username).
		Infof("waiting for privileged TiDB to become ready")

	var lastErr error
	for i := 0; i < 30; i++ {
		select {
		case <-ctx.Done():
			return nil, fmt.Errorf("context cancelled while waiting for privileged TiDB to be ready: %w", ctx.Err())
		default:
		}

		db, err := sql.Open("mysql", cfg.FormatDSN())
		if err != nil {
			lastErr = err
			tilog.Warn(ctx, "failed to open MySQL connection to privileged TiDB",
				zap.String("error", sqlexec.SanitizeSQLError(err)))
			time.Sleep(time.Second)
			continue
		}

		if err = db.PingContext(ctx); err == nil {
			return db, nil
		}
		tilog.Warn(ctx, "failed to ping MySQL connection to privileged TiDB",
			zap.String("error", sqlexec.SanitizeSQLError(err)))
		lastErr = err
		_ = db.Close()
		time.Sleep(time.Second)
	}

	tilog.WithContextS(ctx).With("tidb_addr", tidbAddr, "username", username).
		Errorf("privileged TiDB not ready after 30 retries: %v", sqlexec.SanitizeSQLError(lastErr))
	return nil, fmt.Errorf("privileged TiDB not ready after 30 retries: %w", lastErr)
}
