package ports

import (
	"context"
	"errors"
	"fmt"
	"sync"
)

// ErrTransactionAborted marks a transaction that was explicitly made unsafe
// before commit. The transaction manager must roll back and return a typed
// error instead of committing once this marker is set.
var ErrTransactionAborted = errors.New("transaction aborted")

type transactionAbortKey struct{}
type transactionAbortDisabled struct{}

type transactionAbortState struct {
	mu            sync.Mutex
	cancel        context.CancelCauseFunc
	err           error
	commitStarted bool
}

// TransactionAbortedError is returned when a transaction is explicitly aborted
// through AbortTransaction. Cause is preserved for classifiers; for example, a
// parent-lane contention cause remains discoverable via errors.Is.
type TransactionAbortedError struct {
	Cause error
}

func (e *TransactionAbortedError) Error() string {
	if e == nil || e.Cause == nil || errors.Is(e.Cause, ErrTransactionAborted) {
		return ErrTransactionAborted.Error()
	}
	return fmt.Sprintf("transaction aborted: %v", e.Cause)
}

func (e *TransactionAbortedError) Is(target error) bool {
	return target == ErrTransactionAborted
}

func (e *TransactionAbortedError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

// NewTransactionAbortedError returns the typed transaction-abort error. The
// optional cause is preserved as the unwrap target so existing retry and metrics
// classifiers can continue to inspect the underlying reason.
func NewTransactionAbortedError(cause error) *TransactionAbortedError {
	return &TransactionAbortedError{Cause: cause}
}

// WithTransactionAbort attaches a transaction abort controller to ctx and
// returns a cancellable child context. Transaction managers should install this
// on the context they pass to business code and call PrepareTransactionCommit
// before attempting commit.
func WithTransactionAbort(ctx context.Context) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	abortCtx, cancel := context.WithCancelCause(ctx)
	return context.WithValue(abortCtx, transactionAbortKey{}, &transactionAbortState{cancel: cancel})
}

// WithoutTransactionAbort strips any inherited transaction abort controller.
// Detached transactions must not be able to abort an outer transaction.
func WithoutTransactionAbort(ctx context.Context) context.Context {
	if ctx == nil {
		ctx = context.Background()
	}
	return context.WithValue(ctx, transactionAbortKey{}, transactionAbortDisabled{})
}

// TransactionAbortSupported reports whether ctx carries a transaction abort
// controller installed by a transaction manager.
func TransactionAbortSupported(ctx context.Context) bool {
	return transactionAbortStateFromContext(ctx) != nil
}

// AbortTransaction marks ctx's current transaction unsafe to commit and cancels
// the transaction context so in-flight work can observe the abort immediately.
// It returns false when ctx is not a transaction context with abort support.
func AbortTransaction(ctx context.Context, cause error) bool {
	state := transactionAbortStateFromContext(ctx)
	if state == nil {
		return false
	}
	abortErr := NewTransactionAbortedError(cause)
	state.mu.Lock()
	if state.commitStarted {
		state.mu.Unlock()
		return false
	}
	if state.err == nil {
		state.err = abortErr
		state.cancel(abortErr)
	}
	state.mu.Unlock()
	return true
}

// PrepareTransactionCommit closes the pre-commit abort window for ctx. It
// returns a typed abort error when the transaction was already marked unsafe;
// otherwise it marks the transaction as entering commit so future abort attempts
// cannot claim to have vetoed that commit.
func PrepareTransactionCommit(ctx context.Context) error {
	state := transactionAbortStateFromContext(ctx)
	if state == nil {
		return nil
	}
	state.mu.Lock()
	defer state.mu.Unlock()
	if state.err != nil {
		return state.err
	}
	state.commitStarted = true
	return nil
}

// TransactionAbortErr returns the typed abort error set on ctx, if any. A nil
// result means the transaction has not been explicitly aborted.
func TransactionAbortErr(ctx context.Context) error {
	state := transactionAbortStateFromContext(ctx)
	if state == nil {
		return nil
	}
	state.mu.Lock()
	err := state.err
	state.mu.Unlock()
	return err
}

func transactionAbortStateFromContext(ctx context.Context) *transactionAbortState {
	if ctx == nil {
		return nil
	}
	state, _ := ctx.Value(transactionAbortKey{}).(*transactionAbortState)
	return state
}
