package catalogtenant

import (
	"context"

	tenantauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/tenantlayoutbundleauthorityv1"
)

// ApplyNamespaceCommand reserves or permanently tombstones one tenant
// namespace. Exact completed retries return the stored outcome before reading
// current namespace state.
func (a *Authority) ApplyNamespaceCommand(
	ctx context.Context,
	commandID tenantauthorityv1.CommandID,
	request tenantauthorityv1.TenantNamespaceCommandRequest,
) (NamespaceCommandOutcome, error) {
	if a == nil || a.db == nil {
		return NamespaceCommandOutcome{}, ErrConflict
	}
	if err := tenantauthorityv1.ValidateCommandID(commandID); err != nil {
		return NamespaceCommandOutcome{}, err
	}
	requestDigest, err := tenantauthorityv1.NamespaceCommandRequestDigest(request)
	if err != nil {
		return NamespaceCommandOutcome{}, err
	}
	desiredDigest, err := tenantauthorityv1.NamespaceValueDigest(
		request.DesiredNamespaceValue,
	)
	if err != nil {
		return NamespaceCommandOutcome{}, err
	}

	tx, priorSequence, nextSequence, existing, err := a.startCommand(ctx, commandID)
	if err != nil {
		return NamespaceCommandOutcome{}, err
	}
	defer rollbackTx(tx)
	if existing != nil {
		if err := validateNamespaceOutcome(request, *existing); err != nil {
			return NamespaceCommandOutcome{}, err
		}
		if err := tx.Commit(); err != nil {
			return NamespaceCommandOutcome{}, err
		}
		return namespaceOutcome(*existing), nil
	}

	current, present, err := lockNamespaceCurrent(
		ctx,
		tx,
		request.TargetTenantNamespaceID,
	)
	if err != nil {
		return NamespaceCommandOutcome{}, err
	}
	observed := referenceForNamespace(current, present)
	hasActiveBinding := false
	hasActiveSlot := false
	if request.Operation == tenantauthorityv1.NamespaceOperationTombstone &&
		observed == namespaceExpectedReference(request.ExpectedPriorNamespace) {
		hasActiveBinding, err = lockAnyActiveBinding(
			ctx,
			tx,
			request.TargetTenantNamespaceID,
		)
		if err != nil {
			return NamespaceCommandOutcome{}, err
		}
		if !hasActiveBinding {
			hasActiveSlot, err = lockAnyActiveSlot(
				ctx,
				tx,
				request.TargetTenantNamespaceID,
			)
			if err != nil {
				return NamespaceCommandOutcome{}, err
			}
		}
	}

	result := decideNamespace(request, observed, hasActiveBinding, hasActiveSlot)
	resulting := observed
	if result == ResultApplied {
		resulting = ValueReference{Present: true, Digest: desiredDigest}
	}
	command, err := completeTerminalCommand(terminalCommand{
		CommandID:      commandID,
		Kind:           commandKindNamespace,
		RequestDigest:  requestDigest,
		Result:         result,
		ObservedPrior:  observed,
		ResultingValue: resulting,
		Sequence:       nextSequence,
	})
	if err != nil {
		return NamespaceCommandOutcome{}, err
	}
	if err := validateNamespaceOutcome(request, command); err != nil {
		return NamespaceCommandOutcome{}, err
	}
	if result == ResultApplied {
		if err := persistNamespaceValue(
			ctx,
			tx,
			command,
			request,
			desiredDigest,
			current,
			present,
		); err != nil {
			return NamespaceCommandOutcome{}, err
		}
	}
	if err := finishCommand(ctx, tx, priorSequence, command); err != nil {
		return NamespaceCommandOutcome{}, err
	}
	return namespaceOutcome(command), nil
}

