package credentialgeneration

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

	"github.com/pingcap-inc/nexuscrm-connectors/internal/sqltx"
)

const dispatchGateSelect = `
SELECT CredentialGenerationId, Mode, FenceToken
  FROM _System_ConnectorCredentialGenerationDispatchGate
 WHERE CredentialGenerationId = ? AND BINARY CredentialGenerationId = BINARY ?
 LIMIT 1`

// DispatchGateRepository is the sole exact read and transaction-lock owner for
// credential-generation dispatch gates. It exposes no list, ensure, generic
// mode setter, mutable-name lookup, update, or delete API.
type DispatchGateRepository struct {
	db *sql.DB
}

func NewDispatchGateRepository(db *sql.DB) (*DispatchGateRepository, error) {
	if db == nil {
		return nil, fmt.Errorf("%w: dispatch gate database is nil", ErrDependencyMissing)
	}
	return &DispatchGateRepository{db: db}, nil
}

func (repository *DispatchGateRepository) VerifySchema(ctx context.Context) error {
	if repository == nil || repository.db == nil {
		return fmt.Errorf("%w: dispatch gate database is nil", ErrDependencyMissing)
	}
	if ctx == nil {
		return fmt.Errorf("%w: context is nil", ErrDependencyMissing)
	}
	return verifyDispatchGateSchema(ctx, repository.db)
}

// ResolveExact returns one exact, validated gate. Absence is a typed error and
// never an implicit enabled/default state.
func (repository *DispatchGateRepository) ResolveExact(ctx context.Context, generationID string) (DispatchGate, error) {
	if repository == nil || repository.db == nil {
		return DispatchGate{}, fmt.Errorf("%w: dispatch gate database is nil", ErrDependencyMissing)
	}
	if ctx == nil {
		return DispatchGate{}, fmt.Errorf("%w: context is nil", ErrDependencyMissing)
	}
	if !validOpaqueIdentifier(generationID) {
		return DispatchGate{}, invalidGeneration("generation ID is not canonical")
	}
	if err := repository.VerifySchema(ctx); err != nil {
		return DispatchGate{}, err
	}
	gate, found, err := loadDispatchGateExact(ctx, repository.db, generationID, false)
	if err != nil {
		return DispatchGate{}, err
	}
	if !found {
		return DispatchGate{}, fmt.Errorf("%w: %s", ErrDispatchGateNotFound, generationID)
	}
	return gate, nil
}

// DispatchGateTransitionService owns the two authorized state changes. The
// authorizer is mandatory and is invoked before the row lock; the exact state
// and fence are then revalidated under lock before the guarded update.
type DispatchGateTransitionService struct {
	repository *DispatchGateRepository
	authorizer DispatchTransitionAuthorizer
}

func NewDispatchGateTransitionService(
	repository *DispatchGateRepository,
	authorizer DispatchTransitionAuthorizer,
) (*DispatchGateTransitionService, error) {
	if repository == nil || repository.db == nil {
		return nil, fmt.Errorf("%w: dispatch gate repository is nil", ErrDependencyMissing)
	}
	if authorizer == nil {
		return nil, fmt.Errorf("%w: dispatch transition authorizer is nil", ErrDependencyMissing)
	}
	return &DispatchGateTransitionService{repository: repository, authorizer: authorizer}, nil
}

func (service *DispatchGateTransitionService) Revoke(
	ctx context.Context,
	request DispatchTransitionRequest,
) (DispatchGate, error) {
	return service.transition(ctx, request, DispatchTransitionRevoke, DispatchModeEnabled, DispatchModeRevoked)
}

func (service *DispatchGateTransitionService) Reenable(
	ctx context.Context,
	request DispatchTransitionRequest,
) (DispatchGate, error) {
	return service.transition(ctx, request, DispatchTransitionReenable, DispatchModeRevoked, DispatchModeEnabled)
}

