package binding

import (
	"context"
	"fmt"
	"time"

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

type DisableBindingParams struct {
	WorkspaceID      string
	BindingID        string
	ExpectedRevision int64
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

type ArchiveBindingParams struct {
	WorkspaceID      string
	BindingID        string
	ExpectedRevision int64
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

type RestoreBindingParams struct {
	WorkspaceID      string
	BindingID        string
	ExpectedRevision int64
	Actor            Actor
	IdempotencyKey   string
	RequestDigest    string
	Now              time.Time
}

func (store *Store) DisableBinding(ctx context.Context, params DisableBindingParams) (Binding, error) {
	workspaceID, bindingID, actor, idempotencyKey, err := normalizeLifecycle(params.WorkspaceID, params.BindingID, params.ExpectedRevision, params.Actor, params.IdempotencyKey)
	if err != nil {
		return Binding{}, err
	}
	if err := store.Validate(); err != nil {
		return Binding{}, err
	}
	now := utcOrNow(params.Now)
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, lifecycleRequestDigest(CommandOperationDisable, bindingID, params.ExpectedRevision, actor))
	if err != nil {
		return Binding{}, err
	}
	var disabled Binding
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		receipt, found, err := beginBindingCommand(ctx, tx, workspaceID, bindingID, actor, CommandOperationDisable, idempotencyKey, requestDigest, now)
		if err != nil {
			return err
		}
		if found {
			disabled, err = decodeBindingCommandResult[Binding](receipt)
			if err != nil {
				return err
			}
			if disabled.ID != bindingID || disabled.WorkspaceID != workspaceID {
				return fmt.Errorf("workflow binding disable receipt contains a mismatched result")
			}
			return nil
		}
		binding, err := loadBinding(ctx, tx, workspaceID, bindingID, true)
		if err != nil {
			return err
		}
		if err := requireBindingRevision(binding, params.ExpectedRevision); err != nil {
			return err
		}
		if binding.State == BindingStateArchived {
			return ErrArchived
		}
		if binding.State == BindingStateDisabled {
			disabled = binding
			return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationDisable, idempotencyKey, disabled)
		}
		if err := closeActiveSource(ctx, tx, binding, "disabled", now); err != nil {
			return err
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET state = 'disabled',
    active_revision_id = NULL,
    active_source_id = NULL,
    revision = revision + 1,
    updated_by_member_id = $1,
    updated_by_actor_type = $2,
    updated_by_actor_id = $3,
    updated_at = $4
WHERE workspace_id = $5 AND id = $6 AND revision = $7 AND state = 'enabled'
`, actor.MemberID, actor.Type, actor.ID, now, workspaceID, bindingID, params.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("disable workflow binding: %w", mapWriteError(err))
		}
		if updated.RowsAffected() != 1 {
			return RevisionConflictError{Resource: "binding", Expected: params.ExpectedRevision, Actual: binding.Revision}
		}
		disabled, err = loadBinding(ctx, tx, workspaceID, bindingID, false)
		if err != nil {
			return err
		}
		return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationDisable, idempotencyKey, disabled)
	})
	return disabled, err
}

func (store *Store) ArchiveBinding(ctx context.Context, params ArchiveBindingParams) (Binding, error) {
	workspaceID, bindingID, actor, idempotencyKey, err := normalizeLifecycle(params.WorkspaceID, params.BindingID, params.ExpectedRevision, params.Actor, params.IdempotencyKey)
	if err != nil {
		return Binding{}, err
	}
	if err := store.Validate(); err != nil {
		return Binding{}, err
	}
	now := utcOrNow(params.Now)
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, lifecycleRequestDigest(CommandOperationArchive, bindingID, params.ExpectedRevision, actor))
	if err != nil {
		return Binding{}, err
	}
	var archived Binding
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		receipt, found, err := beginBindingCommand(ctx, tx, workspaceID, bindingID, actor, CommandOperationArchive, idempotencyKey, requestDigest, now)
		if err != nil {
			return err
		}
		if found {
			archived, err = decodeBindingCommandResult[Binding](receipt)
			if err != nil {
				return err
			}
			if archived.ID != bindingID || archived.WorkspaceID != workspaceID {
				return fmt.Errorf("workflow binding archive receipt contains a mismatched result")
			}
			return nil
		}
		binding, err := loadBinding(ctx, tx, workspaceID, bindingID, true)
		if err != nil {
			return err
		}
		if err := requireBindingRevision(binding, params.ExpectedRevision); err != nil {
			return err
		}
		if binding.State == BindingStateArchived {
			archived = binding
			return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationArchive, idempotencyKey, archived)
		}
		if binding.ActiveSourceID != "" {
			if err := closeActiveSource(ctx, tx, binding, "archived", now); err != nil {
				return err
			}
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET state = 'archived',
    active_revision_id = NULL,
    active_source_id = NULL,
    revision = revision + 1,
    updated_by_member_id = $1,
    updated_by_actor_type = $2,
    updated_by_actor_id = $3,
    archived_by_member_id = $1,
    archived_by_actor_type = $2,
    archived_by_actor_id = $3,
    archived_at = $4,
    updated_at = $4
WHERE workspace_id = $5 AND id = $6 AND revision = $7 AND state <> 'archived'
`, actor.MemberID, actor.Type, actor.ID, now, workspaceID, bindingID, params.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("archive workflow binding: %w", mapWriteError(err))
		}
		if updated.RowsAffected() != 1 {
			return RevisionConflictError{Resource: "binding", Expected: params.ExpectedRevision, Actual: binding.Revision}
		}
		archived, err = loadBinding(ctx, tx, workspaceID, bindingID, false)
		if err != nil {
			return err
		}
		return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationArchive, idempotencyKey, archived)
	})
	return archived, err
}

