package message

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

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

const WorkflowScopeType = "workflow"

type WorkflowScopeMessagePage struct {
	Messages   []AppMessage
	HasMore    bool
	NextCursor int64
}

type WorkflowScopeMessageWrite struct {
	MessageID                   string
	WorkspaceID                 string
	ScopeID                     string
	ChannelName                 string
	AuthorMemberID              string
	AuthorType                  string
	ClientMessageID             string
	PayloadDigest               string
	Content                     string
	Metadata                    map[string]any
	DeliveryMetadata            map[string]any
	Recipient                   *AgentRecipient
	AuthorProfile               AuthorProfile
	CreatedAt                   time.Time
	RequireWorkflowEditMetadata bool
}

type WorkflowScopeMessageWriteResult struct {
	Message  AppMessage
	Delivery MessageDelivery
	Inserted bool
}

type WorkflowEditRequestMetadata struct {
	PlanID            string
	ResponderAgentID  string
	BaseDraftRevision int64
	TargetKind        string
	TargetID          string
	TargetNodeType    string
}

// RequireWorkflowScope validates one exact product scope without enumerating
// workspace scopes or relying on generic membership authority.
func RequireWorkflowScope(ctx context.Context, queryer coredb.Queryer, workspaceID, scopeID string) error {
	var scopeType string
	err := queryer.QueryRow(ctx, `
SELECT type
FROM scopes
WHERE workspace_id = $1
  AND id = $2
  AND parent_scope_id IS NULL
  AND root_message_id IS NULL
  AND root_message_short_id IS NULL
  AND status = 'active'
  AND archived_at IS NULL
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID)).Scan(&scopeType)
	if err != nil {
		if coredb.IsNotFound(err) {
			return ErrNotFound
		}
		return fmt.Errorf("load workflow scope: %w", err)
	}
	if strings.TrimSpace(scopeType) != WorkflowScopeType {
		return ErrForbidden
	}
	return nil
}

// EnsureWorkflowScopeParticipantMembership sparsely admits one explicitly
// participating member. Existing membership authority is preserved verbatim;
// an inactive open interval is only reactivated, and a public interval is
// inserted only when no open interval exists.
func EnsureWorkflowScopeParticipantMembership(
	ctx context.Context,
	execer coredb.QueryExecer,
	scopeID string,
	memberID string,
	participatedAt time.Time,
) error {
	scopeID = strings.TrimSpace(scopeID)
	memberID = strings.TrimSpace(memberID)
	if scopeID == "" || memberID == "" {
		return invalidArgument("workflow scope and participant member are required")
	}
	participatedAt = participatedAt.UTC()
	if participatedAt.IsZero() {
		participatedAt = time.Now().UTC()
	}

	var effectiveFromSeq int64
	var removedAt, archivedAt sql.NullTime
	err := execer.QueryRow(ctx, `
SELECT effective_from_seq, removed_at, archived_at
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
ORDER BY effective_from_seq DESC
LIMIT 1
FOR UPDATE
`, scopeID, memberID).Scan(&effectiveFromSeq, &removedAt, &archivedAt)
	if err == nil {
		if !removedAt.Valid && !archivedAt.Valid {
			return nil
		}
		result, err := execer.Exec(ctx, `
UPDATE scope_memberships
SET removed_at = NULL,
    archived_at = NULL,
    authority_from_at = LEAST($4, CURRENT_TIMESTAMP(6)),
    authority_to_at = NULL,
    unread_generation = unread_generation + 1,
    updated_at = $4
WHERE scope_id = $1
  AND member_id = $2
  AND effective_from_seq = $3
  AND effective_to_seq IS NULL
`, scopeID, memberID, effectiveFromSeq, participatedAt)
		if err != nil {
			return fmt.Errorf("reactivate workflow scope participant membership: %w", err)
		}
		if result.RowsAffected() != 1 {
			return fmt.Errorf("reactivate workflow scope participant membership: %w", sql.ErrNoRows)
		}
		return nil
	}
	if !coredb.IsNotFound(err) {
		return fmt.Errorf("load workflow scope participant membership: %w", err)
	}

	result, err := execer.Exec(ctx, `
INSERT INTO scope_memberships (
    scope_id, member_id, visibility, owner_member_id,
    last_read_seq, last_read_global_seq, last_activity_global_seq,
    owner_context_dismissed_through_global_seq,
    effective_from_seq, effective_to_seq, authority_from_at, authority_to_at,
    created_at, updated_at, removed_at, archived_at
)
SELECT
    workflow_scope.id, $2, 'public', NULL,
    0, 0, 0, 0,
    GREATEST(
      workflow_scope.latest_seq + 1,
      COALESCE((
        SELECT MAX(existing.effective_from_seq) + 1
        FROM scope_memberships existing
        WHERE existing.scope_id = workflow_scope.id
          AND existing.member_id = $2
      ), 1)
    ),
    NULL,
    LEAST($3, CURRENT_TIMESTAMP(6)), NULL, $3, $3, NULL, NULL
FROM scopes workflow_scope
WHERE workflow_scope.id = $1
  AND workflow_scope.type = 'workflow'
  AND workflow_scope.parent_scope_id IS NULL
  AND workflow_scope.root_message_id IS NULL
  AND workflow_scope.root_message_short_id IS NULL
  AND workflow_scope.status = 'active'
  AND workflow_scope.archived_at IS NULL
`, scopeID, memberID, participatedAt)
	if err != nil {
		return fmt.Errorf("insert workflow scope participant membership: %w", err)
	}
	if result.RowsAffected() != 1 {
		return ErrNotFound
	}
	return nil
}

// RequireWorkflowScopeParticipant verifies current write participation without
// granting Workflow history authority.
func RequireWorkflowScopeParticipant(
	ctx context.Context,
	queryer coredb.Queryer,
	scopeID, memberID string,
) error {
	var found int
	err := queryer.QueryRow(ctx, `
SELECT 1
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND effective_to_seq IS NULL
  AND removed_at IS NULL
  AND archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(scopeID), strings.TrimSpace(memberID)).Scan(&found)
	if coredb.IsNotFound(err) {
		return ErrForbidden
	}
	if err != nil {
		return fmt.Errorf("load workflow scope participant: %w", err)
	}
	return nil
}

