package plan

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

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

// requireNoMatchingActiveAgentEditWake runs after the Plan row is locked. It
// checks only that Plan's latest Human Builder-edit request and the matching
// responder's durable wake facts, so concurrent request creation and lifecycle
// mutation serialize on the same Plan lock.
func requireNoMatchingActiveAgentEditWake(
	ctx context.Context,
	tx coredb.Tx,
	plan Plan,
	actor Actor,
) error {
	if actor.Type != ActorTypeAgent {
		return nil
	}
	var messageID string
	var responderAgentID string
	err := tx.QueryRow(ctx, `
SELECT
  message.id,
  COALESCE(
    JSON_UNQUOTE(
      JSON_EXTRACT(message.metadata, '$.workflowEdit.responderAgentId')
    ),
    ''
  )
FROM scope_messages message
WHERE message.workspace_id = $1
  AND message.scope_id = $2
  AND message.author_type = 'human'
ORDER BY message.seq DESC
LIMIT 1
`, plan.WorkspaceID, plan.ScopeID).Scan(&messageID, &responderAgentID)
	if coredb.IsNotFound(err) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("load latest Workflow Agent edit request: %w", err)
	}
	if strings.TrimSpace(responderAgentID) != actor.ID {
		return nil
	}

	var outboxStatus sql.NullString
	var terminalEvent sql.NullString
	if err := tx.QueryRow(ctx, `
SELECT
  (
    SELECT outbox.status
    FROM scope_fanout_outbox outbox
    WHERE outbox.workspace_id = $1
      AND outbox.message_id = $3
      AND outbox.event_ref_type = 'message'
      AND outbox.event_ref_id = $3
      AND outbox.fanout_type = 'scope_fanout'
    LIMIT 1
  ),
  (
    SELECT runtime.event_class
    FROM agent_runtime_events runtime
    WHERE runtime.workspace_id = $1
      AND runtime.agent_id = $2
      AND runtime.message_id = $3
      AND runtime.event_class IN (
        'visible_reply_sent',
        'completed_without_visible_reply',
        'wake_failed'
      )
    ORDER BY CASE runtime.event_class
               WHEN 'visible_reply_sent' THEN 3
               WHEN 'completed_without_visible_reply' THEN 2
               WHEN 'wake_failed' THEN 1
               ELSE 0
             END DESC,
             runtime.occurred_at DESC,
             runtime.id DESC
    LIMIT 1
  )
`, plan.WorkspaceID, actor.ID, messageID).Scan(
		&outboxStatus,
		&terminalEvent,
	); err != nil {
		return fmt.Errorf("load Workflow Agent edit wake state: %w", err)
	}
	if terminalEvent.Valid {
		return nil
	}
	switch strings.TrimSpace(outboxStatus.String) {
	case "failed", "suppressed":
		return nil
	default:
		// Missing, unknown, pending, leased, and delivered states all fail
		// closed, matching the durable wake-state owner.
		return ErrAgentEditWakeActive
	}
}

// RequireNoMatchingActiveAgentEditWake applies the same authoritative wake
// fence for a caller that already holds the Plan row lock.
func (store *TxStore) RequireNoMatchingActiveAgentEditWake(
	ctx context.Context,
	plan Plan,
	actor Actor,
) error {
	if err := store.validate(); err != nil {
		return err
	}
	actor, err := validateActor(actor)
	if err != nil {
		return err
	}
	return requireNoMatchingActiveAgentEditWake(ctx, store.tx, plan, actor)
}
