package mysql

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

	mysqlerr "github.com/go-sql-driver/mysql"
	"hyapp/pkg/appcode"
	roomservice "hyapp/services/room-service/internal/room/service"
)

// EnsureGiftOperation 在第一次可能调用钱包前固化恢复输入；重复 command_id 只返回原记录，不能覆盖首次载荷。
func (r *Repository) EnsureGiftOperation(ctx context.Context, operation roomservice.GiftOperation) (roomservice.GiftOperation, bool, error) {
	operation.AppCode = normalizedRecordAppCode(ctx, operation.AppCode)
	if operation.Status == "" {
		operation.Status = roomservice.GiftOperationStatusPending
	}
	_, err := r.db.ExecContext(ctx, `
		INSERT INTO room_gift_operations (
		  app_code, room_id, command_id, actor_user_id, command_payload, request_payload, status,
		  billing_payload, lucky_payload, result_payload, last_error, retry_count, next_retry_at_ms,
		  worker_id, lock_until_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, '', NULL, ?, ?)
	`, operation.AppCode, operation.RoomID, operation.CommandID, operation.ActorUserID,
		operation.CommandPayload, operation.RequestPayload, operation.Status,
		nullableBytes(operation.BillingPayload), nullableBytes(operation.LuckyPayload), nullableBytes(operation.ResultPayload),
		nullableString(operation.LastError), operation.NextRetryAtMS, operation.CreatedAtMS, operation.UpdatedAtMS)
	if err == nil {
		return operation, true, nil
	}
	var duplicate *mysqlerr.MySQLError
	if !errors.As(err, &duplicate) || duplicate.Number != 1062 {
		return roomservice.GiftOperation{}, false, err
	}

	existing, exists, err := r.GetGiftOperation(ctx, operation.RoomID, operation.CommandID)
	if err != nil || !exists {
		return roomservice.GiftOperation{}, false, err
	}
	// A duplicate command may carry a different gift/recipient/count. Do not reactivate compensated
	// work here: the service must first compare normalized idempotency payloads, otherwise a rejected
	// conflicting request could make the old stored request claimable and debit it in the background.
	return existing, false, nil
}

// ReactivateCompensatedGiftOperation reopens a confirmed-identical request. The status predicate is
// the concurrency boundary: if another caller already moved it forward, return the authoritative row
// and let the shared command_id idempotency path converge instead of overwriting its progress.
func (r *Repository) ReactivateCompensatedGiftOperation(ctx context.Context, roomID string, commandID string, updatedAtMS int64) (roomservice.GiftOperation, bool, error) {
	result, err := r.db.ExecContext(ctx, `
		UPDATE room_gift_operations
		   SET status = ?, last_error = NULL, retry_count = 0, next_retry_at_ms = 0,
		       worker_id = '', lock_until_ms = NULL, updated_at_ms = ?
		 WHERE app_code = ? AND room_id = ? AND command_id = ? AND status = ?
	`, roomservice.GiftOperationStatusPending, updatedAtMS, appcode.FromContext(ctx), roomID, commandID,
		roomservice.GiftOperationStatusCompensated)
	if err != nil {
		return roomservice.GiftOperation{}, false, err
	}
	affected, err := result.RowsAffected()
	if err != nil {
		return roomservice.GiftOperation{}, false, err
	}
	operation, exists, err := r.GetGiftOperation(ctx, roomID, commandID)
	if err != nil {
		return roomservice.GiftOperation{}, false, err
	}
	if !exists {
		return roomservice.GiftOperation{}, false, fmt.Errorf("gift operation disappeared during reactivation: room_id=%s command_id=%s", roomID, commandID)
	}
	return operation, affected == 1, nil
}

func (r *Repository) GetGiftOperation(ctx context.Context, roomID string, commandID string) (roomservice.GiftOperation, bool, error) {
	row := r.db.QueryRowContext(ctx, `
		SELECT app_code, room_id, command_id, actor_user_id, command_payload, request_payload, status,
		       billing_payload, lucky_payload, result_payload, COALESCE(last_error, ''), retry_count,
		       next_retry_at_ms, worker_id, lock_until_ms, created_at_ms, updated_at_ms
		  FROM room_gift_operations
		 WHERE app_code = ? AND room_id = ? AND command_id = ?
		 LIMIT 1
	`, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID))
	operation, err := scanGiftOperation(row)
	if errors.Is(err, sql.ErrNoRows) {
		return roomservice.GiftOperation{}, false, nil
	}
	if err != nil {
		return roomservice.GiftOperation{}, false, err
	}
	return operation, true, nil
}

