package invocation

import (
	"context"
	"fmt"
	"strings"
	"time"

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

const (
	schedulePlacementStateActive   = "active"
	schedulePlacementStateDraining = "draining"
	schedulePlacementStateMoving   = "moving"
)

// lockManualAcceptancePlacement is the first topology lock taken after an
// exact actor-scoped replay miss. Direct and scheduled admission therefore
// share the same routable placement states and cannot create durable work
// behind a missing, disabled, or malformed routing fence.
func lockManualAcceptancePlacement(ctx context.Context, tx coredb.Tx, workspaceID string) error {
	var groupID string
	var state string
	var epoch int64
	err := tx.QueryRow(ctx, `
SELECT workspace_group_id, state, placement_epoch
FROM workspace_placements
WHERE workspace_id = $1
FOR UPDATE
`, workspaceID).Scan(&groupID, &state, &epoch)
	if err != nil {
		if coredb.IsNotFound(err) {
			return storeerr.Conflict(fmt.Sprintf("workspace placement for manual Workflow invocation %s is missing", workspaceID))
		}
		return fmt.Errorf("lock workspace placement for manual Workflow invocation %s: %w", workspaceID, err)
	}
	groupID = strings.TrimSpace(groupID)
	state = strings.TrimSpace(state)
	if groupID == "" || epoch <= 0 || !schedulePlacementRoutable(state) {
		return storeerr.Conflict(fmt.Sprintf(
			"workspace placement for manual Workflow invocation %s is not routable: group=%q state=%q epoch=%d",
			workspaceID, groupID, state, epoch,
		))
	}
	return nil
}

// scheduleCandidate is deliberately private. Its placement generation is a
// DB-issued capability which callers can inspect only through the domain's
// diagnostic ScheduleCandidate methods and must return unchanged.
type scheduleCandidate struct {
	sourceID       string
	workspaceID    string
	bindingID      string
	occurrenceAt   time.Time
	activation     int64
	workspaceGroup string
	placementState string
	placementEpoch int64
}

func (candidate scheduleCandidate) SourceID() string        { return candidate.sourceID }
func (candidate scheduleCandidate) WorkspaceID() string     { return candidate.workspaceID }
func (candidate scheduleCandidate) OccurrenceAt() time.Time { return candidate.occurrenceAt }

var _ domain.ScheduleCandidate = scheduleCandidate{}

func requireStoreScheduleCandidate(candidate domain.ScheduleCandidate) (scheduleCandidate, error) {
	value, ok := candidate.(scheduleCandidate)
	if !ok {
		return scheduleCandidate{}, storeerr.BadRequest("schedule candidate was not issued by the Workflow Invocation store")
	}
	value.sourceID = strings.TrimSpace(value.sourceID)
	value.workspaceID = strings.TrimSpace(value.workspaceID)
	value.bindingID = strings.TrimSpace(value.bindingID)
	value.workspaceGroup = strings.TrimSpace(value.workspaceGroup)
	value.placementState = strings.TrimSpace(value.placementState)
	value.occurrenceAt = value.occurrenceAt.UTC().Truncate(time.Microsecond)
	if value.sourceID == "" || value.workspaceID == "" || value.bindingID == "" || value.occurrenceAt.IsZero() ||
		value.activation <= 0 || value.workspaceGroup == "" || !schedulePlacementRoutable(value.placementState) || value.placementEpoch <= 0 {
		return scheduleCandidate{}, storeerr.BadRequest("schedule candidate is incomplete")
	}
	return value, nil
}

// lockScheduleCandidatePlacement is always the first locking read performed
// by scheduled materialization. A missing placement is an integrity error;
// a changed but valid placement is an ordinary fenced outcome.
func (store *Store) lockScheduleCandidatePlacement(ctx context.Context, tx coredb.Tx, candidate scheduleCandidate) (bool, error) {
	var groupID string
	var state string
	var epoch int64
	err := tx.QueryRow(ctx, `
SELECT workspace_group_id, state, placement_epoch
FROM workspace_placements
WHERE workspace_id = $1
FOR UPDATE
`, candidate.workspaceID).Scan(&groupID, &state, &epoch)
	if err != nil {
		if coredb.IsNotFound(err) {
			return false, fmt.Errorf("workspace placement for scheduled Workflow candidate %s is missing: %w", candidate.workspaceID, err)
		}
		return false, fmt.Errorf("lock workspace placement for scheduled Workflow candidate %s: %w", candidate.workspaceID, err)
	}
	groupID = strings.TrimSpace(groupID)
	state = strings.TrimSpace(state)
	if groupID != candidate.workspaceGroup || state != candidate.placementState || epoch != candidate.placementEpoch || !schedulePlacementRoutable(state) {
		return false, nil
	}
	if scopedGroup := store.schedulerWorkspaceGroupID(); scopedGroup != "" && scopedGroup != groupID {
		return false, nil
	}
	return true, nil
}

func schedulePlacementRoutable(state string) bool {
	switch strings.TrimSpace(state) {
	case schedulePlacementStateActive, schedulePlacementStateDraining, schedulePlacementStateMoving:
		return true
	default:
		return false
	}
}
