package packs

import (
	"bytes"
	"context"
	"database/sql"
	"errors"
	"fmt"
	"reflect"
	"time"

	cp "github.com/pingcap-inc/nexuscrm-contract/pkg/controlplane"
)

var errManagedLifecycleTargetDrift = errors.New("managed lifecycle targets changed after serialization")

type managedLifecycleAttemptProvider struct {
	replan  func(context.Context, cp.ComponentManagedTransactionReadStore) (cp.ComponentManagedPlan, error)
	mutate  func(context.Context, cp.ComponentManagedPlan, cp.ComponentManagedTransactionMutationStore) (cp.ComponentManagedMutationResult, error)
	project func(context.Context, cp.ComponentManagedPlan, cp.ComponentManagedTransactionReadStore) (cp.ComponentManagedProjection, error)
}

func validateManagedLifecycleStagedProjection(
	ctx context.Context,
	host *ManagedLifecycleHost,
	provider func(context.Context, cp.ComponentManagedPlan, cp.ComponentManagedTransactionReadStore) (cp.ComponentManagedProjection, error),
	plan cp.ComponentManagedPlan,
	store *managedLifecycleMutationStore,
) (cp.ComponentManagedProjection, error) {
	projection, err := provider(ctx, plan, managedLifecycleReadView{delegate: store})
	if err != nil {
		return cp.ComponentManagedProjection{}, fmt.Errorf("project staged managed lifecycle %s: %w", host.lifecycle.Kind(), err)
	}
	if err := store.failureErr(); err != nil {
		return cp.ComponentManagedProjection{}, fmt.Errorf("staged managed lifecycle projection ignored a structured-store failure: %w", err)
	}
	if err := projection.Validate(); err != nil {
		return cp.ComponentManagedProjection{}, fmt.Errorf("staged managed lifecycle projection: %w", err)
	}
	if !bytes.Equal(projection.Expected, projection.Live) {
		return cp.ComponentManagedProjection{}, fmt.Errorf("managed lifecycle staged projection does not match expected bytes")
	}
	if !bytes.Equal(projection.Identity, plan.ProjectionIdentity()) {
		return cp.ComponentManagedProjection{}, fmt.Errorf("managed lifecycle staged projection identity changed")
	}
	return cloneManagedLifecycleProjection(projection), nil
}

type managedLifecycleAttemptRequest struct {
	tx              *sql.Tx
	catalog         managedLifecycleCatalog
	operationID     []byte
	attempt         int
	scope           cp.ComponentManagedScope
	desiredItems    int
	initialUsage    cp.ComponentManagedUsage
	ambientPlan     cp.ComponentManagedPlan
	preparationWait time.Duration
	started         time.Time
	provider        managedLifecycleAttemptProvider
}

type managedLifecycleAttemptResult struct {
	lockedPlan           cp.ComponentManagedPlan
	lockedPlanReady      bool
	mutation             cp.ComponentManagedMutationResult
	stagedProjection     cp.ComponentManagedProjection
	stagedProjectionDone bool
	receipt              cp.ComponentManagedMutationReceipt
	effectReceipts       []managedLifecycleEffectReceipt
	semanticMutationRows int
}

type managedLifecycleRetryableAttemptError struct {
	err error
}

func (e managedLifecycleRetryableAttemptError) Error() string {
	return e.err.Error()
}

func (e managedLifecycleRetryableAttemptError) Unwrap() error {
	return e.err
}

func managedLifecycleRetryableAttempt(err error) error {
	if err == nil {
		return nil
	}
	return managedLifecycleRetryableAttemptError{err: err}
}

func managedLifecycleAttemptCanRetry(err error) bool {
	var retryable managedLifecycleRetryableAttemptError
	return errors.As(err, &retryable) && managedLifecycleTransientLockError(err)
}

func (h *ManagedLifecycleHost) prepareManagedLifecycleAttemptTargets(
	ctx context.Context,
	targets []cp.ComponentManagedTarget,
	started time.Time,
) (time.Duration, error) {
	budget := h.lifecycle.Budget()
	prepareCtx, cancel := context.WithTimeout(ctx, budget.MaxLockWait)
	defer cancel()
	prepareStarted := time.Now()
	err := prepareManagedLifecycleTargets(
		prepareCtx,
		h.db,
		targets,
		budget.MaxElapsed-time.Since(started),
		budget.MaxAttempts,
	)
	wait := time.Since(prepareStarted)
	if err != nil {
		return wait, err
	}
	if wait >= budget.MaxLockWait {
		return wait, fmt.Errorf("managed lifecycle target preparation exhausted lock-wait budget %s", budget.MaxLockWait)
	}
	return wait, nil
}

