package meta

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"
)

// setLegacyTiDBCloudFreeClassificationTx preserves old-binary free-plan
// behavior during the rollback window. New code never reads this value to
// classify or count tenants.
func setLegacyTiDBCloudFreeClassificationTx(ctx context.Context, tx *sql.Tx, tenantID string, free bool) error {
	var legacyValue any
	if free {
		legacyValue = int64(0)
	}
	if _, err := tx.ExecContext(ctx,
		`UPDATE tenant_quota_config SET tidbcloud_spending_limit = ? WHERE tenant_id = ?`,
		legacyValue, tenantID); err != nil {
		return fmt.Errorf("write legacy free tenant classification: %w", err)
	}
	return nil
}

func (s *Store) InsertTiDBCloudFreeTenant(ctx context.Context, t *Tenant, quota QuotaConfigPatch) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "insert_tidbcloud_free_tenant", start, &err)
	if t == nil {
		return fmt.Errorf("tenant is required")
	}
	if quota.MaxStorageBytes == nil || *quota.MaxStorageBytes <= 0 ||
		quota.MaxFileSizeBytes == nil || *quota.MaxFileSizeBytes <= 0 ||
		quota.MaxFileCount == nil || *quota.MaxFileCount <= 0 {
		return fmt.Errorf("explicit free tenant quota is incomplete")
	}
	return withMetaLockConflictRetry(ctx, "insert_tidbcloud_free_tenant", func() error {
		return s.InTx(ctx, func(tx *sql.Tx) error {
			if err := insertTenantTx(ctx, tx, t); err != nil {
				return err
			}
			_, err := tx.ExecContext(ctx, `INSERT INTO tenant_quota_config
					(tenant_id, max_storage_bytes, max_file_size_bytes, max_file_count,
					 max_media_llm_files, max_video_llm_files)
					VALUES (?, ?, ?, ?, ?, ?)`, t.ID, *quota.MaxStorageBytes, *quota.MaxFileSizeBytes,
				*quota.MaxFileCount, DefaultMaxMediaLLMFiles(), DefaultMaxVideoLLMFiles())
			if err != nil {
				return err
			}
			if err := setLegacyTiDBCloudFreeClassificationTx(ctx, tx, t.ID, true); err != nil {
				return err
			}
			return nil
		})
	})
}

