package catalogsystem

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

	systemauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/systemlayoutbundleauthorityv1"
)

var (
	errExpectedPriorConflict = errors.New(
		"catalog system slot authority: expected prior conflict",
	)
	errHistoryConflict = errors.New(
		"catalog system slot authority: history conflict",
	)
)

const transitionSavepoint = "catalog_system_slot_transition"

func beginTransition(ctx context.Context, tx *sql.Tx) error {
	_, err := tx.ExecContext(ctx, "SAVEPOINT "+transitionSavepoint)
	return err
}

func keepTransition(ctx context.Context, tx *sql.Tx) error {
	_, err := tx.ExecContext(ctx, "RELEASE SAVEPOINT "+transitionSavepoint)
	return err
}

func discardTransition(ctx context.Context, tx *sql.Tx) error {
	_, err := tx.ExecContext(ctx, "ROLLBACK TO SAVEPOINT "+transitionSavepoint)
	return err
}

func persistCreate(
	ctx context.Context,
	tx *sql.Tx,
	command durableCommand,
	desiredDigest string,
) error {
	value := command.Request.DesiredSlotValue
	result, err := tx.ExecContext(ctx, `INSERT INTO _System_CatalogSystemLayoutBundleSlot
(BundleKeyVersion, SlotOwnerKind, SlotOwnerNamespace, LocalComponentKey,
ValueVersion, State, ValueDigest, LastCommandId, CreatedDate, LastModifiedDate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(6), CURRENT_TIMESTAMP(6))`,
		value.BundleKey.BundleKeyVersion,
		string(value.BundleKey.SlotOwnerKind),
		value.BundleKey.SlotOwnerNamespace,
		string(value.BundleKey.LocalComponentKey),
		value.ValueVersion,
		string(value.State),
		desiredDigest,
		string(command.CommandID),
	)
	if isDuplicate(err) {
		return errExpectedPriorConflict
	}
	if err != nil {
		return err
	}
	if err := requireExpectedPriorOne(result); err != nil {
		return err
	}
	return insertHistory(
		ctx,
		tx,
		command,
		1,
		desiredDigest,
		sql.NullString{},
	)
}

func persistRetire(
	ctx context.Context,
	tx *sql.Tx,
	command durableCommand,
	desiredDigest string,
	current currentSlot,
) error {
	value := command.Request.DesiredSlotValue
	result, err := tx.ExecContext(ctx, `UPDATE _System_CatalogSystemLayoutBundleSlot
SET ValueVersion = ?, State = ?, ValueDigest = ?, LastCommandId = ?,
LastModifiedDate = CURRENT_TIMESTAMP(6)
WHERE BundleKeyVersion = ? AND SlotOwnerKind = ?
AND SlotOwnerNamespace = ? AND LocalComponentKey = ?
AND State = 'ACTIVE' AND ValueDigest = ? AND LastCommandId = ?`,
		value.ValueVersion,
		string(value.State),
		desiredDigest,
		string(command.CommandID),
		value.BundleKey.BundleKeyVersion,
		string(value.BundleKey.SlotOwnerKind),
		value.BundleKey.SlotOwnerNamespace,
		string(value.BundleKey.LocalComponentKey),
		current.Digest,
		string(current.LastCommandID),
	)
	if err != nil {
		return err
	}
	if err := requireExpectedPriorOne(result); err != nil {
		return err
	}
	return insertHistory(
		ctx,
		tx,
		command,
		2,
		desiredDigest,
		sql.NullString{String: current.Digest, Valid: true},
	)
}

func insertHistory(
	ctx context.Context,
	tx *sql.Tx,
	command durableCommand,
	ordinal uint8,
	valueDigest string,
	priorDigest sql.NullString,
) error {
	value := command.Request.DesiredSlotValue
	result, err := tx.ExecContext(ctx, `INSERT INTO _System_CatalogSystemLayoutBundleSlotHistory
(BundleKeyVersion, SlotOwnerKind, SlotOwnerNamespace, LocalComponentKey,
TransitionOrdinal, CommandId, ValueVersion, State, ValueDigest,
PriorValueDigest, CreatedDate)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP(6))`,
		value.BundleKey.BundleKeyVersion,
		string(value.BundleKey.SlotOwnerKind),
		value.BundleKey.SlotOwnerNamespace,
		string(value.BundleKey.LocalComponentKey),
		ordinal,
		string(command.CommandID),
		value.ValueVersion,
		string(value.State),
		valueDigest,
		nullableSQLString(priorDigest),
	)
	if isDuplicate(err) {
		return errHistoryConflict
	}
	if err != nil {
		return err
	}
	return requireHistoryOne(result)
}

func requireExpectedPriorOne(result sql.Result) error {
	affected, err := result.RowsAffected()
	if err != nil {
		return err
	}
	if affected != 1 {
		return errExpectedPriorConflict
	}
	return nil
}

func requireHistoryOne(result sql.Result) error {
	affected, err := result.RowsAffected()
	if err != nil {
		return err
	}
	if affected != 1 {
		return errHistoryConflict
	}
	return nil
}

func nullableSQLString(value sql.NullString) any {
	if !value.Valid {
		return nil
	}
	return value.String
}

func desiredDigest(
	request systemauthorityv1.SystemLayoutBundleSlotCommandRequest,
) (string, error) {
	return systemauthorityv1.SlotValueDigest(request.DesiredSlotValue)
}
