package binding

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"time"

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

const (
	CommandOperationCreate           = "create"
	CommandOperationMetadataUpdate   = "metadata_update"
	CommandOperationRevisionCreate   = "revision_create"
	CommandOperationRevisionActivate = "revision_activate"
	CommandOperationDisable          = "disable"
	CommandOperationArchive          = "archive"
	CommandOperationRestore          = "restore"
)

type ReplayCommandParams struct {
	WorkspaceID    string
	Actor          Actor
	Operation      string
	IdempotencyKey string
	RequestDigest  string
}

// ReplayCommand loads an immutable command result without reading the mutable
// Binding aggregate, Plan, owner, or membership relationship first. It is the
// preflight half of Agent exact retry; the write methods retain their in-
// transaction receipt check as the concurrency fence after a preflight miss.
func (store *Store) ReplayCommand(ctx context.Context, params ReplayCommandParams) (ReplayCommandResult, bool, error) {
	if err := store.Validate(); err != nil {
		return ReplayCommandResult{}, false, err
	}
	params.WorkspaceID = trim(params.WorkspaceID)
	params.Operation = trim(params.Operation)
	params.IdempotencyKey = trim(params.IdempotencyKey)
	actor, err := validateActor(params.Actor)
	if err != nil {
		return ReplayCommandResult{}, false, err
	}
	digest, err := normalizeCommandRequestDigest(params.RequestDigest, "")
	if err != nil {
		return ReplayCommandResult{}, false, err
	}
	if params.WorkspaceID == "" || !validCommandOperation(params.Operation) || params.IdempotencyKey == "" {
		return ReplayCommandResult{}, false, storeerr.BadRequest("complete workflow binding command replay identity is required")
	}

	bindingID, persistedDigest, payload, found, err := loadBindingCommand(
		ctx, store.db, params.WorkspaceID, actor, params.Operation, params.IdempotencyKey, false,
	)
	if err != nil {
		return ReplayCommandResult{}, false, fmt.Errorf("load workflow binding command replay: %w", mapWriteError(err))
	}
	if !found {
		return ReplayCommandResult{}, false, nil
	}
	if persistedDigest != digest {
		return ReplayCommandResult{}, true, IdempotencyConflictError{Operation: params.Operation}
	}
	if len(payload) == 0 || !json.Valid(payload) {
		return ReplayCommandResult{}, true, fmt.Errorf("workflow binding command receipt is incomplete or invalid")
	}
	result, err := decodeReplayCommandResult(params.Operation, payload)
	if err != nil {
		return ReplayCommandResult{}, true, err
	}
	if result.Binding.ID != bindingID || result.Binding.WorkspaceID != params.WorkspaceID {
		return ReplayCommandResult{}, true, fmt.Errorf("workflow binding command receipt contains a mismatched result")
	}
	if result.Revision != nil && (result.Revision.BindingID != bindingID || result.Revision.WorkspaceID != params.WorkspaceID) {
		return ReplayCommandResult{}, true, fmt.Errorf("workflow binding command receipt contains a mismatched revision result")
	}
	return result, true, nil
}

func decodeReplayCommandResult(operation string, payload json.RawMessage) (ReplayCommandResult, error) {
	switch operation {
	case CommandOperationCreate:
		result, err := decodeBindingCommandResult[CreateResult](payload)
		if err != nil {
			return ReplayCommandResult{}, err
		}
		revision := result.Revision
		return ReplayCommandResult{Binding: result.Binding, Revision: &revision}, nil
	case CommandOperationRevisionCreate:
		result, err := decodeBindingCommandResult[CreateRevisionResult](payload)
		if err != nil {
			return ReplayCommandResult{}, err
		}
		revision := result.Revision
		return ReplayCommandResult{Binding: result.Binding, Revision: &revision}, nil
	case CommandOperationRevisionActivate:
		result, err := decodeBindingCommandResult[ActivateResult](payload)
		if err != nil {
			return ReplayCommandResult{}, err
		}
		revision := result.Revision
		return ReplayCommandResult{Binding: result.Binding, Revision: &revision}, nil
	case CommandOperationMetadataUpdate, CommandOperationDisable, CommandOperationArchive, CommandOperationRestore:
		binding, err := decodeBindingCommandResult[Binding](payload)
		return ReplayCommandResult{Binding: binding}, err
	default:
		return ReplayCommandResult{}, storeerr.BadRequest("unsupported workflow binding command receipt operation")
	}
}