// ApplyOrgBindingCommand binds one exact Catalog OrgID to a reserved tenant
// namespace or permanently tombstones that binding.
func (a *Authority) ApplyOrgBindingCommand(
	ctx context.Context,
	commandID tenantauthorityv1.CommandID,
	request tenantauthorityv1.TenantOrgNamespaceBindingCommandRequest,
) (OrgBindingCommandOutcome, error) {
	if a == nil || a.db == nil {
		return OrgBindingCommandOutcome{}, ErrConflict
	}
	if err := tenantauthorityv1.ValidateCommandID(commandID); err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	requestDigest, err := tenantauthorityv1.OrgBindingCommandRequestDigest(request)
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	desiredDigest, err := tenantauthorityv1.OrgBindingValueDigest(
		request.DesiredBindingValue,
	)
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}

	tx, priorSequence, nextSequence, existing, err := a.startCommand(ctx, commandID)
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	defer rollbackTx(tx)
	if existing != nil {
		if err := validateBindingOutcome(request, *existing); err != nil {
			return OrgBindingCommandOutcome{}, err
		}
		if err := tx.Commit(); err != nil {
			return OrgBindingCommandOutcome{}, err
		}
		return bindingOutcome(*existing), nil
	}

	namespace, namespacePresent, err := lockNamespaceCurrent(
		ctx,
		tx,
		request.DesiredBindingValue.TenantNamespaceID,
	)
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	current, present, err := lockBindingCurrent(ctx, tx, request.TargetOrgID)
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	observedNamespace := referenceForNamespace(namespace, namespacePresent)
	observed := referenceForBinding(current, present)
	blocksSlotRetirement := false
	if request.Operation == tenantauthorityv1.BindingOperationTombstone &&
		activeNamespaceDependencyMatches(
			namespace,
			namespacePresent,
			request.DesiredBindingValue.TenantNamespaceID,
			request.ExpectedNamespaceReservationDigest,
		) &&
		bindingExpectedPriorMatches(request, current, present) {
		// Slots belong to the namespace rather than one OrgID. A non-last
		// binding may retire while another ACTIVE binding retains authority to
		// retire slots, but the last binding must remain until the slots do.
		hasOtherActiveBinding, err := lockAnyOtherActiveBinding(
			ctx,
			tx,
			current.Value.TenantNamespaceID,
			request.TargetOrgID,
		)
		if err != nil {
			return OrgBindingCommandOutcome{}, err
		}
		hasActiveSlot, err := lockAnyActiveSlot(
			ctx,
			tx,
			current.Value.TenantNamespaceID,
		)
		if err != nil {
			return OrgBindingCommandOutcome{}, err
		}
		blocksSlotRetirement = hasActiveSlot && !hasOtherActiveBinding
	}
	result := decideBinding(
		request,
		namespace,
		namespacePresent,
		current,
		present,
		blocksSlotRetirement,
	)
	resulting := observed
	if result == ResultApplied {
		resulting = ValueReference{Present: true, Digest: desiredDigest}
	}
	command, err := completeTerminalCommand(terminalCommand{
		CommandID:         commandID,
		Kind:              commandKindBinding,
		RequestDigest:     requestDigest,
		Result:            result,
		ObservedPrior:     observed,
		ResultingValue:    resulting,
		ObservedNamespace: observedNamespace,
		Sequence:          nextSequence,
	})
	if err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	if err := validateBindingOutcome(request, command); err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	if result == ResultApplied {
		if err := persistBindingValue(
			ctx,
			tx,
			command,
			request,
			desiredDigest,
			current,
			present,
		); err != nil {
			return OrgBindingCommandOutcome{}, err
		}
	}
	if err := finishCommand(ctx, tx, priorSequence, command); err != nil {
		return OrgBindingCommandOutcome{}, err
	}
	return bindingOutcome(command), nil
}

// ApplySlotCommand creates or permanently retires one complete tenant-owned
// BundleKey slot under exact ACTIVE namespace and OrgID binding dependencies.
func (a *Authority) ApplySlotCommand(
	ctx context.Context,
	commandID tenantauthorityv1.CommandID,
	request tenantauthorityv1.TenantLayoutBundleSlotCommandRequest,
) (SlotCommandOutcome, error) {
	if a == nil || a.db == nil {
		return SlotCommandOutcome{}, ErrConflict
	}
	if err := tenantauthorityv1.ValidateCommandID(commandID); err != nil {
		return SlotCommandOutcome{}, err
	}
	requestDigest, err := tenantauthorityv1.SlotCommandRequestDigest(request)
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	desiredDigest, err := tenantauthorityv1.SlotValueDigest(request.DesiredSlotValue)
	if err != nil {
		return SlotCommandOutcome{}, err
	}

	tx, priorSequence, nextSequence, existing, err := a.startCommand(ctx, commandID)
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	defer rollbackTx(tx)
	if existing != nil {
		if err := validateSlotOutcome(request, *existing); err != nil {
			return SlotCommandOutcome{}, err
		}
		if err := tx.Commit(); err != nil {
			return SlotCommandOutcome{}, err
		}
		return slotOutcome(*existing), nil
	}

	namespace, namespacePresent, err := lockNamespaceCurrent(
		ctx,
		tx,
		request.TargetBundleKey.SlotOwnerNamespace,
	)
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	binding, bindingPresent, err := lockBindingCurrent(
		ctx,
		tx,
		request.AuthorizingOrgID,
	)
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	current, present, err := lockSlotCurrent(ctx, tx, request.TargetBundleKey)
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	observedNamespace := referenceForNamespace(namespace, namespacePresent)
	observedBinding := referenceForBinding(binding, bindingPresent)
	observed := referenceForSlot(current, present)
	result := decideSlot(
		request,
		namespace,
		namespacePresent,
		binding,
		bindingPresent,
		observed,
	)
	resulting := observed
	if result == ResultApplied {
		resulting = ValueReference{Present: true, Digest: desiredDigest}
	}
	command, err := completeTerminalCommand(terminalCommand{
		CommandID:         commandID,
		Kind:              commandKindSlot,
		RequestDigest:     requestDigest,
		Result:            result,
		ObservedPrior:     observed,
		ResultingValue:    resulting,
		ObservedNamespace: observedNamespace,
		ObservedBinding:   observedBinding,
		Sequence:          nextSequence,
	})
	if err != nil {
		return SlotCommandOutcome{}, err
	}
	if err := validateSlotOutcome(request, command); err != nil {
		return SlotCommandOutcome{}, err
	}
	if result == ResultApplied {
		if err := persistSlotValue(
			ctx,
			tx,
			command,
			request,
			desiredDigest,
			current,
			present,
		); err != nil {
			return SlotCommandOutcome{}, err
		}
	}
	if err := finishCommand(ctx, tx, priorSequence, command); err != nil {
		return SlotCommandOutcome{}, err
	}
	return slotOutcome(command), nil
}