func (h *ManagedLifecycleHost) runManagedLifecycleAttempt(
	ctx context.Context,
	request managedLifecycleAttemptRequest,
) (managedLifecycleAttemptResult, error) {
	var result managedLifecycleAttemptResult
	budget := h.lifecycle.Budget()
	remainingLockWait := budget.MaxLockWait - request.preparationWait
	if remainingLockWait <= 0 {
		return result, fmt.Errorf("managed lifecycle target preparation exhausted lock-wait budget %s", budget.MaxLockWait)
	}
	if request.catalog == nil {
		return result, fmt.Errorf("managed lifecycle attempt catalog is required")
	}
	lockWait, err := lockManagedLifecycleTargets(ctx, request.tx, request.ambientPlan.Targets(), remainingLockWait)
	if err != nil {
		return result, managedLifecycleRetryableAttempt(err)
	}
	store, err := newManagedLifecycleMutationStore(
		ctx,
		request.tx,
		request.catalog,
		h.registration.Contract.WriteSet,
		h.lifecycle.ReadSet(),
		budget,
		request.desiredItems,
		request.attempt,
	)
	if err != nil {
		return result, err
	}
	store.meter.usage = request.initialUsage
	store.meter.usage.DesiredItems = request.desiredItems
	store.meter.usage.Attempts = request.attempt
	store.meter.usage.LockWait += request.preparationWait + lockWait
	if err := validateManagedLifecycleUsage(budget, store.meter.usage); err != nil {
		return result, err
	}
	lockedPlan, err := request.provider.replan(ctx, managedLifecycleReadView{delegate: store})
	if err != nil {
		return result, managedLifecycleRetryableAttempt(fmt.Errorf("locked plan managed lifecycle %s: %w", h.lifecycle.Kind(), err))
	}
	result.lockedPlan = lockedPlan
	result.lockedPlanReady = true
	if err := store.failureErr(); err != nil {
		return result, fmt.Errorf("locked plan ignored a structured-store failure: %w", err)
	}
	if err := h.validateManagedLifecyclePlan(lockedPlan); err != nil {
		return result, fmt.Errorf("locked plan managed lifecycle %s: %w", h.lifecycle.Kind(), err)
	}
	if err := validateManagedLifecycleLockedEnvelope(request.ambientPlan, lockedPlan); err != nil {
		return result, err
	}
	if !reflect.DeepEqual(request.ambientPlan.Targets(), lockedPlan.Targets()) {
		return result, errManagedLifecycleTargetDrift
	}
	if err := validateManagedLifecycleUsage(budget, store.meter.usage); err != nil {
		return result, err
	}
	mutation, err := request.provider.mutate(ctx, lockedPlan, store)
	if err != nil {
		return result, managedLifecycleRetryableAttempt(fmt.Errorf("mutate managed lifecycle %s: %w", h.lifecycle.Kind(), err))
	}
	if err := store.failureErr(); err != nil {
		return result, fmt.Errorf("managed lifecycle mutation ignored a structured-store failure: %w", err)
	}
	if err := validateManagedLifecycleStagedMutation(budget, lockedPlan, mutation, store.ActualReceipt()); err != nil {
		return result, err
	}
	effectReceipts, effectClasses, err := h.applyEffects(
		ctx,
		request.tx,
		request.operationID,
		request.attempt,
		request.scope,
		mutation,
		lockedPlan,
	)
	if err != nil {
		return result, managedLifecycleRetryableAttempt(err)
	}
	if request.provider.project != nil {
		projection, err := validateManagedLifecycleStagedProjection(ctx, h, request.provider.project, lockedPlan, store)
		if err != nil {
			return result, err
		}
		result.stagedProjection = projection
		result.stagedProjectionDone = true
	}
	semanticMutationRows := store.meter.usage.RowsInserted + store.meter.usage.RowsUpdated + store.meter.usage.RowsDeleted
	if err := insertManagedLifecycleEvidence(
		ctx,
		request.tx,
		request.operationID,
		request.attempt,
		h.lifecycle.Kind(),
		request.scope,
		lockedPlan,
		mutation,
		effectReceipts,
		semanticMutationRows,
	); err != nil {
		return result, managedLifecycleRetryableAttempt(err)
	}
	receipt := store.ActualReceipt()
	receipt.Usage.EffectCalls = len(effectClasses)
	receipt.Usage.EvidenceWrites = 1
	receipt.Usage.Elapsed = time.Since(request.started)
	receipt.EffectClasses = append([]cp.ComponentManagedEffectClass(nil), effectClasses...)
	if err := h.lifecycle.ValidateMutationReceipt(lockedPlan, mutation, receipt); err != nil {
		return result, fmt.Errorf("validate managed lifecycle mutation receipt: %w", err)
	}
	result.mutation = mutation
	result.receipt = receipt
	result.effectReceipts = effectReceipts
	result.semanticMutationRows = semanticMutationRows
	return result, nil
}

func (h *ManagedLifecycleHost) validateManagedLifecyclePlan(plan cp.ComponentManagedPlan) error {
	if err := h.lifecycle.ValidatePlanCapabilities(plan); err != nil {
		return err
	}
	if err := h.validatePlanEffectAdapters(plan); err != nil {
		return err
	}
	return validateManagedLifecycleHostPlan(plan)
}