func (service *DispatchGateTransitionService) transition(
	ctx context.Context,
	request DispatchTransitionRequest,
	action DispatchTransitionAction,
	sourceMode, targetMode DispatchMode,
) (DispatchGate, error) {
	if service == nil || service.repository == nil || service.repository.db == nil || service.authorizer == nil {
		return DispatchGate{}, fmt.Errorf("%w: dispatch transition service is incomplete", ErrDependencyMissing)
	}
	if ctx == nil {
		return DispatchGate{}, fmt.Errorf("%w: context is nil", ErrDependencyMissing)
	}
	if err := validateDispatchTransitionRequest(request); err != nil {
		return DispatchGate{}, err
	}
	if err := service.repository.VerifySchema(ctx); err != nil {
		return DispatchGate{}, err
	}
	if err := verifyCredentialGenerationSchema(ctx, service.repository.db); err != nil {
		return DispatchGate{}, err
	}

	generation, found, err := loadExact(ctx, service.repository.db, request.CredentialGenerationID, false)
	if err != nil {
		return DispatchGate{}, err
	}
	if !found {
		return DispatchGate{}, fmt.Errorf("%w: %s", ErrDispatchGenerationNotFound, request.CredentialGenerationID)
	}
	gate, found, err := loadDispatchGateExact(ctx, service.repository.db, request.CredentialGenerationID, false)
	if err != nil {
		return DispatchGate{}, err
	}
	if !found {
		return DispatchGate{}, fmt.Errorf("%w: %s", ErrDispatchGateNotFound, request.CredentialGenerationID)
	}
	if err := requireDispatchTransitionSource(gate, sourceMode, request.ExpectedFenceToken); err != nil {
		return DispatchGate{}, err
	}

	authorization := DispatchAuthorizationRequest{
		Action:                action,
		DecisionID:            request.Authorization.DecisionID,
		ActorID:               request.Authorization.ActorID,
		L2PolicyGenerationRef: request.Authorization.L2PolicyGenerationRef,
		ExpectedFenceToken:    request.ExpectedFenceToken,
		Generation:            generation.copy(),
		Gate:                  gate,
	}
	if action == DispatchTransitionRevoke {
		err = service.authorizer.AuthorizeRevocation(ctx, authorization)
	} else {
		err = service.authorizer.AuthorizeReenablement(ctx, authorization)
	}
	if err != nil {
		return DispatchGate{}, fmt.Errorf("%w: %w", ErrDispatchAuthorizationDenied, err)
	}

	tx, cleanup, err := sqltx.BeginPessimisticTx(ctx, service.repository.db)
	if err != nil {
		return DispatchGate{}, fmt.Errorf("begin dispatch gate transition: %w", err)
	}
	rollback := func(cause error) (DispatchGate, error) {
		return DispatchGate{}, errors.Join(cause, tx.Rollback(), cleanup())
	}
	commit := func() error { return errors.Join(tx.Commit(), cleanup()) }

	locked, found, err := loadDispatchGateExact(ctx, tx, request.CredentialGenerationID, true)
	if err != nil {
		return rollback(err)
	}
	if !found {
		return rollback(fmt.Errorf("%w: %s", ErrDispatchGateNotFound, request.CredentialGenerationID))
	}
	if err := requireDispatchTransitionSource(locked, sourceMode, request.ExpectedFenceToken); err != nil {
		return rollback(err)
	}
	if err := updateDispatchGate(
		ctx,
		tx,
		request.CredentialGenerationID,
		sourceMode,
		targetMode,
		request.ExpectedFenceToken,
	); err != nil {
		return rollback(err)
	}

	updated, found, err := loadDispatchGateExact(ctx, tx, request.CredentialGenerationID, true)
	if err != nil {
		return rollback(err)
	}
	if !found || updated.mode != targetMode || updated.fenceToken != request.ExpectedFenceToken+1 {
		return rollback(storedDispatchGateInvalid("transition readback disagrees with guarded update"))
	}
	if err := commit(); err != nil {
		return DispatchGate{}, fmt.Errorf("commit dispatch gate transition: %w", err)
	}
	return updated, nil
}