// ListWorkflowScopeMessages reads one bounded scope-local page. It deliberately
// bypasses generic conversation/history authority; callers must first enforce
// the owning Workflow Plan product gate.
func (store *Store) ListWorkflowScopeMessages(ctx context.Context, workspaceID, scopeID string, beforeSeq int64, limit int) (WorkflowScopeMessagePage, error) {
	if store == nil || store.db == nil {
		return WorkflowScopeMessagePage{}, fmt.Errorf("workflow scope message store is not configured")
	}
	if limit <= 0 || limit > 50 {
		return WorkflowScopeMessagePage{}, invalidArgument("workflow message limit must be between 1 and 50")
	}
	if err := RequireWorkflowScope(ctx, store.db, workspaceID, scopeID); err != nil {
		return WorkflowScopeMessagePage{}, err
	}
	records, err := NewScopeRepositories(store.db).Messages.ListBeforeSeq(ctx, scopeID, beforeSeq, limit+1)
	if err != nil {
		return WorkflowScopeMessagePage{}, err
	}
	hasMore := len(records) > limit
	if hasMore {
		records = records[:limit]
	}
	messages, err := workflowAppMessagesFromRecords(ctx, store.db, workspaceID, records)
	if err != nil {
		return WorkflowScopeMessagePage{}, err
	}
	nextCursor := int64(0)
	if hasMore && len(messages) != 0 {
		nextCursor = messages[len(messages)-1].Seq
	}
	return WorkflowScopeMessagePage{Messages: messages, HasMore: hasMore, NextCursor: nextCursor}, nil
}

func FindWorkflowScopeMessageByClientID(
	ctx context.Context,
	tx coredb.Tx,
	scopeID, authorMemberID, clientMessageID string,
) (ScopeMessageRecord, bool, error) {
	record, err := NewScopeRepositories(tx).Messages.LoadByClientMessageIDForUpdate(
		ctx, scopeID, authorMemberID, authorMemberID, clientMessageID,
	)
	if errors.Is(err, ErrNotFound) {
		return ScopeMessageRecord{}, false, nil
	}
	if err != nil {
		return ScopeMessageRecord{}, false, err
	}
	return record, true, nil
}