// SaveGiftOperationProgress 只允许 saga 向前推进；恢复重放 wallet 时写 debited 不能把已 drawn 的操作降级。
func (r *Repository) SaveGiftOperationProgress(ctx context.Context, progress roomservice.GiftOperationProgress) error {
	statusRank, ok := giftOperationStatusRank(progress.Status)
	if !ok || statusRank < giftOperationStatusRankMust(roomservice.GiftOperationStatusDebited) || statusRank > giftOperationStatusRankMust(roomservice.GiftOperationStatusDrawn) {
		return fmt.Errorf("invalid gift operation progress status %q", progress.Status)
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE room_gift_operations
		   SET status = CASE
		         WHEN status = ? THEN status
		         WHEN status = ? THEN status
		         WHEN status = ? AND ? = ? THEN status
		         ELSE ?
		       END,
		       billing_payload = COALESCE(?, billing_payload),
		       lucky_payload = COALESCE(?, lucky_payload),
		       last_error = NULL, next_retry_at_ms = ?, updated_at_ms = ?
		 WHERE app_code = ? AND room_id = ? AND command_id = ?
		   AND status IN (?, ?, ?)
	`, roomservice.GiftOperationStatusCommitted, roomservice.GiftOperationStatusCompensated,
		roomservice.GiftOperationStatusDrawn, progress.Status, roomservice.GiftOperationStatusDebited, progress.Status,
		nullableBytes(progress.BillingPayload), nullableBytes(progress.LuckyPayload), progress.NextRetryAtMS, progress.UpdatedAtMS,
		appcode.FromContext(ctx), progress.RoomID, progress.CommandID,
		roomservice.GiftOperationStatusPending, roomservice.GiftOperationStatusDebited, roomservice.GiftOperationStatusDrawn)
	if err != nil {
		return err
	}
	return nil
}

// ClaimRecoverableGiftOperations 用短租约分摊恢复任务；command_id 幂等是最终防线，租约只避免多实例重复施压。
func (r *Repository) ClaimRecoverableGiftOperations(ctx context.Context, workerID string, limit int, nowMS int64, lockUntilMS int64) ([]roomservice.GiftOperation, error) {
	if limit <= 0 {
		return nil, nil
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	rows, err := tx.QueryContext(ctx, `
		SELECT app_code, room_id, command_id, actor_user_id, command_payload, request_payload, status,
		       billing_payload, lucky_payload, result_payload, COALESCE(last_error, ''), retry_count,
		       next_retry_at_ms, worker_id, lock_until_ms, created_at_ms, updated_at_ms
		  FROM room_gift_operations
		 WHERE status IN (?, ?, ?)
		   AND next_retry_at_ms <= ?
		   AND (lock_until_ms IS NULL OR lock_until_ms <= ?)
		 ORDER BY updated_at_ms ASC, command_id ASC
		 LIMIT ?
		 FOR UPDATE SKIP LOCKED
	`, roomservice.GiftOperationStatusPending, roomservice.GiftOperationStatusDebited, roomservice.GiftOperationStatusDrawn, nowMS, nowMS, limit)
	if err != nil {
		_ = tx.Rollback()
		return nil, err
	}
	operations := make([]roomservice.GiftOperation, 0, limit)
	for rows.Next() {
		operation, err := scanGiftOperation(rows)
		if err != nil {
			_ = rows.Close()
			_ = tx.Rollback()
			return nil, err
		}
		operations = append(operations, operation)
	}
	if err := rows.Close(); err != nil {
		_ = tx.Rollback()
		return nil, err
	}
	for index := range operations {
		operation := &operations[index]
		if _, err := tx.ExecContext(ctx, `
			UPDATE room_gift_operations
			   SET worker_id = ?, lock_until_ms = ?, updated_at_ms = ?
			 WHERE app_code = ? AND room_id = ? AND command_id = ?
		`, workerID, lockUntilMS, nowMS, operation.AppCode, operation.RoomID, operation.CommandID); err != nil {
			_ = tx.Rollback()
			return nil, err
		}
		operation.WorkerID = workerID
		operation.LockUntilMS = lockUntilMS
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return operations, nil
}

func (r *Repository) MarkGiftOperationRetry(ctx context.Context, roomID string, commandID string, lastError string, nextRetryAtMS int64, updatedAtMS int64, preserveLease bool) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE room_gift_operations
		   SET retry_count = retry_count + 1, last_error = ?, next_retry_at_ms = ?,
		       worker_id = CASE WHEN ? THEN worker_id ELSE '' END,
		       lock_until_ms = CASE WHEN ? THEN lock_until_ms ELSE NULL END,
		       updated_at_ms = ?
		 WHERE app_code = ? AND room_id = ? AND command_id = ? AND status IN (?, ?, ?)
	`, nullableString(strings.TrimSpace(lastError)), nextRetryAtMS, preserveLease, preserveLease, updatedAtMS, appcode.FromContext(ctx), roomID, commandID,
		roomservice.GiftOperationStatusPending, roomservice.GiftOperationStatusDebited, roomservice.GiftOperationStatusDrawn)
	return err
}

