package binding

import (
	"context"
	"fmt"
	"time"

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

type CreateBindingParams struct {
	ID               string
	WorkspaceID      string
	PlanID           string
	Key              string
	DisplayName      string
	DescriptionMD    string
	PlanVersionID    string
	SourceKind       string
	ConfigJSON       []byte
	Digest           string
	Activate         bool
	NextOccurrenceAt *time.Time
	Owner            Owner
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

type ListBindingsParams struct {
	WorkspaceID     string
	PlanID          string
	State           string
	IncludeArchived bool
	Limit           int
	BeforeUpdatedAt *time.Time
	BeforeID        string
}

type UpdateBindingMetadataParams struct {
	WorkspaceID      string
	BindingID        string
	ExpectedRevision int64
	DisplayName      *string
	DescriptionMD    *string
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

func (store *Store) CreateBinding(ctx context.Context, params CreateBindingParams) (CreateResult, error) {
	if err := store.Validate(); err != nil {
		return CreateResult{}, err
	}
	normalized, createDigest, revisionDigest, err := normalizeCreateBinding(params)
	if err != nil {
		return CreateResult{}, err
	}
	result := CreateResult{}
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		existingID, existingDigest, found, err := loadBindingCreateReceipt(ctx, tx, normalized.WorkspaceID, normalized.Actor, normalized.IdempotencyKey, true)
		if err != nil {
			return err
		}
		if found {
			if existingDigest != createDigest {
				return IdempotencyConflictError{Operation: "create"}
			}
			_, receiptDigest, receipt, receiptFound, err := loadBindingCommand(
				ctx, tx, normalized.WorkspaceID, normalized.Actor, CommandOperationCreate, normalized.IdempotencyKey, true,
			)
			if err != nil {
				return err
			}
			if !receiptFound || receiptDigest != createDigest || len(receipt) == 0 {
				return fmt.Errorf("workflow binding create receipt is incomplete")
			}
			result, err = decodeBindingCommandResult[CreateResult](receipt)
			if err != nil {
				return err
			}
			if result.Binding.ID != existingID || result.Binding.WorkspaceID != normalized.WorkspaceID ||
				result.Revision.BindingID != existingID || result.Revision.WorkspaceID != normalized.WorkspaceID {
				return fmt.Errorf("workflow binding create receipt contains a mismatched result")
			}
			result.Replayed = true
			return nil
		}
		if err := requireActivePlanVersion(ctx, tx, normalized.WorkspaceID, normalized.PlanID, normalized.PlanVersionID); err != nil {
			return err
		}

		bindingID := normalized.ID
		if bindingID == "" {
			bindingID, err = newID("wbd")
			if err != nil {
				return err
			}
		}
		revisionID, err := newID("wbr")
		if err != nil {
			return err
		}
		var sourceID string
		if normalized.Activate {
			sourceID, err = newID("wbs")
			if err != nil {
				return err
			}
		}
		now := utcOrNow(normalized.Now)
		if _, err := tx.Exec(ctx, `
INSERT INTO workflow_bindings (
    id, workspace_id, plan_id, binding_key, display_name, description_md,
    state, revision, latest_revision_id, active_revision_id, active_source_id,
    owner_user_id, owner_member_id,
    create_idempotency_key, create_request_digest,
    created_by_member_id, created_by_actor_type, created_by_actor_id,
    updated_by_member_id, updated_by_actor_type, updated_by_actor_id,
    archived_by_member_id, archived_by_actor_type, archived_by_actor_id, archived_at,
    created_at, updated_at
) VALUES (
    $1, $2, $3, $4, $5, $6,
    'disabled', 1, NULL, NULL, NULL,
    $7, $8,
    $9, $10,
    $11, $12, $13,
    $11, $12, $13,
    NULL, NULL, NULL, NULL,
    $14, $14
)
ON DUPLICATE KEY UPDATE id = id
`, bindingID, normalized.WorkspaceID, normalized.PlanID, normalized.Key, normalized.DisplayName, normalized.DescriptionMD,
			normalized.Owner.UserID, normalized.Owner.MemberID,
			normalized.IdempotencyKey, createDigest,
			normalized.Actor.MemberID, normalized.Actor.Type, normalized.Actor.ID, now); err != nil {
			return fmt.Errorf("insert workflow binding: %w", mapWriteError(err))
		}

		createdID, persistedDigest, found, err := loadBindingCreateReceipt(ctx, tx, normalized.WorkspaceID, normalized.Actor, normalized.IdempotencyKey, true)
		if err != nil {
			return err
		}
		if !found {
			return storeerr.Conflict("workflow binding key or id already exists")
		}
		if persistedDigest != createDigest {
			return IdempotencyConflictError{Operation: "create"}
		}
		if createdID != bindingID {
			_, receiptDigest, receipt, receiptFound, err := loadBindingCommand(
				ctx, tx, normalized.WorkspaceID, normalized.Actor, CommandOperationCreate, normalized.IdempotencyKey, true,
			)
			if err != nil {
				return err
			}
			if !receiptFound || receiptDigest != createDigest || len(receipt) == 0 {
				return fmt.Errorf("workflow binding create receipt is incomplete")
			}
			result, err = decodeBindingCommandResult[CreateResult](receipt)
			result.Replayed = true
			return err
		}

		if err := insertRevision(ctx, tx, Revision{
			ID:                     revisionID,
			WorkspaceID:            normalized.WorkspaceID,
			BindingID:              bindingID,
			PlanID:                 normalized.PlanID,
			Revision:               1,
			PlanVersionID:          normalized.PlanVersionID,
			SourceKind:             normalized.SourceKind,
			ConfigJSON:             normalized.ConfigJSON,
			Digest:                 normalized.Digest,
			CreateIdempotencyScope: IdempotencyScopeBindingCreate,
			CreateIdempotencyKey:   normalized.IdempotencyKey,
			CreateRequestDigest:    revisionDigest,
			CreatedBy:              normalized.Actor,
			CreatedAt:              now,
		}); err != nil {
			return err
		}

		state := BindingStateDisabled
		var activeRevisionID any
		var activeSourceID any
		if normalized.Activate {
			state = BindingStateEnabled
			activeRevisionID = revisionID
			activeSourceID = sourceID
			source := newActiveSource(
				sourceID, normalized.WorkspaceID, bindingID, revisionID, 1,
				normalized.SourceKind, normalized.NextOccurrenceAt,
				normalized.Actor, IdempotencyScopeBindingCreate, normalized.IdempotencyKey,
				initialActivationRequestDigest(createDigest), now,
			)
			if err := insertSource(ctx, tx, source); err != nil {
				return err
			}
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET state = $1,
    latest_revision_id = $2,
    active_revision_id = $3,
    active_source_id = $4
WHERE workspace_id = $5 AND id = $6 AND revision = 1 AND latest_revision_id IS NULL
`, state, revisionID, activeRevisionID, activeSourceID, normalized.WorkspaceID, bindingID)
		if err != nil {
			return fmt.Errorf("finalize workflow binding creation: %w", mapWriteError(err))
		}
		if updated.RowsAffected() != 1 {
			return storeerr.Conflict("workflow binding creation lost its aggregate fence")
		}
		result, err = loadCreateResult(ctx, tx, normalized.WorkspaceID, bindingID, false)
		if err != nil {
			return err
		}
		if _, found, err := beginBindingCommand(
			ctx, tx, normalized.WorkspaceID, bindingID, normalized.Actor,
			CommandOperationCreate, normalized.IdempotencyKey, createDigest, now,
		); err != nil {
			return err
		} else if found {
			return fmt.Errorf("new workflow binding create unexpectedly found a completed command receipt")
		}
		return completeBindingCommand(
			ctx, tx, normalized.WorkspaceID, normalized.Actor,
			CommandOperationCreate, normalized.IdempotencyKey, result,
		)
	})
	return result, err
}

func (store *Store) LoadBinding(ctx context.Context, workspaceID string, bindingID string) (Binding, error) {
	if err := store.Validate(); err != nil {
		return Binding{}, err
	}
	workspaceID, bindingID = trim(workspaceID), trim(bindingID)
	if workspaceID == "" || bindingID == "" {
		return Binding{}, storeerr.BadRequest("workspaceId and bindingId are required")
	}
	return loadBinding(ctx, store.db, workspaceID, bindingID, false)
}

func (store *Store) ListBindings(ctx context.Context, params ListBindingsParams) ([]Binding, error) {
	if err := store.Validate(); err != nil {
		return nil, err
	}
	params.WorkspaceID = trim(params.WorkspaceID)
	params.PlanID = trim(params.PlanID)
	params.State = trim(params.State)
	params.BeforeID = trim(params.BeforeID)
	if params.WorkspaceID == "" {
		return nil, storeerr.BadRequest("workspaceId is required")
	}
	if params.State != "" && params.State != BindingStateDisabled && params.State != BindingStateEnabled && params.State != BindingStateArchived {
		return nil, storeerr.BadRequest("state must be disabled, enabled, or archived")
	}
	if params.BeforeUpdatedAt != nil && params.BeforeID == "" {
		return nil, storeerr.BadRequest("beforeId is required with beforeUpdatedAt")
	}
	query := bindingListSelectSQL() + "\nWHERE b.workspace_id = $1\n"
	args := []any{params.WorkspaceID}
	if params.PlanID != "" {
		args = append(args, params.PlanID)
		query += fmt.Sprintf("  AND b.plan_id = $%d\n", len(args))
	}
	if params.State != "" {
		args = append(args, params.State)
		query += fmt.Sprintf("  AND b.state = $%d\n", len(args))
	} else if !params.IncludeArchived {
		query += "  AND b.state <> 'archived'\n"
	}
	if params.BeforeUpdatedAt != nil {
		args = append(args, params.BeforeUpdatedAt.UTC(), params.BeforeID)
		query += fmt.Sprintf("  AND (b.updated_at < $%d OR (b.updated_at = $%d AND b.id < $%d))\n", len(args)-1, len(args)-1, len(args))
	}
	args = append(args, normalizeLimit(params.Limit))
	query += fmt.Sprintf("ORDER BY b.updated_at DESC, b.id DESC\nLIMIT $%d\n", len(args))
	rows, err := store.db.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("list workflow bindings: %w", mapWriteError(err))
	}
	defer rows.Close()
	values := make([]Binding, 0)
	for rows.Next() {
		value, err := scanListedBinding(rows)
		if err != nil {
			return nil, fmt.Errorf("scan workflow binding: %w", err)
		}
		values = append(values, value)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate workflow bindings: %w", err)
	}
	return values, nil
}

func (store *Store) UpdateBindingMetadata(ctx context.Context, params UpdateBindingMetadataParams) (Binding, error) {
	if err := store.Validate(); err != nil {
		return Binding{}, err
	}
	params.WorkspaceID = trim(params.WorkspaceID)
	params.BindingID = trim(params.BindingID)
	params.IdempotencyKey = trim(params.IdempotencyKey)
	if params.WorkspaceID == "" || params.BindingID == "" || params.ExpectedRevision <= 0 || params.IdempotencyKey == "" {
		return Binding{}, storeerr.BadRequest("workspaceId, bindingId, a positive expectedRevision, and idempotencyKey are required")
	}
	if params.DisplayName == nil && params.DescriptionMD == nil {
		return Binding{}, storeerr.BadRequest("displayName or descriptionMd is required")
	}
	actor, err := validateActor(params.Actor)
	if err != nil {
		return Binding{}, err
	}
	displaySet, displayName := params.DisplayName != nil, ""
	if displaySet {
		displayName = trim(*params.DisplayName)
		if displayName == "" {
			return Binding{}, storeerr.BadRequest("displayName is required")
		}
	}
	descriptionSet, description := params.DescriptionMD != nil, ""
	if descriptionSet {
		description = trim(*params.DescriptionMD)
	}
	now := utcOrNow(params.Now)
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, metadataUpdateRequestDigest(
		params.BindingID, params.ExpectedRevision, displaySet, displayName, descriptionSet, description, actor,
	))
	if err != nil {
		return Binding{}, err
	}
	var updated Binding
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		receipt, found, err := beginBindingCommand(
			ctx, tx, params.WorkspaceID, params.BindingID, actor,
			CommandOperationMetadataUpdate, params.IdempotencyKey, requestDigest, now,
		)
		if err != nil {
			return err
		}
		if found {
			updated, err = decodeBindingCommandResult[Binding](receipt)
			if err != nil {
				return err
			}
			if updated.ID != params.BindingID || updated.WorkspaceID != params.WorkspaceID {
				return fmt.Errorf("workflow binding metadata receipt contains a mismatched result")
			}
			return nil
		}
		current, err := loadBinding(ctx, tx, params.WorkspaceID, params.BindingID, true)
		if err != nil {
			return err
		}
		if err := requireBindingRevision(current, params.ExpectedRevision); err != nil {
			return err
		}
		if current.State == BindingStateArchived {
			return ErrArchived
		}
		result, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET display_name = CASE WHEN $1 THEN $2 ELSE display_name END,
    description_md = CASE WHEN $3 THEN $4 ELSE description_md END,
    revision = revision + 1,
    updated_by_member_id = $5,
    updated_by_actor_type = $6,
    updated_by_actor_id = $7,
    updated_at = $8
WHERE workspace_id = $9 AND id = $10 AND revision = $11
`, displaySet, displayName, descriptionSet, description,
			actor.MemberID, actor.Type, actor.ID, now,
			params.WorkspaceID, params.BindingID, params.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("update workflow binding metadata: %w", mapWriteError(err))
		}
		if result.RowsAffected() != 1 {
			return RevisionConflictError{Resource: "binding", Expected: params.ExpectedRevision, Actual: current.Revision}
		}
		updated, err = loadBinding(ctx, tx, params.WorkspaceID, params.BindingID, false)
		if err != nil {
			return err
		}
		return completeBindingCommand(ctx, tx, params.WorkspaceID, actor, CommandOperationMetadataUpdate, params.IdempotencyKey, updated)
	})
	return updated, err
}

func metadataUpdateRequestDigest(bindingID string, expectedRevision int64, displaySet bool, displayName string, descriptionSet bool, description string, actor Actor) string {
	return requestDigest(
		CommandOperationMetadataUpdate, bindingID,
		fmt.Sprintf("%d", expectedRevision),
		fmt.Sprintf("%t", displaySet), displayName,
		fmt.Sprintf("%t", descriptionSet), description,
		actor.Type, actor.ID, actor.MemberID,
	)
}

func normalizeCreateBinding(params CreateBindingParams) (CreateBindingParams, string, string, error) {
	params.ID = trim(params.ID)
	params.WorkspaceID = trim(params.WorkspaceID)
	params.PlanID = trim(params.PlanID)
	params.Key = trim(params.Key)
	params.DisplayName = trim(params.DisplayName)
	params.DescriptionMD = trim(params.DescriptionMD)
	params.PlanVersionID = trim(params.PlanVersionID)
	params.IdempotencyKey = trim(params.IdempotencyKey)
	if params.WorkspaceID == "" || params.PlanID == "" || params.Key == "" || params.DisplayName == "" || params.PlanVersionID == "" || params.IdempotencyKey == "" {
		return CreateBindingParams{}, "", "", storeerr.BadRequest("workspaceId, planId, key, displayName, planVersionId, and idempotencyKey are required")
	}
	actor, err := validateActor(params.Actor)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	owner, err := validateOwner(params.Owner)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	sourceKind, err := validateSourceKind(params.SourceKind)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	configJSON, err := normalizeConfigJSON(params.ConfigJSON)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	if err := requireConfigSourceKind(configJSON, sourceKind); err != nil {
		return CreateBindingParams{}, "", "", err
	}
	digest, err := validateDigest(params.Digest)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	if params.NextOccurrenceAt != nil {
		next := params.NextOccurrenceAt.UTC()
		params.NextOccurrenceAt = &next
	}
	if err := validateActivationCursor(sourceKind, params.Activate, params.NextOccurrenceAt); err != nil {
		return CreateBindingParams{}, "", "", err
	}
	params.Actor = actor
	params.Owner = owner
	params.SourceKind = sourceKind
	params.ConfigJSON = configJSON
	params.Digest = digest
	defaultCreateDigest := requestDigest(
		"binding-create", params.WorkspaceID, params.PlanID, params.Key, params.DisplayName, params.DescriptionMD,
		params.PlanVersionID, params.SourceKind, string(params.ConfigJSON), params.Digest,
		params.Owner.UserID, params.Owner.MemberID, params.Actor.Type, params.Actor.ID,
		fmt.Sprintf("%t", params.Activate),
	)
	createDigest, err := normalizeCommandRequestDigest(params.RequestDigest, defaultCreateDigest)
	if err != nil {
		return CreateBindingParams{}, "", "", err
	}
	revisionDigest := initialRevisionRequestDigest(createDigest)
	return params, createDigest, revisionDigest, nil
}

func requireBindingRevision(binding Binding, expected int64) error {
	if binding.Revision != expected {
		return RevisionConflictError{Resource: "binding", Expected: expected, Actual: binding.Revision}
	}
	return nil
}

func loadBindingCreateReceipt(ctx context.Context, queryer coredb.QueryRower, workspaceID string, actor Actor, idempotencyKey string, forUpdate bool) (string, string, bool, error) {
	query := `
SELECT id, create_request_digest
FROM workflow_bindings
WHERE workspace_id = $1
  AND created_by_actor_type = $2
  AND created_by_actor_id = $3
  AND create_idempotency_key = $4
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	var bindingID string
	var digest string
	err := queryer.QueryRow(ctx, query, workspaceID, actor.Type, actor.ID, idempotencyKey).Scan(&bindingID, &digest)
	if coredb.IsNotFound(err) {
		return "", "", false, nil
	}
	if err != nil {
		return "", "", false, mapWriteError(err)
	}
	return bindingID, digest, true, nil
}

func loadCreateResult(ctx context.Context, queryer coredb.QueryRower, workspaceID string, bindingID string, replayed bool) (CreateResult, error) {
	// A replay can arrive after its receipt read waited for the winning create
	// transaction. Under repeatable-read every row reconstructed from that
	// winner must therefore use a current (locking) read; otherwise the replay
	// may retain its pre-wait snapshot and miss the winner's child rows.
	binding, err := loadBinding(ctx, queryer, workspaceID, bindingID, replayed)
	if err != nil {
		return CreateResult{}, err
	}
	revision, found, err := loadRevisionByCreateIdempotency(
		ctx, queryer, workspaceID, bindingID, binding.CreatedBy,
		IdempotencyScopeBindingCreate, binding.CreateIdempotencyKey, replayed,
	)
	if err != nil {
		return CreateResult{}, err
	}
	if !found {
		return CreateResult{}, fmt.Errorf("workflow binding %s has no create revision receipt", bindingID)
	}
	var source *Source
	value, found, err := loadSourceByActivationIdempotency(
		ctx, queryer, workspaceID, bindingID, binding.CreatedBy,
		IdempotencyScopeBindingCreate, binding.CreateIdempotencyKey, replayed,
	)
	if err != nil {
		return CreateResult{}, err
	}
	if found {
		source = &value
	}
	return CreateResult{Binding: binding, Revision: revision, Source: source, Replayed: replayed}, nil
}

func initialRevisionRequestDigest(createRequestDigest string) string {
	return requestDigest("binding-create-initial-revision", createRequestDigest)
}

func initialActivationRequestDigest(createRequestDigest string) string {
	return requestDigest("binding-create-initial-activation", createRequestDigest)
}