func DecodeWorkflowEditRequestMetadata(raw []byte) (WorkflowEditRequestMetadata, error) {
	metadata, err := DecodeMessageMetadata(raw)
	if err != nil {
		return WorkflowEditRequestMetadata{}, err
	}
	edit, ok := metadata[WorkflowAgentEditMetadataKey].(map[string]any)
	if !ok {
		return WorkflowEditRequestMetadata{}, invalidArgument("workflow edit request metadata is missing")
	}
	kind, _ := edit["kind"].(string)
	if strings.TrimSpace(kind) != "request" {
		return WorkflowEditRequestMetadata{}, invalidArgument("workflow edit request metadata kind is invalid")
	}
	target, ok := edit["target"].(map[string]any)
	if !ok {
		return WorkflowEditRequestMetadata{}, invalidArgument("workflow edit request target metadata is missing")
	}
	value := WorkflowEditRequestMetadata{
		PlanID:           stringMetadataValue(edit, "planId"),
		ResponderAgentID: stringMetadataValue(edit, "responderAgentId"),
		TargetKind:       stringMetadataValue(target, "kind"),
		TargetID:         stringMetadataValue(target, "id"),
		TargetNodeType:   stringMetadataValue(target, "nodeType"),
	}
	revision, ok := integerMetadataValue(edit["baseDraftRevision"])
	if !ok || revision <= 0 {
		return WorkflowEditRequestMetadata{}, invalidArgument("workflow edit request revision metadata is invalid")
	}
	value.BaseDraftRevision = revision
	if value.PlanID == "" || value.ResponderAgentID == "" || value.TargetKind == "" || value.TargetID == "" {
		return WorkflowEditRequestMetadata{}, invalidArgument("workflow edit request metadata is incomplete")
	}
	return value, nil
}

func stringMetadataValue(values map[string]any, key string) string {
	value, _ := values[key].(string)
	return strings.TrimSpace(value)
}

func integerMetadataValue(value any) (int64, bool) {
	switch typed := value.(type) {
	case float64:
		result := int64(typed)
		return result, float64(result) == typed
	case int64:
		return typed, true
	default:
		return 0, false
	}
}

// LoadLatestWorkflowEditRequest reads the newest server-owned Human request
// without taking a row lock.
func LoadLatestWorkflowEditRequest(ctx context.Context, queryer coredb.Queryer, workspaceID, scopeID string) (ScopeMessageRecord, bool, error) {
	return loadLatestWorkflowEditRequest(ctx, queryer, workspaceID, scopeID, false)
}

// LoadLatestWorkflowEditRequestForUpdate performs the latest-request lookup as
// a current locking read inside the caller's Plan transaction.
func LoadLatestWorkflowEditRequestForUpdate(ctx context.Context, queryer coredb.Queryer, workspaceID, scopeID string) (ScopeMessageRecord, bool, error) {
	return loadLatestWorkflowEditRequest(ctx, queryer, workspaceID, scopeID, true)
}

func loadLatestWorkflowEditRequest(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID, scopeID string,
	forUpdate bool,
) (ScopeMessageRecord, bool, error) {
	var record ScopeMessageRecord
	var metadata []byte
	query := `
SELECT id, workspace_id, scope_id, seq, workspace_global_seq, author_member_id,
       author_type, COALESCE(client_message_id, ''), COALESCE(idempotency_payload_digest, ''),
       message_type, content, metadata, created_at, updated_at
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND author_type = 'human'
ORDER BY seq DESC
LIMIT 1
`
	if forUpdate {
		query += "FOR UPDATE\n"
	}
	err := queryer.QueryRow(ctx, query, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID)).Scan(
		&record.ID, &record.WorkspaceID, &record.ScopeID, &record.Seq,
		&record.WorkspaceGlobalSeq, &record.AuthorMemberID, &record.AuthorType,
		&record.ClientMessageID, &record.IdempotencyPayloadDigest,
		&record.MessageType, &record.Content, &metadata, &record.CreatedAt, &record.UpdatedAt,
	)
	if coredb.IsNotFound(err) {
		return ScopeMessageRecord{}, false, nil
	}
	if err != nil {
		return ScopeMessageRecord{}, false, fmt.Errorf("load latest workflow edit request: %w", err)
	}
	record.Metadata = append([]byte(nil), metadata...)
	return record, true, nil
}