func (s *Store) CountTiDBCloudOrganizationTenants(ctx context.Context, organizationID string) (count int, err error) {
	start := time.Now()
	defer observeMeta(ctx, "count_tidbcloud_organization_tenants", start, &err)
	organizationID = strings.TrimSpace(organizationID)
	if organizationID == "" {
		return 0, fmt.Errorf("tidbcloud organization id is required")
	}
	err = s.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(tenant_count), 0)
			FROM (
				SELECT COUNT(*) AS tenant_count
				FROM tenant_tidbcloud_org_bindings b
				STRAIGHT_JOIN tenants t ON t.id = b.tenant_id
				WHERE b.organization_id = ?
					AND b.pool_status <> ?
					AND t.provider = ?
					AND t.status <> ?
				UNION ALL
				SELECT COUNT(DISTINCT t.id) AS tenant_count
				FROM db_pool d
				STRAIGHT_JOIN tenant_placements p ON p.db_id = d.db_id
				STRAIGHT_JOIN fs_registry f ON f.fs_id = p.fs_id
				STRAIGHT_JOIN tenants t ON t.id = f.tenant_id
				LEFT JOIN tenant_pool_memberships m ON m.tenant_id = t.id
				WHERE d.org_id = ?
					AND t.provider = ?
					AND t.status <> ?
					AND (m.tenant_id IS NULL OR m.pool_status <> ?)
			) organization_tenants`,
		organizationID, TenantPoolBindingFree, tidbCloudNativeProvider, TenantDeleted,
		organizationID, tidbCloudNativeSharedProvider, TenantDeleted, TenantPoolBindingFree).Scan(&count)
	return count, err
}

// HasTenantPoolOwnership reports whether a tenant is explicitly associated
// with a native pool binding, logical pool membership, or shared DB placement.
func (s *Store) HasTenantPoolOwnership(ctx context.Context, tenantID string) (owned bool, err error) {
	start := time.Now()
	defer observeMeta(ctx, "has_tenant_pool_ownership", start, &err)
	tenantID = strings.TrimSpace(tenantID)
	if tenantID == "" {
		return false, fmt.Errorf("tenant id is required")
	}
	err = s.db.QueryRowContext(ctx, `SELECT
		EXISTS (
			SELECT 1 FROM tenant_tidbcloud_org_bindings b
			WHERE b.tenant_id = ? AND TRIM(b.pool_id) <> '')
		OR EXISTS (
			SELECT 1 FROM tenant_pool_memberships m WHERE m.tenant_id = ?)
		OR EXISTS (
			SELECT 1 FROM fs_registry f
			JOIN tenant_placements p ON p.fs_id = f.fs_id
			WHERE f.tenant_id = ?)`, tenantID, tenantID, tenantID).Scan(&owned)
	if err != nil {
		return false, fmt.Errorf("check tenant pool ownership %s: %w", tenantID, err)
	}
	return owned, nil
}

func upsertTenantQuotaPatchTx(ctx context.Context, tx *sql.Tx, tenantID string, patch QuotaConfigPatch) error {
	insertStorage := DefaultMaxStorageBytes()
	if patch.MaxStorageBytes != nil {
		insertStorage = *patch.MaxStorageBytes
	}
	insertFileSize := DefaultMaxFileSizeBytes()
	if patch.MaxFileSizeBytes != nil {
		insertFileSize = *patch.MaxFileSizeBytes
	}
	insertFileCount := DefaultMaxFileCount()
	if patch.MaxFileCount != nil {
		insertFileCount = *patch.MaxFileCount
	}
	insertMediaLLM := DefaultMaxMediaLLMFiles()
	if patch.MaxMediaLLMFiles != nil {
		insertMediaLLM = *patch.MaxMediaLLMFiles
	}
	insertVideoLLM := DefaultMaxVideoLLMFiles()
	if patch.MaxVideoLLMFiles != nil {
		insertVideoLLM = *patch.MaxVideoLLMFiles
	}
	updateStorage := sql.NullInt64{}
	if patch.MaxStorageBytes != nil {
		updateStorage = sql.NullInt64{Int64: *patch.MaxStorageBytes, Valid: true}
	}
	updateFileSize := sql.NullInt64{}
	if patch.MaxFileSizeBytes != nil {
		updateFileSize = sql.NullInt64{Int64: *patch.MaxFileSizeBytes, Valid: true}
	}
	updateFileCount := sql.NullInt64{}
	if patch.MaxFileCount != nil {
		updateFileCount = sql.NullInt64{Int64: *patch.MaxFileCount, Valid: true}
	}
	updateMediaLLM := sql.NullInt64{}
	if patch.MaxMediaLLMFiles != nil {
		updateMediaLLM = sql.NullInt64{Int64: *patch.MaxMediaLLMFiles, Valid: true}
	}
	updateVideoLLM := sql.NullInt64{}
	if patch.MaxVideoLLMFiles != nil {
		updateVideoLLM = sql.NullInt64{Int64: *patch.MaxVideoLLMFiles, Valid: true}
	}
	insertSpendingLimit := nullInt64FromPtr(patch.TiDBCloudSpendingLimit)
	updateSpendingLimit := nullInt64FromPtr(patch.TiDBCloudSpendingLimit)
	_, err := tx.ExecContext(ctx, `INSERT INTO tenant_quota_config
		(tenant_id, max_storage_bytes, max_file_size_bytes, max_file_count,
		 max_media_llm_files, max_video_llm_files, tidbcloud_spending_limit)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
		 max_storage_bytes = COALESCE(?, max_storage_bytes),
		 max_file_size_bytes = COALESCE(?, max_file_size_bytes),
		 max_file_count = COALESCE(?, max_file_count),
		 max_media_llm_files = COALESCE(?, max_media_llm_files),
		 max_video_llm_files = COALESCE(?, max_video_llm_files),
		 tidbcloud_spending_limit = COALESCE(?, tidbcloud_spending_limit)`,
		tenantID, insertStorage, insertFileSize, insertFileCount,
		insertMediaLLM, insertVideoLLM, insertSpendingLimit,
		updateStorage, updateFileSize, updateFileCount,
		updateMediaLLM, updateVideoLLM, updateSpendingLimit)
	return err
}

func nullInt64FromPtr(v *int64) sql.NullInt64 {
	if v == nil {
		return sql.NullInt64{}
	}
	return sql.NullInt64{Int64: *v, Valid: true}
}

func (s *Store) AcquireTiDBCloudFreeQuotaLock(ctx context.Context, organizationID string) (release func() error, err error) {
	start := time.Now()
	defer observeMeta(ctx, "acquire_tidbcloud_free_quota_lock", start, &err)
	organizationID = strings.TrimSpace(organizationID)
	if organizationID == "" {
		return nil, fmt.Errorf("tidbcloud organization id is required")
	}
	conn, err := s.db.Conn(ctx)
	if err != nil {
		return nil, err
	}
	closeOnError := true
	defer func() {
		if closeOnError {
			_ = conn.Close()
		}
	}()
	var databaseName sql.NullString
	if err := conn.QueryRowContext(ctx, "SELECT DATABASE()").Scan(&databaseName); err != nil {
		return nil, err
	}
	lockName := tenantPoolDatabaseLockName(tidbCloudFreeQuotaLockName(organizationID), databaseName.String)
	var got sql.NullInt64
	if err := conn.QueryRowContext(ctx, "SELECT GET_LOCK(?, ?)", lockName, tidbCloudFreeQuotaLockTimeoutSeconds).Scan(&got); err != nil {
		return nil, err
	}
	if !got.Valid {
		return nil, fmt.Errorf("tidbcloud free quota named lock returned NULL")
	}
	if got.Int64 != 1 {
		return nil, ErrTiDBCloudFreeQuotaBusy
	}
	closeOnError = false
	var once sync.Once
	var releaseErr error
	return func() error {
		once.Do(func() {
			defer func() {
				if closeErr := conn.Close(); closeErr != nil {
					releaseErr = errors.Join(releaseErr, closeErr)
				}
			}()
			releaseCtx, cancel := context.WithTimeout(context.Background(), tidbCloudFreeQuotaReleaseLockTimeout)
			defer cancel()
			var released sql.NullInt64
			if err := conn.QueryRowContext(releaseCtx, "SELECT RELEASE_LOCK(?)", lockName).Scan(&released); err != nil {
				releaseErr = err
				return
			}
			if !released.Valid {
				releaseErr = fmt.Errorf("tidbcloud free quota named lock release returned NULL")
				return
			}
			if released.Int64 != 1 {
				releaseErr = fmt.Errorf("tidbcloud free quota named lock was not held by current connection")
			}
		})
		return releaseErr
	}, nil
}

func (s *Store) WithTiDBCloudFreeQuotaLock(ctx context.Context, organizationID string, fn func(context.Context) error) (err error) {
	start := time.Now()
	defer observeMeta(ctx, "tidbcloud_free_quota_lock", start, &err)
	if fn == nil {
		return fmt.Errorf("tidbcloud free quota lock callback is required")
	}
	release, err := s.AcquireTiDBCloudFreeQuotaLock(ctx, organizationID)
	if err != nil {
		return err
	}
	defer func() {
		err = errors.Join(err, release())
	}()
	return fn(ctx)
}

func tidbCloudFreeQuotaLockName(organizationID string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(organizationID)))
	return "d9_free_quota:" + hex.EncodeToString(sum[:16])
}