func (store *Store) RestoreBinding(ctx context.Context, params RestoreBindingParams) (Binding, error) {
	workspaceID, bindingID, actor, idempotencyKey, err := normalizeLifecycle(params.WorkspaceID, params.BindingID, params.ExpectedRevision, params.Actor, params.IdempotencyKey)
	if err != nil {
		return Binding{}, err
	}
	if err := store.Validate(); err != nil {
		return Binding{}, err
	}
	now := utcOrNow(params.Now)
	requestDigest, err := normalizeCommandRequestDigest(params.RequestDigest, lifecycleRequestDigest(CommandOperationRestore, bindingID, params.ExpectedRevision, actor))
	if err != nil {
		return Binding{}, err
	}
	var restored Binding
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		receipt, found, err := beginBindingCommand(ctx, tx, workspaceID, bindingID, actor, CommandOperationRestore, idempotencyKey, requestDigest, now)
		if err != nil {
			return err
		}
		if found {
			restored, err = decodeBindingCommandResult[Binding](receipt)
			if err != nil {
				return err
			}
			if restored.ID != bindingID || restored.WorkspaceID != workspaceID {
				return fmt.Errorf("workflow binding restore receipt contains a mismatched result")
			}
			return nil
		}
		binding, err := loadBinding(ctx, tx, workspaceID, bindingID, true)
		if err != nil {
			return err
		}
		if err := requireBindingRevision(binding, params.ExpectedRevision); err != nil {
			return err
		}
		if binding.State == BindingStateDisabled {
			restored = binding
			return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationRestore, idempotencyKey, restored)
		}
		if binding.State != BindingStateArchived {
			return ErrInvalidState
		}
		updated, err := tx.Exec(ctx, `
UPDATE workflow_bindings
SET state = 'disabled',
    active_revision_id = NULL,
    active_source_id = NULL,
    revision = revision + 1,
    updated_by_member_id = $1,
    updated_by_actor_type = $2,
    updated_by_actor_id = $3,
    archived_by_member_id = NULL,
    archived_by_actor_type = NULL,
    archived_by_actor_id = NULL,
    archived_at = NULL,
    updated_at = $4
WHERE workspace_id = $5 AND id = $6 AND revision = $7 AND state = 'archived'
`, actor.MemberID, actor.Type, actor.ID, now, workspaceID, bindingID, params.ExpectedRevision)
		if err != nil {
			return fmt.Errorf("restore workflow binding: %w", mapWriteError(err))
		}
		if updated.RowsAffected() != 1 {
			return RevisionConflictError{Resource: "binding", Expected: params.ExpectedRevision, Actual: binding.Revision}
		}
		restored, err = loadBinding(ctx, tx, workspaceID, bindingID, false)
		if err != nil {
			return err
		}
		return completeBindingCommand(ctx, tx, workspaceID, actor, CommandOperationRestore, idempotencyKey, restored)
	})
	return restored, err
}

func normalizeLifecycle(workspaceID string, bindingID string, expectedRevision int64, actor Actor, idempotencyKey string) (string, string, Actor, string, error) {
	workspaceID, bindingID, idempotencyKey = trim(workspaceID), trim(bindingID), trim(idempotencyKey)
	if workspaceID == "" || bindingID == "" || expectedRevision <= 0 || idempotencyKey == "" {
		return "", "", Actor{}, "", storeerr.BadRequest("workspaceId, bindingId, a positive expectedRevision, and idempotencyKey are required")
	}
	actor, err := validateActor(actor)
	if err != nil {
		return "", "", Actor{}, "", err
	}
	return workspaceID, bindingID, actor, idempotencyKey, nil
}
