package agentops

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

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/onboarding"
)

type MessageWakeState struct {
	OutboxStatus string
	EventClass   string
	Detail       string
	OccurredAt   time.Time
	RecordedAt   time.Time
	Active       bool
}

// LoadMessageWakeState derives the lifecycle of one exact Agent/message wake.
// Missing or unknown facts fail closed as active so callers cannot overlap
// Workflow edits when durable state is ambiguous.
func (txs TxStore) LoadMessageWakeState(ctx context.Context, workspaceID, agentID, messageID string) (MessageWakeState, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	messageID = strings.TrimSpace(messageID)
	if workspaceID == "" || agentID == "" || messageID == "" {
		return MessageWakeState{}, fmt.Errorf("workspace, Agent, and message are required")
	}
	var outboxStatus string
	var outboxDetail string
	var outboxOccurredAt time.Time
	err := txs.tx.QueryRow(ctx, `
SELECT status,
       COALESCE(last_error, ''),
       COALESCE(delivered_at, suppressed_at, next_attempt_at, created_at)
FROM scope_fanout_outbox
WHERE workspace_id = $1
  AND message_id = $2
  AND event_ref_type = 'message'
  AND event_ref_id = $2
  AND fanout_type = 'scope_fanout'
LIMIT 1
`, workspaceID, messageID).Scan(&outboxStatus, &outboxDetail, &outboxOccurredAt)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return MessageWakeState{}, fmt.Errorf("load message wake outbox: %w", err)
	}

	var eventClass string
	var eventDetail string
	var eventOccurredAt time.Time
	var eventRecordedAt time.Time
	err = txs.tx.QueryRow(ctx, `
SELECT event_class, detail, occurred_at, created_at
FROM agent_runtime_events
WHERE workspace_id = $1
  AND agent_id = $2
  AND message_id = $3
  AND event_class IN (
    'visible_reply_sent',
    'completed_without_visible_reply',
    'wake_failed'
  )
ORDER BY CASE event_class
           WHEN 'visible_reply_sent' THEN 3
           WHEN 'completed_without_visible_reply' THEN 2
           WHEN 'wake_failed' THEN 1
           ELSE 0
         END DESC,
         occurred_at DESC,
         id DESC
LIMIT 1
`, workspaceID, agentID, messageID).Scan(&eventClass, &eventDetail, &eventOccurredAt, &eventRecordedAt)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return MessageWakeState{}, fmt.Errorf("load message wake runtime event: %w", err)
	}

	state := MessageWakeState{
		OutboxStatus: strings.TrimSpace(outboxStatus),
		EventClass:   strings.TrimSpace(eventClass),
		Detail:       strings.TrimSpace(outboxDetail),
		OccurredAt:   outboxOccurredAt.UTC(),
		RecordedAt:   outboxOccurredAt.UTC(),
		Active:       true,
	}
	if state.EventClass != "" {
		state.Detail = strings.TrimSpace(eventDetail)
		state.OccurredAt = eventOccurredAt.UTC()
		state.RecordedAt = eventRecordedAt.UTC()
	}
	switch state.EventClass {
	case AgentRuntimeEventClassVisibleReplySent,
		AgentRuntimeEventClassCompletedWithoutVisibleReply,
		AgentRuntimeEventClassWakeFailed:
		state.Active = false
		return state, nil
	}
	switch state.OutboxStatus {
	case "failed", "suppressed":
		state.Active = false
	case "pending", "pending_v2", "leased", "leased_v2", "delivered":
		state.Active = true
	default:
		// Unknown or missing status remains active.
	}
	return state, nil
}

const (
	AgentRuntimeEventClassRoutedMessageDeferred        = agent.AgentRuntimeEventClassRoutedMessageDeferred
	AgentRuntimeEventClassDeferredReplayStarted        = agent.AgentRuntimeEventClassDeferredReplayStarted
	AgentRuntimeEventClassVisibleReplySent             = agent.AgentRuntimeEventClassVisibleReplySent
	AgentRuntimeEventClassCompletedWithoutVisibleReply = agent.AgentRuntimeEventClassCompletedWithoutVisibleReply
	AgentRuntimeEventClassWakeFailed                   = agent.AgentRuntimeEventClassWakeFailed
	AgentRuntimeEventClassRuntimeStartFailed           = agent.AgentRuntimeEventClassRuntimeStartFailed
	AgentRuntimeEventClassRuntimeExitedUnexpectedly    = agent.AgentRuntimeEventClassRuntimeExitedUnexpectedly
	AgentRuntimeEventClassRuntimeRetrying              = agent.AgentRuntimeEventClassRuntimeRetrying
)

type agentRuntimeFailureSummaryRow struct {
	AgentID    string    `db:"agent_id"`
	MessageID  string    `db:"message_id"`
	EventClass string    `db:"event_class"`
	Detail     string    `db:"detail"`
	OccurredAt time.Time `db:"occurred_at"`
}