// WriteWorkflowScopeMessageTx appends a trusted product-scope message with one
// authoritative outbox. A nil recipient is an Agent response and intentionally
// wakes nobody.
func WriteWorkflowScopeMessageTx(ctx context.Context, tx coredb.Tx, input WorkflowScopeMessageWrite) (WorkflowScopeMessageWriteResult, error) {
	if err := RequireWorkflowScope(ctx, tx, input.WorkspaceID, input.ScopeID); err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	if err := ValidateWorkflowContextMetadata(input.Metadata, true); err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	if input.RequireWorkflowEditMetadata {
		if _, ok := input.Metadata[WorkflowAgentEditMetadataKey]; !ok {
			return WorkflowScopeMessageWriteResult{}, invalidArgument("workflow edit metadata is required")
		}
	}
	encodedMetadata, decodedMetadata, err := EncodeMessageMetadata(input.Metadata)
	if err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	deliveryMetadata := decodedMetadata
	if input.DeliveryMetadata != nil {
		if _, deliveryMetadata, err = EncodeMessageMetadata(input.DeliveryMetadata); err != nil {
			return WorkflowScopeMessageWriteResult{}, err
		}
	}
	digest := strings.TrimSpace(input.PayloadDigest)
	if !isMessagePayloadDigest(digest) {
		return WorkflowScopeMessageWriteResult{}, invalidArgument("valid workflow message payload digest is required")
	}
	messageID := strings.TrimSpace(input.MessageID)
	if messageID == "" {
		messageID, err = identifiers.NewMessageID()
		if err != nil {
			return WorkflowScopeMessageWriteResult{}, fmt.Errorf("generate workflow message id: %w", err)
		}
	}
	createdAt := input.CreatedAt.UTC()
	if createdAt.IsZero() {
		createdAt = time.Now().UTC()
	}
	seq, err := AllocateNextScopeMessageSeq(ctx, tx, input.ScopeID)
	if err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	globalSeq, err := AllocateNextWorkspaceMessageSeq(ctx, tx, input.WorkspaceID)
	if err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	appMessage := AppMessage{
		ID: messageID, Seq: seq, ChannelID: strings.TrimSpace(input.ScopeID),
		ChannelType: WorkflowScopeType, ClientMessageID: strings.TrimSpace(input.ClientMessageID),
		Content: input.Content, Metadata: decodedMetadata, CreatedAt: createdAt, MessageType: "text",
	}
	appMessage.ApplyAuthorProfile(input.AuthorProfile)
	delivery := MessageDelivery{
		ThreadID: input.ScopeID, Seq: seq, GlobalSeq: globalSeq, MessageID: messageID,
		ClientMessageID:             strings.TrimSpace(input.ClientMessageID),
		NotifyPrincipalMemberID:     strings.TrimSpace(input.AuthorMemberID),
		HumanRecipientsTargeted:     true,
		SuppressBrowserNotification: true,
		Message: ReceiveMessage{
			MessageID: messageID, TargetKey: input.ScopeID, TargetSeq: seq,
			NotifyPrincipalMemberID: strings.TrimSpace(input.AuthorMemberID),
			ChannelType:             WorkflowScopeType, ChannelName: firstNonEmpty(strings.TrimSpace(input.ChannelName), input.ScopeID),
			AuthorProfile: appMessage.AuthorProfile, MessageType: "text",
			Content: input.Content, Metadata: CopyMessageMetadata(deliveryMetadata), Timestamp: createdAt,
		},
	}
	if input.Recipient != nil {
		recipient := *input.Recipient
		recipient.DeliveryReason = AgentDeliveryReasonDirectMention
		delivery.Recipients = []AgentRecipient{recipient}
		delivery.WakeRecipients = []AgentRecipient{recipient}
	}
	delivery.AppMessage = &appMessage
	payload, err := MarshalScopeFanoutPayload(appMessage, delivery)
	if err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	outbox := BuildScopeMessageFanoutOutboxRecord(ScopeMessageFanoutOutboxRecordParams{
		WorkspaceID: input.WorkspaceID, ScopeID: input.ScopeID, MessageID: messageID,
		DedupeKey: "scope:" + input.ScopeID + ":message:" + messageID + ":fanout:" + ScopeOutboxFanoutScope,
		Payload:   payload, TargetedHumanDelivery: true, CreatedAt: createdAt, NextAttemptAt: createdAt,
	})
	applied, err := ApplyMessagePlanTx(ctx, tx, MessagePlan{
		MessageWrite: ScopeMessageRecord{
			ID: messageID, WorkspaceID: input.WorkspaceID, ScopeID: input.ScopeID,
			Seq: seq, WorkspaceGlobalSeq: globalSeq, AuthorMemberID: input.AuthorMemberID,
			AuthorType: input.AuthorType, NotifyPrincipalMemberID: input.AuthorMemberID,
			IdempotencySubjectMemberID: input.AuthorMemberID, ClientMessageID: input.ClientMessageID,
			IdempotencyPayloadDigest: digest, MessageType: "text", Content: input.Content,
			Metadata: encodedMetadata, CreatedAt: createdAt, UpdatedAt: createdAt,
		},
		SideEffectProfile: MessagePlanSideEffectProfileProductScopeFanout,
		OutboxEvents:      []ScopeFanoutOutboxRecord{outbox},
	})
	if err != nil {
		return WorkflowScopeMessageWriteResult{}, err
	}
	if !applied.Message.Inserted {
		replayed, err := workflowAppMessagesFromRecords(ctx, tx, input.WorkspaceID, []ScopeMessageRecord{applied.Message.Message})
		if err != nil {
			return WorkflowScopeMessageWriteResult{}, err
		}
		return WorkflowScopeMessageWriteResult{Message: replayed[0], Inserted: false}, nil
	}
	return WorkflowScopeMessageWriteResult{Message: appMessage, Delivery: delivery, Inserted: true}, nil
}