func requireDispatchTransitionSource(gate DispatchGate, sourceMode DispatchMode, expectedFence uint64) error {
	if gate.fenceToken != expectedFence {
		return fmt.Errorf("%w: stored=%d expected=%d", ErrDispatchFenceConflict, gate.fenceToken, expectedFence)
	}
	if gate.mode != sourceMode {
		return fmt.Errorf("%w: stored=%s required=%s", ErrDispatchModeConflict, gate.mode, sourceMode)
	}
	if gate.fenceToken == MaxDispatchFenceToken {
		return ErrDispatchFenceExhausted
	}
	return nil
}

type dispatchGateQueryer interface {
	QueryRowContext(context.Context, string, ...any) *sql.Row
}

func loadDispatchGateExact(
	ctx context.Context,
	queryer dispatchGateQueryer,
	generationID string,
	forUpdate bool,
) (DispatchGate, bool, error) {
	query := dispatchGateSelect
	if forUpdate {
		query += " FOR UPDATE"
	}
	var gate DispatchGate
	var fence int64
	err := queryer.QueryRowContext(ctx, query, generationID, generationID).Scan(
		&gate.credentialGenerationID,
		&gate.mode,
		&fence,
	)
	if errors.Is(err, sql.ErrNoRows) {
		return DispatchGate{}, false, nil
	}
	if err != nil {
		return DispatchGate{}, false, fmt.Errorf("load dispatch gate row: %w", err)
	}
	if fence < 0 {
		return DispatchGate{}, false, storedDispatchGateInvalid("fence is negative")
	}
	gate.fenceToken = uint64(fence)
	if err := gate.validateStored(); err != nil {
		return DispatchGate{}, false, err
	}
	return gate, true, nil
}

func insertInitialDispatchGate(ctx context.Context, tx *sql.Tx, generationID string) error {
	result, err := tx.ExecContext(ctx, `
INSERT INTO _System_ConnectorCredentialGenerationDispatchGate
       (CredentialGenerationId, Mode, FenceToken)
VALUES (?, ?, ?)`, generationID, string(DispatchModeEnabled), int64(0))
	if err != nil {
		return err
	}
	return requireOneDispatchGateRow(result, "insert initial dispatch gate")
}

func initializeDispatchGateIfAbsent(ctx context.Context, tx *sql.Tx, generationID string) error {
	if err := insertInitialDispatchGate(ctx, tx, generationID); err != nil && !isMySQLDuplicateKey(err) {
		return err
	}
	gate, found, err := loadDispatchGateExact(ctx, tx, generationID, true)
	if err != nil {
		return err
	}
	if !found {
		return fmt.Errorf("%w: %s", ErrDispatchGateNotFound, generationID)
	}
	return gate.validateStored()
}

func updateDispatchGate(
	ctx context.Context,
	tx *sql.Tx,
	generationID string,
	sourceMode, targetMode DispatchMode,
	expectedFence uint64,
) error {
	result, err := tx.ExecContext(ctx, `
UPDATE _System_ConnectorCredentialGenerationDispatchGate
   SET Mode = ?, FenceToken = FenceToken + 1, LastModifiedDate = CURRENT_TIMESTAMP(6)
 WHERE CredentialGenerationId = ? AND BINARY CredentialGenerationId = BINARY ?
   AND Mode = ? AND BINARY Mode = BINARY ? AND FenceToken = ?`,
		string(targetMode),
		generationID,
		generationID,
		string(sourceMode),
		string(sourceMode),
		int64(expectedFence),
	)
	if err != nil {
		return fmt.Errorf("update dispatch gate: %w", err)
	}
	return requireOneDispatchGateRow(result, "update dispatch gate")
}

func requireOneDispatchGateRow(result sql.Result, operation string) error {
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("%s rows affected: %w", operation, err)
	}
	if rows != 1 {
		return fmt.Errorf("%s rows affected = %d, want 1", operation, rows)
	}
	return nil
}