// beginBindingCommand acquires the workspace/actor/operation/idempotency-key
// gate before any Binding aggregate row is locked. That global lock order is
// essential: a retry that changes bindingId must conflict instead of locking a
// second Binding and executing there, and concurrent calls using one key must
// not deadlock by first locking different Binding rows.
//
// The placeholder and final result are written in the same transaction as the
// business mutation. A crash rolls both back; a concurrent retry waits for the
// first transaction and then observes its durable result.
func beginBindingCommand(
	ctx context.Context,
	execer coredb.QueryExecer,
	workspaceID string,
	bindingID string,
	actor Actor,
	operation string,
	idempotencyKey string,
	requestDigest string,
	createdAt time.Time,
) (json.RawMessage, bool, error) {
	if !validCommandOperation(operation) {
		return nil, false, storeerr.BadRequest("unsupported workflow binding command receipt operation")
	}
	if _, err := execer.Exec(ctx, `
INSERT INTO workflow_binding_command_receipts (
    workspace_id, binding_id, actor_type, actor_id, operation_kind,
    idempotency_key, request_digest, result_json, created_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8)
ON DUPLICATE KEY UPDATE idempotency_key = idempotency_key
`, workspaceID, bindingID, actor.Type, actor.ID, operation,
		idempotencyKey, requestDigest, utcOrNow(createdAt)); err != nil {
		return nil, false, fmt.Errorf("reserve workflow binding command receipt: %w", mapWriteError(err))
	}

	persistedBindingID, persistedDigest, payload, found, err := loadBindingCommand(
		ctx, execer, workspaceID, actor, operation, idempotencyKey, true,
	)
	if err != nil {
		return nil, false, fmt.Errorf("load workflow binding command receipt: %w", mapWriteError(err))
	}
	if !found {
		return nil, false, fmt.Errorf("workflow binding command receipt reservation disappeared")
	}
	if persistedBindingID != bindingID || persistedDigest != requestDigest {
		return nil, false, IdempotencyConflictError{Operation: operation}
	}
	if len(payload) == 0 {
		return nil, false, nil
	}
	if !json.Valid(payload) {
		return nil, false, fmt.Errorf("workflow binding command receipt contains invalid result JSON")
	}
	return payload, true, nil
}

func loadBindingCommand(
	ctx context.Context,
	queryer coredb.QueryRower,
	workspaceID string,
	actor Actor,
	operation string,
	idempotencyKey string,
	forUpdate bool,
) (string, string, json.RawMessage, bool, error) {
	query := `
SELECT binding_id, request_digest, result_json
FROM workflow_binding_command_receipts
WHERE workspace_id = $1
  AND actor_type = $2
  AND actor_id = $3
  AND operation_kind = $4
  AND idempotency_key = $5
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	var bindingID string
	var requestDigest string
	var resultJSON sql.NullString
	err := queryer.QueryRow(ctx, query, workspaceID, actor.Type, actor.ID, operation, idempotencyKey).Scan(
		&bindingID, &requestDigest, &resultJSON,
	)
	if coredb.IsNotFound(err) {
		return "", "", nil, false, nil
	}
	if err != nil {
		return "", "", nil, false, err
	}
	if !resultJSON.Valid {
		return bindingID, requestDigest, nil, true, nil
	}
	return bindingID, requestDigest, json.RawMessage(resultJSON.String), true, nil
}

func completeBindingCommand(
	ctx context.Context,
	execer coredb.QueryExecer,
	workspaceID string,
	actor Actor,
	operation string,
	idempotencyKey string,
	result any,
) error {
	resultJSON, err := json.Marshal(result)
	if err != nil {
		return fmt.Errorf("encode workflow binding command receipt: %w", err)
	}
	updated, err := execer.Exec(ctx, `
UPDATE workflow_binding_command_receipts
SET result_json = $1
WHERE workspace_id = $2
  AND actor_type = $3
  AND actor_id = $4
  AND operation_kind = $5
  AND idempotency_key = $6
  AND result_json IS NULL
`, string(resultJSON), workspaceID, actor.Type, actor.ID, operation, idempotencyKey)
	if err != nil {
		return fmt.Errorf("complete workflow binding command receipt: %w", mapWriteError(err))
	}
	if updated.RowsAffected() != 1 {
		return storeerr.Conflict("workflow binding command receipt lost its completion fence")
	}
	return nil
}

func decodeBindingCommandResult[T any](payload json.RawMessage) (T, error) {
	var result T
	if err := json.Unmarshal(payload, &result); err != nil {
		return result, fmt.Errorf("decode workflow binding command receipt: %w", err)
	}
	return result, nil
}

func validCommandOperation(operation string) bool {
	switch operation {
	case CommandOperationCreate,
		CommandOperationMetadataUpdate,
		CommandOperationRevisionCreate,
		CommandOperationRevisionActivate,
		CommandOperationDisable,
		CommandOperationArchive,
		CommandOperationRestore:
		return true
	default:
		return false
	}
}

func lifecycleRequestDigest(operation string, bindingID string, expectedRevision int64, actor Actor) string {
	return requestDigest(
		operation, bindingID, fmt.Sprintf("%d", expectedRevision), actor.Type, actor.ID, actor.MemberID,
	)
}
