package binding

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type ActivateRevisionParams struct {
	WorkspaceID      string
	BindingID        string
	Revision         int64
	ExpectedRevision int64
	NextOccurrenceAt *time.Time
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

func (store *Store) ActivateRevision(ctx context.Context, params ActivateRevisionParams) (ActivateResult, error) {
	if err := store.Validate(); err != nil {
		return ActivateResult{}, err
	}
	normalized, err := normalizeActivateRevision(params)
	if err != nil {
		return ActivateResult{}, err
	}
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, activationRequestDigest(
		normalized.BindingID, normalized.Revision, normalized.ExpectedRevision, normalized.Actor,
	))
	if err != nil {
		return ActivateResult{}, err
	}
	result := ActivateResult{}
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		now := utcOrNow(normalized.Now)
		receipt, found, err := beginBindingCommand(
			ctx, tx, normalized.WorkspaceID, normalized.BindingID, normalized.Actor,
			CommandOperationRevisionActivate, normalized.IdempotencyKey, requestDigest, now,
		)
		if err != nil {
			return err
		}
		if found {
			result, err = decodeBindingCommandResult[ActivateResult](receipt)
			if err != nil {
				return err
			}
			if result.Binding.ID != normalized.BindingID || result.Binding.WorkspaceID != normalized.WorkspaceID ||
				result.Revision.BindingID != normalized.BindingID || result.Revision.WorkspaceID != normalized.WorkspaceID ||
				result.Source.BindingID != normalized.BindingID || result.Source.WorkspaceID != normalized.WorkspaceID {
				return fmt.Errorf("workflow binding revision-activate receipt contains a mismatched result")
			}
			result.Replayed = true
			return nil
		}
		binding, err := loadBinding(ctx, tx, normalized.WorkspaceID, normalized.BindingID, true)
		if err != nil {
			return err
		}
		target, err := loadRevisionByNumber(ctx, tx, normalized.WorkspaceID, normalized.BindingID, normalized.Revision, false)
		if err != nil {
			return err
		}
		if err := validateActivationCursor(target.SourceKind, true, normalized.NextOccurrenceAt); err != nil {
			return err
		}
		if err := requireBindingRevision(binding, normalized.ExpectedRevision); err != nil {
			return err
		}
		if binding.State == BindingStateArchived {
			return ErrArchived
		}
		var activation int64
		if err := tx.QueryRow(ctx, `
SELECT COALESCE(MAX(activation_number), 0) + 1
FROM workflow_binding_sources
WHERE workspace_id = $1 AND binding_id = $2
`, normalized.WorkspaceID, normalized.BindingID).Scan(&activation); err != nil {
			return fmt.Errorf("allocate workflow binding source activation: %w", err)
		}
		if binding.ActiveSourceID != "" {
			if err := closeActiveSource(ctx, tx, binding, "superseded", now); err != nil {
				return err
			}
		}
		sourceID, err := newID("wbs")
		if err != nil {
			return err
		}
		source := newActiveSource(
			sourceID, normalized.WorkspaceID, normalized.BindingID, target.ID, activation,
			target.SourceKind, normalized.NextOccurrenceAt,
			normalized.Actor, IdempotencyScopeRevisionActivate, normalized.IdempotencyKey,
			requestDigest, now,
		)
		if err := insertSource(ctx, tx, source); err != nil {
			return err
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET state = 'enabled',
    active_revision_id = $1,
    active_source_id = $2,
    revision = revision + 1,
    updated_by_member_id = $3,
    updated_by_actor_type = $4,
    updated_by_actor_id = $5,
    updated_at = $6
WHERE workspace_id = $7 AND id = $8 AND revision = $9
`, target.ID, source.ID,
			normalized.Actor.MemberID, normalized.Actor.Type, normalized.Actor.ID, now,
			normalized.WorkspaceID, normalized.BindingID, normalized.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("activate workflow binding revision: %w", mapWriteError(err))
		}
		if updated.RowsAffected() != 1 {
			return RevisionConflictError{Resource: "binding", Expected: normalized.ExpectedRevision, Actual: binding.Revision}
		}
		binding, err = loadBinding(ctx, tx, normalized.WorkspaceID, normalized.BindingID, false)
		if err != nil {
			return err
		}
		result = ActivateResult{Binding: binding, Revision: target, Source: source}
		return completeBindingCommand(
			ctx, tx, normalized.WorkspaceID, normalized.Actor,
			CommandOperationRevisionActivate, normalized.IdempotencyKey, result,
		)
	})
	return result, err
}

func (store *Store) LoadSource(ctx context.Context, workspaceID string, sourceID string) (Source, error) {
	if err := store.Validate(); err != nil {
		return Source{}, err
	}
	workspaceID, sourceID = trim(workspaceID), trim(sourceID)
	if workspaceID == "" || sourceID == "" {
		return Source{}, storeerr.BadRequest("workspaceId and sourceId are required")
	}
	return loadSource(ctx, store.db, workspaceID, sourceID, false)
}

// Schedule occurrence acceptance deliberately has no standalone claim or
// cursor-advance API. The Invocation boundary must fence workspace placement,
// reserve/replay the stable occurrence identity, create the immutable
// Invocation snapshot, and advance this cursor in one transaction. Exposing a
// partial lease/advance primitive here would create a crash window that could
// permanently lose an occurrence.

func normalizeActivateRevision(params ActivateRevisionParams) (ActivateRevisionParams, error) {
	params.WorkspaceID = trim(params.WorkspaceID)
	params.BindingID = trim(params.BindingID)
	params.IdempotencyKey = trim(params.IdempotencyKey)
	if params.WorkspaceID == "" || params.BindingID == "" || params.Revision <= 0 || params.ExpectedRevision <= 0 || params.IdempotencyKey == "" {
		return ActivateRevisionParams{}, storeerr.BadRequest("workspaceId, bindingId, revision, expectedRevision, and idempotencyKey are required")
	}
	actor, err := validateActor(params.Actor)
	if err != nil {
		return ActivateRevisionParams{}, err
	}
	params.Actor = actor
	if params.NextOccurrenceAt != nil {
		next := params.NextOccurrenceAt.UTC()
		params.NextOccurrenceAt = &next
	}
	return params, nil
}

func validateActivationCursor(sourceKind string, activate bool, nextOccurrenceAt *time.Time) error {
	if !activate {
		if nextOccurrenceAt != nil {
			return storeerr.BadRequest("nextOccurrenceAt is only accepted when activating")
		}
		return nil
	}
	if sourceKind == SourceKindSchedule && nextOccurrenceAt == nil {
		return storeerr.BadRequest("schedule source activation requires nextOccurrenceAt")
	}
	return nil
}

func activationRequestDigest(bindingID string, revision int64, expectedRevision int64, actor Actor) string {
	return requestDigest(
		"binding-activate", bindingID, fmt.Sprintf("%d", revision), fmt.Sprintf("%d", expectedRevision),
		actor.Type, actor.ID,
	)
}

func newActiveSource(
	id string,
	workspaceID string,
	bindingID string,
	revisionID string,
	activation int64,
	sourceKind string,
	nextOccurrenceAt *time.Time,
	actor Actor,
	idempotencyScope string,
	idempotencyKey string,
	requestDigest string,
	now time.Time,
) Source {
	return Source{
		ID:                         id,
		WorkspaceID:                workspaceID,
		BindingID:                  bindingID,
		BindingRevisionID:          revisionID,
		Activation:                 activation,
		SourceKind:                 sourceKind,
		Status:                     SourceStatusActive,
		Revision:                   1,
		NextOccurrenceAt:           nextOccurrenceAt,
		ActivationIdempotencyScope: idempotencyScope,
		ActivationIdempotencyKey:   idempotencyKey,
		ActivationRequestDigest:    requestDigest,
		ActivatedBy:                actor,
		ActivatedAt:                now,
		CreatedAt:                  now,
		UpdatedAt:                  now,
	}
}

func insertSource(ctx context.Context, execer coredb.QueryExecer, value Source) error {
	if _, err := execer.Exec(ctx, `
INSERT INTO workflow_binding_sources (
    id, workspace_id, binding_id, binding_revision_id, activation_number,
    source_kind, status, revision,
	    next_occurrence_at,
	    activation_idempotency_scope, activation_idempotency_key, activation_request_digest,
    activated_by_member_id, activated_by_actor_type, activated_by_actor_id, activated_at,
    closed_at, closed_reason, created_at, updated_at
) VALUES (
    $1, $2, $3, $4, $5,
    $6, 'active', 1,
	    $7,
	    $8, $9, $10,
	    $11, $12, $13, $14,
	    NULL, '', $14, $14
)
`, value.ID, value.WorkspaceID, value.BindingID, value.BindingRevisionID, value.Activation,
		value.SourceKind, value.NextOccurrenceAt,
		value.ActivationIdempotencyScope, value.ActivationIdempotencyKey, value.ActivationRequestDigest,
		value.ActivatedBy.MemberID, value.ActivatedBy.Type, value.ActivatedBy.ID, value.ActivatedAt); err != nil {
		return fmt.Errorf("insert workflow binding source: %w", mapWriteError(err))
	}
	return nil
}

func loadSourceByActivationIdempotency(ctx context.Context, queryer coredb.QueryRower, workspaceID string, bindingID string, actor Actor, scope string, idempotencyKey string, forUpdate bool) (Source, bool, error) {
	query := sourceSelectSQL() + `
WHERE workspace_id = $1
  AND activated_by_actor_type = $2
  AND activated_by_actor_id = $3
  AND activation_idempotency_scope = $4
  AND activation_idempotency_key = $5
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	value, err := scanSource(queryer.QueryRow(ctx, query, workspaceID, actor.Type, actor.ID, scope, idempotencyKey))
	if coredb.IsNotFound(err) {
		return Source{}, false, nil
	}
	if err != nil {
		return Source{}, false, mapWriteError(err)
	}
	if value.BindingID != bindingID {
		return Source{}, false, IdempotencyConflictError{Operation: "activation"}
	}
	return value, true, nil
}

func closeActiveSource(ctx context.Context, execer coredb.QueryExecer, binding Binding, reason string, now time.Time) error {
	result, err := execer.Exec(ctx, `
UPDATE workflow_binding_sources
SET status = 'disabled',
	    closed_at = $1,
    closed_reason = $2,
    revision = revision + 1,
    updated_at = $1
WHERE workspace_id = $3 AND binding_id = $4 AND id = $5 AND status = 'active'
`, now, reason, binding.WorkspaceID, binding.ID, binding.ActiveSourceID)
	if err != nil {
		return fmt.Errorf("close workflow binding source: %w", mapWriteError(err))
	}
	if result.RowsAffected() == 0 {
		// A completed one-shot is already durably closed; lifecycle and
		// supersession only need to move the aggregate pointer away from it.
		source, err := loadSource(ctx, execer, binding.WorkspaceID, binding.ActiveSourceID, false)
		if err != nil {
			return err
		}
		if source.Status != SourceStatusCompleted {
			return storeerr.Conflict("workflow binding active source is not active")
		}
	}
	return nil
}