func (r *Repository) MarkGiftOperationCompensated(ctx context.Context, roomID string, commandID string, lastError string, updatedAtMS int64) error {
	_, err := r.db.ExecContext(ctx, `
		UPDATE room_gift_operations
		   SET status = ?, last_error = ?, next_retry_at_ms = 0, worker_id = '', lock_until_ms = NULL, updated_at_ms = ?
		 WHERE app_code = ? AND room_id = ? AND command_id = ? AND status = ?
	`, roomservice.GiftOperationStatusCompensated, nullableString(strings.TrimSpace(lastError)), updatedAtMS,
		appcode.FromContext(ctx), roomID, commandID, roomservice.GiftOperationStatusPending)
	return err
}

func (r *Repository) HasRecoverableGiftOperations(ctx context.Context, roomID string) (bool, error) {
	var exists int
	err := r.db.QueryRowContext(ctx, `
		SELECT 1
		  FROM room_gift_operations
		 WHERE app_code = ? AND room_id = ? AND status IN (?, ?, ?)
		 LIMIT 1
	`, appcode.FromContext(ctx), strings.TrimSpace(roomID), roomservice.GiftOperationStatusPending,
		roomservice.GiftOperationStatusDebited, roomservice.GiftOperationStatusDrawn).Scan(&exists)
	if errors.Is(err, sql.ErrNoRows) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

type giftOperationScanner interface {
	Scan(dest ...any) error
}

func scanGiftOperation(scanner giftOperationScanner) (roomservice.GiftOperation, error) {
	var operation roomservice.GiftOperation
	var billingPayload, luckyPayload, resultPayload []byte
	var lockUntil sql.Null[int64]
	err := scanner.Scan(&operation.AppCode, &operation.RoomID, &operation.CommandID, &operation.ActorUserID,
		&operation.CommandPayload, &operation.RequestPayload, &operation.Status,
		&billingPayload, &luckyPayload, &resultPayload, &operation.LastError, &operation.RetryCount,
		&operation.NextRetryAtMS, &operation.WorkerID, &lockUntil, &operation.CreatedAtMS, &operation.UpdatedAtMS)
	if err != nil {
		return roomservice.GiftOperation{}, err
	}
	operation.BillingPayload = billingPayload
	operation.LuckyPayload = luckyPayload
	operation.ResultPayload = resultPayload
	if lockUntil.Valid {
		operation.LockUntilMS = lockUntil.V
	}
	return operation, nil
}

func giftOperationStatusRank(status string) (int, bool) {
	switch status {
	case roomservice.GiftOperationStatusPending:
		return 1, true
	case roomservice.GiftOperationStatusDebited:
		return 2, true
	case roomservice.GiftOperationStatusDrawn:
		return 3, true
	case roomservice.GiftOperationStatusCommitted:
		return 4, true
	case roomservice.GiftOperationStatusCompensated:
		return 5, true
	default:
		return 0, false
	}
}

func giftOperationStatusRankMust(status string) int {
	rank, _ := giftOperationStatusRank(status)
	return rank
}