func normalizeAgentRuntimeEventClass(eventClass string) string {
	switch strings.TrimSpace(eventClass) {
	case AgentRuntimeEventClassRoutedMessageDeferred,
		AgentRuntimeEventClassDeferredReplayStarted,
		AgentRuntimeEventClassVisibleReplySent,
		AgentRuntimeEventClassCompletedWithoutVisibleReply,
		AgentRuntimeEventClassWakeFailed,
		AgentRuntimeEventClassRuntimeStartFailed,
		AgentRuntimeEventClassRuntimeExitedUnexpectedly,
		AgentRuntimeEventClassRuntimeRetrying:
		return strings.TrimSpace(eventClass)
	default:
		return ""
	}
}

func summaryForAgentRuntimeFailureEventClass(eventClass string) string {
	switch strings.TrimSpace(eventClass) {
	case AgentRuntimeEventClassWakeFailed:
		return "Wake handling failed after the daemon accepted the message."
	case AgentRuntimeEventClassRuntimeStartFailed:
		return "Runtime startup failed after the daemon accepted the message."
	case AgentRuntimeEventClassRuntimeExitedUnexpectedly:
		return "Runtime exited unexpectedly while handling accepted work."
	default:
		return ""
	}
}

func (store *Store) RecordAgentRuntimeEvent(ctx context.Context, event agent.AgentRuntimeEvent) error {
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		return store.InTx(tx).RecordAgentRuntimeEvent(ctx, event)
	})
	if err != nil {
		return err
	}
	if store.onboardingEvidence != nil && strings.TrimSpace(event.EventClass) == AgentRuntimeEventClassVisibleReplySent {
		if err := store.onboardingEvidence.RecordVisibleReply(ctx, onboarding.VisibleReplyEvidence{
			WorkspaceID:    event.WorkspaceID,
			AgentID:        event.AgentID,
			MessageID:      event.MessageID,
			ReplyMessageID: event.ReplyMessageID,
		}); err != nil {
			// Runtime event persistence is authoritative. Onboarding projection can
			// be retried without asking the daemon to replay a delivered reply.
			store.logger.WarnContext(ctx, "project visible reply into desktop onboarding", "error", err)
		}
	}
	return nil
}

func (txs TxStore) RecordAgentRuntimeEvent(ctx context.Context, event agent.AgentRuntimeEvent) error {
	eventClass := normalizeAgentRuntimeEventClass(event.EventClass)
	if eventClass == "" {
		return fmt.Errorf("event class is required")
	}
	workspaceID := strings.TrimSpace(event.WorkspaceID)
	agentID := strings.TrimSpace(event.AgentID)
	machineID := strings.TrimSpace(event.MachineID)
	messageID := strings.TrimSpace(event.MessageID)
	wakeToken := strings.TrimSpace(event.WakeToken)
	target := strings.TrimSpace(event.Target)
	replyMessageID := strings.TrimSpace(event.ReplyMessageID)
	detail := strings.TrimSpace(event.Detail)
	if workspaceID == "" || agentID == "" || machineID == "" {
		return fmt.Errorf("workspace id, agent id, and machine id are required")
	}
	occurredAt := event.OccurredAt.UTC()
	if occurredAt.IsZero() {
		occurredAt = time.Now().UTC()
	}

	_, err := txs.tx.Exec(ctx, `
INSERT INTO agent_runtime_events (
    workspace_id,
    agent_id,
    machine_id,
    message_id,
    wake_token,
    event_class,
    target,
    reply_message_id,
    detail,
    occurred_at,
    created_at
) VALUES ($1, $2, $3, NULLIF($4, ''), NULLIF($5, ''), $6, $7, $8, $9, $10, NOW())
`, workspaceID, agentID, machineID, messageID, wakeToken, eventClass, target, replyMessageID, detail, occurredAt)
	if err != nil {
		return fmt.Errorf("insert agent runtime event: %w", err)
	}
	if summaryForAgentRuntimeFailureEventClass(eventClass) != "" {
		if err := txs.upsertLatestAgentRuntimeFailureSummary(ctx, workspaceID, agentID, messageID, eventClass, detail, occurredAt); err != nil {
			return err
		}
	}
	return nil
}

func (txs TxStore) upsertLatestAgentRuntimeFailureSummary(ctx context.Context, workspaceID string, agentID string, messageID string, eventClass string, detail string, occurredAt time.Time) error {
	const newerEventCondition = "VALUES(occurred_at) > occurred_at OR (VALUES(occurred_at) = occurred_at AND VALUES(recorded_at) >= recorded_at)"

	_, err := txs.tx.Exec(ctx, fmt.Sprintf(`
INSERT INTO agent_runtime_failure_summaries (
    workspace_id,
    agent_id,
    message_id,
    event_class,
    detail,
    occurred_at,
    recorded_at
) VALUES ($1, $2, $3, $4, $5, $6, NOW(6))
ON DUPLICATE KEY UPDATE
    message_id = IF(%s, VALUES(message_id), message_id),
    event_class = IF(%s, VALUES(event_class), event_class),
    detail = IF(%s, VALUES(detail), detail),
    occurred_at = IF(%s, VALUES(occurred_at), occurred_at),
    recorded_at = IF(%s, VALUES(recorded_at), recorded_at)
`, newerEventCondition, newerEventCondition, newerEventCondition, newerEventCondition, newerEventCondition), workspaceID, agentID, messageID, eventClass, detail, occurredAt)
	if err != nil {
		return fmt.Errorf("upsert latest agent runtime failure summary: %w", err)
	}
	return nil
}

