package binding

import (
	"context"
	"fmt"
	"time"

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

type CreateRevisionParams struct {
	WorkspaceID      string
	BindingID        string
	ExpectedRevision int64
	PlanVersionID    string
	SourceKind       string
	ConfigJSON       []byte
	Digest           string
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

type ListRevisionsParams struct {
	WorkspaceID    string
	BindingID      string
	Limit          int
	BeforeRevision int64
}

func (store *Store) CreateRevision(ctx context.Context, params CreateRevisionParams) (CreateRevisionResult, error) {
	if err := store.Validate(); err != nil {
		return CreateRevisionResult{}, err
	}
	normalized, requestDigest, err := normalizeCreateRevision(params)
	if err != nil {
		return CreateRevisionResult{}, err
	}
	result := CreateRevisionResult{}
	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,
			CommandOperationRevisionCreate, normalized.IdempotencyKey, requestDigest, now,
		)
		if err != nil {
			return err
		}
		if found {
			result, err = decodeBindingCommandResult[CreateRevisionResult](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 {
				return fmt.Errorf("workflow binding revision-create 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
		}
		if err := requireBindingRevision(binding, normalized.ExpectedRevision); err != nil {
			return err
		}
		if binding.State == BindingStateArchived {
			return ErrArchived
		}
		if err := requireActivePlanVersion(ctx, tx, normalized.WorkspaceID, binding.PlanID, normalized.PlanVersionID); err != nil {
			return err
		}
		revisionID, err := newID("wbr")
		if err != nil {
			return err
		}
		revision := Revision{
			ID:                     revisionID,
			WorkspaceID:            normalized.WorkspaceID,
			BindingID:              normalized.BindingID,
			PlanID:                 binding.PlanID,
			Revision:               binding.LatestRevision.Revision + 1,
			PlanVersionID:          normalized.PlanVersionID,
			SourceKind:             normalized.SourceKind,
			ConfigJSON:             normalized.ConfigJSON,
			Digest:                 normalized.Digest,
			CreateIdempotencyScope: IdempotencyScopeRevisionCreate,
			CreateIdempotencyKey:   normalized.IdempotencyKey,
			CreateRequestDigest:    requestDigest,
			CreatedBy:              normalized.Actor,
			CreatedAt:              now,
		}
		if err := insertRevision(ctx, tx, revision); err != nil {
			return err
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET latest_revision_id = $1,
    revision = revision + 1,
    updated_by_member_id = $2,
    updated_by_actor_type = $3,
    updated_by_actor_id = $4,
    updated_at = $5
WHERE workspace_id = $6 AND id = $7 AND revision = $8
`, revisionID,
			normalized.Actor.MemberID, normalized.Actor.Type, normalized.Actor.ID, now,
			normalized.WorkspaceID, normalized.BindingID, normalized.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("advance workflow binding revision pointer: %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 = CreateRevisionResult{Binding: binding, Revision: revision}
		return completeBindingCommand(
			ctx, tx, normalized.WorkspaceID, normalized.Actor,
			CommandOperationRevisionCreate, normalized.IdempotencyKey, result,
		)
	})
	return result, err
}

func (store *Store) LoadRevision(ctx context.Context, workspaceID string, bindingID string, revision int64) (Revision, error) {
	if err := store.Validate(); err != nil {
		return Revision{}, err
	}
	workspaceID, bindingID = trim(workspaceID), trim(bindingID)
	if workspaceID == "" || bindingID == "" || revision <= 0 {
		return Revision{}, storeerr.BadRequest("workspaceId, bindingId, and a positive revision are required")
	}
	return loadRevisionByNumber(ctx, store.db, workspaceID, bindingID, revision, false)
}

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

func (store *Store) ListRevisions(ctx context.Context, params ListRevisionsParams) ([]Revision, error) {
	if err := store.Validate(); err != nil {
		return nil, err
	}
	params.WorkspaceID, params.BindingID = trim(params.WorkspaceID), trim(params.BindingID)
	if params.WorkspaceID == "" || params.BindingID == "" || params.BeforeRevision < 0 {
		return nil, storeerr.BadRequest("workspaceId and bindingId are required and beforeRevision cannot be negative")
	}
	query := revisionSelectSQL() + "\nWHERE workspace_id = $1 AND binding_id = $2\n"
	args := []any{params.WorkspaceID, params.BindingID}
	if params.BeforeRevision > 0 {
		args = append(args, params.BeforeRevision)
		query += fmt.Sprintf("  AND revision_number < $%d\n", len(args))
	}
	args = append(args, normalizeLimit(params.Limit))
	query += fmt.Sprintf("ORDER BY revision_number DESC\nLIMIT $%d\n", len(args))
	rows, err := store.db.Query(ctx, query, args...)
	if err != nil {
		return nil, fmt.Errorf("list workflow binding revisions: %w", mapWriteError(err))
	}
	defer rows.Close()
	values := make([]Revision, 0)
	for rows.Next() {
		value, err := scanRevision(rows)
		if err != nil {
			return nil, fmt.Errorf("scan workflow binding revision: %w", err)
		}
		values = append(values, value)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate workflow binding revisions: %w", err)
	}
	return values, nil
}

func normalizeCreateRevision(params CreateRevisionParams) (CreateRevisionParams, string, error) {
	params.WorkspaceID = trim(params.WorkspaceID)
	params.BindingID = trim(params.BindingID)
	params.PlanVersionID = trim(params.PlanVersionID)
	params.IdempotencyKey = trim(params.IdempotencyKey)
	if params.WorkspaceID == "" || params.BindingID == "" || params.ExpectedRevision <= 0 || params.PlanVersionID == "" || params.IdempotencyKey == "" {
		return CreateRevisionParams{}, "", storeerr.BadRequest("workspaceId, bindingId, expectedRevision, planVersionId, and idempotencyKey are required")
	}
	actor, err := validateActor(params.Actor)
	if err != nil {
		return CreateRevisionParams{}, "", err
	}
	sourceKind, err := validateSourceKind(params.SourceKind)
	if err != nil {
		return CreateRevisionParams{}, "", err
	}
	configJSON, err := normalizeConfigJSON(params.ConfigJSON)
	if err != nil {
		return CreateRevisionParams{}, "", err
	}
	if err := requireConfigSourceKind(configJSON, sourceKind); err != nil {
		return CreateRevisionParams{}, "", err
	}
	digest, err := validateDigest(params.Digest)
	if err != nil {
		return CreateRevisionParams{}, "", err
	}
	params.Actor = actor
	params.SourceKind = sourceKind
	params.ConfigJSON = configJSON
	params.Digest = digest
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, revisionCreateRequestDigest(
		params.BindingID, params.ExpectedRevision, params.PlanVersionID, params.SourceKind, params.ConfigJSON, params.Digest, params.Actor,
	))
	if err != nil {
		return CreateRevisionParams{}, "", err
	}
	return params, requestDigest, nil
}

func revisionCreateRequestDigest(bindingID string, expectedRevision int64, planVersionID string, sourceKind string, configJSON []byte, digest string, actor Actor) string {
	return requestDigest(
		"binding-revision-create", bindingID, fmt.Sprintf("%d", expectedRevision),
		planVersionID, sourceKind, string(configJSON), digest, actor.Type, actor.ID,
	)
}

func insertRevision(ctx context.Context, execer coredb.QueryExecer, value Revision) error {
	if _, err := execer.Exec(ctx, `
INSERT INTO workflow_binding_revisions (
    id, workspace_id, binding_id, plan_id, revision_number,
    plan_version_id, source_kind, config_json, digest,
	    create_idempotency_scope, create_idempotency_key, create_request_digest,
    created_by_member_id, created_by_actor_type, created_by_actor_id, created_at
) VALUES (
    $1, $2, $3, $4, $5,
    $6, $7, $8, $9,
	    $10, $11, $12,
	    $13, $14, $15, $16
)
`, value.ID, value.WorkspaceID, value.BindingID, value.PlanID, value.Revision,
		value.PlanVersionID, value.SourceKind, string(value.ConfigJSON), value.Digest,
		value.CreateIdempotencyScope, value.CreateIdempotencyKey, value.CreateRequestDigest,
		value.CreatedBy.MemberID, value.CreatedBy.Type, value.CreatedBy.ID, value.CreatedAt); err != nil {
		return fmt.Errorf("insert workflow binding revision: %w", mapWriteError(err))
	}
	return nil
}

func loadRevisionByCreateIdempotency(ctx context.Context, queryer coredb.QueryRower, workspaceID string, bindingID string, actor Actor, scope string, idempotencyKey string, forUpdate bool) (Revision, bool, error) {
	query := revisionSelectSQL() + `
WHERE workspace_id = $1
  AND created_by_actor_type = $2
  AND created_by_actor_id = $3
  AND create_idempotency_scope = $4
  AND create_idempotency_key = $5
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	value, err := scanRevision(queryer.QueryRow(ctx, query, workspaceID, actor.Type, actor.ID, scope, idempotencyKey))
	if coredb.IsNotFound(err) {
		return Revision{}, false, nil
	}
	if err != nil {
		return Revision{}, false, mapWriteError(err)
	}
	if value.BindingID != bindingID {
		return Revision{}, false, IdempotencyConflictError{Operation: "revision create"}
	}
	return value, true, nil
}
