package persistence

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

	apperrors "github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/backend/internal/infrastructure/database"
)

// txContextKey is the key for storing transaction in context
type txContextKey struct{}

// TransactionManager handles database transactions with retry logic for deadlocks
type TransactionManager struct {
	db *database.TiDBConnection
}

// NewTransactionManager creates a new TransactionManager
func NewTransactionManager(db *database.TiDBConnection) *TransactionManager {
	return &TransactionManager{db: db}
}

// WithTransaction executes a function within a database transaction.
// The transaction is automatically rolled back if the function returns an error or panics.
// The transaction is committed if the function returns nil.
// It injects the transaction into the context for downstream consumers.
func (tm *TransactionManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
	tx, err := tm.db.Begin()
	if err != nil {
		return apperrors.NewInternalError("failed to begin transaction", err)
	}

	// Ensure rollback on panic
	defer func() {
		if p := recover(); p != nil {
			_ = tx.Rollback()
			panic(p) // Re-throw panic after rollback
		}
	}()

	// Inject transaction into context
	txCtx := tm.InjectTx(ctx, tx)

	// Execute the function
	if err := fn(txCtx); err != nil {
		if rbErr := tx.Rollback(); rbErr != nil {
			return apperrors.NewInternalError(fmt.Sprintf("transaction failed (rollback error: %v)", rbErr), err)
		}
		return err
	}

	// Commit the transaction
	if err := tx.Commit(); err != nil {
		return apperrors.NewInternalError("failed to commit transaction", err)
	}

	return nil
}

// WithRetry executes a function within a transaction with automatic retry on deadlock.
// Deadlocks are retried up to maxRetries times with exponential backoff.
// Other errors are returned immediately without retry.
func (tm *TransactionManager) WithRetry(ctx context.Context, fn func(ctx context.Context) error, maxRetries int) error {
	if maxRetries < 1 {
		maxRetries = 1
	}

	var lastErr error
	for attempt := 0; attempt < maxRetries; attempt++ {
		err := tm.WithTransaction(ctx, fn)
		if err == nil {
			return nil // Success
		}

		lastErr = err

		// Check if it's a deadlock error
		if !isDeadlock(err) {
			return err // Not a deadlock, return immediately
		}

		// Deadlock detected, retry with exponential backoff
		if attempt < maxRetries-1 {
			backoff := time.Millisecond * time.Duration(100*(1<<uint(attempt)))
			time.Sleep(backoff)
		}
	}

	return apperrors.NewInternalError(fmt.Sprintf("transaction failed after %d retries", maxRetries), lastErr)
}

// InjectTx injects a transaction into the context
func (tm *TransactionManager) InjectTx(ctx context.Context, tx *sql.Tx) context.Context {
	return context.WithValue(ctx, txContextKey{}, tx)
}

// DetachTx returns a context with no active transaction. The returned context
// will start a fresh, independent transaction when passed to WithTransaction /
// RunInTransaction. Any existing transaction value is overwritten with nil so
// that IsTransactionActive returns false and RunInTransaction uses WithRetry
// to begin a new independent transaction.
//
// This is used by automation steps (e.g. AfterSave flows) that need to write
// to the same row the enclosing outbox transaction already holds a lock on —
// passing the original txCtx would cause a TiDB lock-wait timeout (Error 1205).
func (tm *TransactionManager) DetachTx(ctx context.Context) context.Context {
	// Store a typed nil. IsTransactionActive / ExtractTx both guard against
	// nil *sql.Tx so this correctly signals "no active transaction".
	return context.WithValue(ctx, txContextKey{}, (*sql.Tx)(nil))
}

// ExtractTx extracts a transaction from the context.
// Returns nil (untyped) when no transaction is active or when the stored
// transaction pointer is nil (e.g. after DetachTx).
func (tm *TransactionManager) ExtractTx(ctx context.Context) interface{} {
	if tx, ok := ctx.Value(txContextKey{}).(*sql.Tx); ok && tx != nil {
		return tx
	}
	return nil
}

// IsTransactionActive checks if a transaction is currently active in the context
func (tm *TransactionManager) IsTransactionActive(ctx context.Context) bool {
	return tm.ExtractTx(ctx) != nil
}

// WithIsolationLevel executes a function within a transaction with a specific isolation level.
// Supported levels: ReadUncommitted, ReadCommitted, RepeatableRead, Serializable
func (tm *TransactionManager) WithIsolationLevel(
	level IsolationLevel,
	ctx context.Context,
	fn func(ctx context.Context) error,
) error {
	// Validate isolation level to prevent injection
	switch level {
	case ReadUncommitted, ReadCommitted, RepeatableRead, Serializable:
		// Valid
	default:
		return apperrors.NewValidationError("isolationLevel", fmt.Sprintf("invalid isolation level: %s", level))
	}

	return tm.WithTransaction(ctx, func(txCtx context.Context) error {
		txRaw := tm.ExtractTx(txCtx)
		if txRaw == nil {
			return apperrors.NewInternalError("failed to extract transaction for isolation level", nil)
		}

		tx, ok := txRaw.(*sql.Tx)
		if !ok {
			return apperrors.NewInternalError("transaction object is not *sql.Tx", nil)
		}

		// Set isolation level
		// Note: database/sql doesn't support changing isolation level strictly on existing tx object portably
		// but TiDB/MySQL supports executing SET TRANSACTION.
		_, err := tx.Exec(fmt.Sprintf("SET TRANSACTION ISOLATION LEVEL %s", level))
		if err != nil {
			return apperrors.NewInternalError("failed to set isolation level", err)
		}

		return fn(txCtx)
	})
}

// IsolationLevel represents SQL transaction isolation levels
type IsolationLevel string

const (
	ReadUncommitted IsolationLevel = "READ UNCOMMITTED"
	ReadCommitted   IsolationLevel = "READ COMMITTED"
	RepeatableRead  IsolationLevel = "REPEATABLE READ"
	Serializable    IsolationLevel = "SERIALIZABLE"
)

// isDeadlock checks if an error is a deadlock error.
func isDeadlock(err error) bool {
	if err == nil {
		return false
	}

	errMsg := strings.ToLower(err.Error())
	return strings.Contains(errMsg, "deadlock") ||
		strings.Contains(errMsg, "lock wait timeout") ||
		strings.Contains(errMsg, ErrCodeDeadlock) ||
		strings.Contains(errMsg, ErrCodeLockWait)
}