func (store *Store) LoadLatestAgentRuntimeFailureSummary(ctx context.Context, workspaceID string, agentID string) (*agent.AgentRuntimeFailureSummary, error) {
	var summary *agent.AgentRuntimeFailureSummary
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		summary, err = store.InTx(tx).LoadLatestAgentRuntimeFailureSummary(ctx, workspaceID, agentID)
		return err
	})
	return summary, err
}

func (txs TxStore) LoadLatestAgentRuntimeFailureSummary(ctx context.Context, workspaceID string, agentID string) (*agent.AgentRuntimeFailureSummary, error) {
	summaries, err := txs.LoadLatestAgentRuntimeFailureSummaries(ctx, workspaceID, []string{agentID})
	if err != nil {
		return nil, err
	}
	summary, ok := summaries[strings.TrimSpace(agentID)]
	if !ok {
		return nil, nil
	}
	return &summary, nil
}

func (store *Store) LoadLatestAgentRuntimeFailureSummaries(ctx context.Context, workspaceID string, agentIDs []string) (map[string]agent.AgentRuntimeFailureSummary, error) {
	var summaries map[string]agent.AgentRuntimeFailureSummary
	err := coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		var err error
		summaries, err = store.InTx(tx).LoadLatestAgentRuntimeFailureSummaries(ctx, workspaceID, agentIDs)
		return err
	})
	return summaries, err
}

func (txs TxStore) LoadLatestAgentRuntimeFailureSummaries(ctx context.Context, workspaceID string, agentIDs []string) (map[string]agent.AgentRuntimeFailureSummary, error) {
	normalizedAgentIDs := make([]string, 0, len(agentIDs))
	seen := make(map[string]struct{}, len(agentIDs))
	for _, agentID := range agentIDs {
		trimmed := strings.TrimSpace(agentID)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		normalizedAgentIDs = append(normalizedAgentIDs, trimmed)
	}
	if len(normalizedAgentIDs) == 0 {
		return map[string]agent.AgentRuntimeFailureSummary{}, nil
	}

	query, args, err := buildLoadLatestAgentRuntimeFailureSummariesQuery(strings.TrimSpace(workspaceID), normalizedAgentIDs)
	if err != nil {
		return nil, err
	}

	var rows []agentRuntimeFailureSummaryRow
	if err := coredb.Select(ctx, txs.tx, &rows, query, args...); err != nil {
		return nil, fmt.Errorf("load latest agent runtime failure summaries: %w", err)
	}

	summaries := make(map[string]agent.AgentRuntimeFailureSummary)
	for _, row := range rows {
		summary := summaryForAgentRuntimeFailureEventClass(row.EventClass)
		if summary == "" {
			continue
		}
		summaries[strings.TrimSpace(row.AgentID)] = agent.AgentRuntimeFailureSummary{
			EventClass: strings.TrimSpace(row.EventClass),
			MessageID:  strings.TrimSpace(row.MessageID),
			Summary:    summary,
			Detail:     strings.TrimSpace(row.Detail),
			OccurredAt: row.OccurredAt,
		}
	}

	return summaries, nil
}

func buildLoadLatestAgentRuntimeFailureSummariesQuery(workspaceID string, agentIDs []string) (string, []any, error) {
	trimmedWorkspaceID := strings.TrimSpace(workspaceID)
	if trimmedWorkspaceID == "" {
		return "", nil, fmt.Errorf("workspace id is required")
	}

	agentIDsCTE, agentArgs, _, err := coredb.BuildStringValuesCTE("agent_ids", "agent_id", agentIDs, 2)
	if err != nil {
		return "", nil, err
	}

	query := fmt.Sprintf(`
WITH %s
SELECT
    summaries.agent_id,
    COALESCE(summaries.message_id, '') AS message_id,
    summaries.event_class,
    COALESCE(summaries.detail, '') AS detail,
    summaries.occurred_at
FROM agent_ids
JOIN agent_runtime_failure_summaries summaries
  ON summaries.workspace_id = $1
 AND summaries.agent_id = agent_ids.agent_id
ORDER BY summaries.agent_id ASC
`, agentIDsCTE)
	args := append([]any{trimmedWorkspaceID}, agentArgs...)
	return query, args, nil
}