func workflowAppMessagesFromRecords(ctx context.Context, queryer coredb.Queryer, workspaceID string, records []ScopeMessageRecord) ([]AppMessage, error) {
	memberIDs := make([]string, 0, len(records))
	for _, record := range records {
		memberIDs = append(memberIDs, record.AuthorMemberID)
	}
	presentations, err := loadScopeMemberPresentations(ctx, queryer, workspaceID, memberIDs)
	if err != nil {
		return nil, err
	}
	messages := make([]AppMessage, 0, len(records))
	for _, record := range records {
		metadata, err := DecodeMessageMetadata(record.Metadata)
		if err != nil {
			return nil, err
		}
		item := AppMessage{
			ID: record.ID, Seq: record.Seq, ChannelID: record.ScopeID,
			ChannelType: WorkflowScopeType, ClientMessageID: record.ClientMessageID,
			Content: record.Content, Metadata: metadata, CreatedAt: record.CreatedAt,
			EditedAt: record.EditedAt, EditedByMemberID: record.EditedByMemberID,
			EditRevision: record.EditRevision, MessageType: record.MessageType,
		}
		item.ApplyAuthorProfile(presentations[record.AuthorMemberID].AuthorProfile())
		messages = append(messages, item)
	}
	return messages, nil
}

func WorkflowAppMessageFromRecord(ctx context.Context, queryer coredb.Queryer, workspaceID string, record ScopeMessageRecord) (AppMessage, error) {
	messages, err := workflowAppMessagesFromRecords(ctx, queryer, workspaceID, []ScopeMessageRecord{record})
	if err != nil {
		return AppMessage{}, err
	}
	if len(messages) != 1 {
		return AppMessage{}, fmt.Errorf("workflow message projection is missing")
	}
	return messages[0], nil
}

// HasWorkflowScopeAgentReplyAfter performs a current locking read for an
// existing responder reply after one Human request.
func HasWorkflowScopeAgentReplyAfter(
	ctx context.Context,
	queryer coredb.Queryer,
	workspaceID, scopeID, authorMemberID string,
	requestSeq int64,
) (bool, error) {
	var found int
	err := queryer.QueryRow(ctx, `
SELECT 1
FROM scope_messages
WHERE workspace_id = $1
  AND scope_id = $2
  AND author_member_id = $3
  AND author_type = 'agent'
  AND seq > $4
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID), strings.TrimSpace(authorMemberID), requestSeq).Scan(&found)
	if err == nil {
		return true, nil
	}
	if coredb.IsNotFound(err) {
		return false, nil
	}
	return false, fmt.Errorf("load workflow Agent reply after request: %w", err)
}

// LatestWorkflowScopeMessageSeq returns the latest Scope sequence using a
// current locking read inside the caller's Plan transaction.
func LatestWorkflowScopeMessageSeq(ctx context.Context, queryer coredb.Queryer, scopeID string) (int64, error) {
	var seq int64
	err := queryer.QueryRow(ctx, `
SELECT seq
FROM scope_messages
WHERE scope_id = $1
ORDER BY seq DESC
LIMIT 1
FOR UPDATE
`, strings.TrimSpace(scopeID)).Scan(&seq)
	if coredb.IsNotFound(err) {
		return 0, nil
	}
	if err != nil {
		return 0, fmt.Errorf("load latest workflow message sequence: %w", err)
	}
	return seq, nil
}
