package app

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/coordinator"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/taskboard"
)

// SendAgentTaskScopeMessage persists an Agent-authored Task discussion and its
// frozen fanout receipt in one transaction.
func (store *Store) SendAgentTaskScopeMessage(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.MessageDelivery, error) {
	result, err := store.SendAgentTaskScopeMessageWithFreshness(ctx, authorAgent, params)
	if err != nil {
		return message.MessageDelivery{}, err
	}
	if result.State == message.SendStateHeld {
		return message.MessageDelivery{}, badRequest("message send was held by freshness gate")
	}
	return result.Delivery, nil
}

// SendAgentTaskScopeMessageWithFreshness preserves the Task message freshness
// states while making message persistence, frozen recipient resolution, and
// outbox insertion atomic. Team-invoke routing is checked once before entering
// the write transaction for a cheap rejection and again while projecting the
// committed receipt so mutable team membership cannot bypass authorization.
func (store *Store) SendAgentTaskScopeMessageWithFreshness(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.SendMessageResult, error) {
	normalizedSource, err := params.Source.NormalizeForWrite()
	if err != nil {
		return message.SendMessageResult{}, mapMessagingError(err)
	}
	params.Source = normalizedSource
	params.TaskScopeID = strings.TrimSpace(params.TaskScopeID)
	if params.TaskScopeID == "" {
		return message.SendMessageResult{}, badRequest("task scope id is required")
	}
	if strings.TrimSpace(params.TargetRef) != "" || strings.TrimSpace(params.DMTo) != "" {
		return message.SendMessageResult{}, badRequest("task scope send does not support another message target")
	}
	params.Content = strings.TrimSpace(params.Content)
	params.AttachmentIDs, err = normalizeAttachmentIDs(params.AttachmentIDs)
	if err != nil {
		return message.SendMessageResult{}, err
	}
	if params.Content == "" && len(params.AttachmentIDs) == 0 {
		return message.SendMessageResult{}, badRequest("content is required")
	}
	if err := message.ValidateWorkflowContextMetadata(params.Metadata, false); err != nil {
		return message.SendMessageResult{}, badRequest(err.Error())
	}
	params.ClientMessageID, err = normalizeClientMessageID(params.ClientMessageID)
	if err != nil {
		return message.SendMessageResult{}, err
	}
	if params.ClientMessageID != "" {
		replayed, found, replayErr := store.loadAgentTaskScopeMessageReplay(ctx, authorAgent, params)
		if replayErr != nil {
			return message.SendMessageResult{}, replayErr
		}
		if found {
			return replayed, nil
		}
	}
	if err := store.PreflightTaskScopeTeamInvokeForAgent(ctx, authorAgent, params.TaskScopeID, params.Content, params.TeamInvokeTeamID); err != nil {
		return message.SendMessageResult{}, err
	}
	return store.persistAgentTaskScopeMessageWithFreshness(ctx, authorAgent, params)
}

func (store *Store) persistAgentTaskScopeMessageWithFreshness(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.SendMessageResult, error) {
	var result message.SendMessageResult
	var taskChange *taskboard.Task
	var workflowErr error
	for attempt := 0; attempt < 2; attempt++ {
		workflowErr = coordinator.New(store.db).Run(ctx, "send agent task scope message workflow", func(tx coredb.Tx) error {
			taskChange = nil
			response, sendErr := taskboard.New(store.db).InTx(tx).SendTaskScopeMessage(ctx, taskboard.SendTaskScopeMessageRequest{
				Context:     taskboard.NewAgentServiceContext(authorAgent.WorkspaceID, authorAgent.AgentID, authorAgent.MemberID),
				TaskScopeID: params.TaskScopeID,
				Message: taskboard.TaskScopeMessageInput{
					Content:       params.Content,
					AttachmentIDs: params.AttachmentIDs,
					Metadata:      message.CopyMessageMetadata(params.Metadata),
				},
				ClientMessageID: params.ClientMessageID,
				Freshness:       params.Freshness,
			})
			if sendErr != nil {
				return mapTaskScopeMessageError(sendErr)
			}
			result = taskScopeAgentSendResult(response, params.TaskScopeID)
			switch result.State {
			case message.SendStateHeld:
				return nil
			case message.SendStateSent, message.SendStateAlreadySent:
			default:
				return fmt.Errorf("task discussion message returned unsupported send state %q", result.State)
			}
			if result.MessageID == "" {
				return fmt.Errorf("task discussion message returned no message id")
			}
			if err := store.bindScheduleRunMessageSource(ctx, tx, authorAgent.WorkspaceID, params.Source, result.MessageID, response.Message.CreatedAt); err != nil {
				return err
			}
			if err := store.bindWorkflowRunMessageSource(ctx, tx, authorAgent.WorkspaceID, params.Source, result.MessageID, params.ClientMessageID, response.Message.CreatedAt); err != nil {
				return err
			}

			if result.State == message.SendStateAlreadySent {
				appMessage, delivery, replayErr := loadTaskScopeMessageReplay(ctx, tx, authorAgent.WorkspaceID, result.TargetKey, result.MessageID)
				if replayErr != nil {
					return replayErr
				}
				// The receipt is the immutable recipient snapshot. Marking the
				// replay prevents synchronous callers from duplicating fanout.
				delivery.IdempotentReplay = true
				delivery.AppMessage = &appMessage
				result.Delivery = delivery
				fillTaskScopeAgentSendResultFromDelivery(&result, delivery)
				return nil
			}

			var authorTargetID, authorDisplayName, authorMachineID string
			if err := tx.QueryRow(ctx, `
SELECT a.name, COALESCE(NULLIF(m.display_name, ''), a.name), a.machine_id
FROM agents a
JOIN members m ON m.id = a.member_id
WHERE a.workspace_id = $1 AND a.id = $2 AND a.member_id = $3
`, strings.TrimSpace(authorAgent.WorkspaceID), strings.TrimSpace(authorAgent.AgentID), strings.TrimSpace(authorAgent.MemberID)).Scan(
				&authorTargetID,
				&authorDisplayName,
				&authorMachineID,
			); err != nil {
				return fmt.Errorf("load task discussion agent author identity: %w", err)
			}
			appMessage, delivery, projectErr := store.projectTaskScopeDiscussionDeliveryTx(ctx, tx, taskScopeDiscussionDeliveryParams{
				WorkspaceID:       strings.TrimSpace(authorAgent.WorkspaceID),
				TaskScopeID:       result.TargetKey,
				MessageID:         result.MessageID,
				AuthorMemberID:    strings.TrimSpace(authorAgent.MemberID),
				AuthorType:        domain.MemberTypeAgent,
				AuthorPrincipalID: strings.TrimSpace(authorAgent.AgentID),
				AuthorTargetID:    authorTargetID,
				AuthorDisplayName: authorDisplayName,
				AuthorMachineID:   authorMachineID,
				TeamInvokeTeamID:  params.TeamInvokeTeamID,
				Source:            params.Source,
			})
			if projectErr != nil {
				return projectErr
			}
			delivery.AppMessage = &appMessage
			result.Delivery = delivery
			fillTaskScopeAgentSendResultFromDelivery(&result, delivery)
			task := response.Task
			taskChange = &task
			return nil
		})
		if workflowErr == nil {
			if taskChange != nil {
				store.emitTaskScopeMessageUpdated(ctx, taskboard.NewAgentServiceContext(authorAgent.WorkspaceID, authorAgent.AgentID, authorAgent.MemberID), *taskChange)
			}
			return result, nil
		}
		if params.ClientMessageID == "" || !uniqueViolationMatches(workflowErr, "scope_messages_client_message_id_key") || attempt > 0 {
			break
		}
	}
	return message.SendMessageResult{}, mapTaskScopeMessageError(workflowErr)
}

// loadAgentTaskScopeMessageReplay resolves an immutable command replay before
// mutable Team routing preflight. Current Task write authority is still
// required, but recipient membership is restored only from the durable outbox
// receipt written by the original transaction.
func (store *Store) loadAgentTaskScopeMessageReplay(ctx context.Context, authorAgent agent.AuthenticatedAgent, params message.SendMessageParams) (message.SendMessageResult, bool, error) {
	encodedMetadata, _, err := message.EncodeMessageMetadata(params.Metadata)
	if err != nil {
		return message.SendMessageResult{}, false, err
	}
	requestedDigest, err := message.ComputeMessageCreatePayloadDigest(message.MessageCreateDigestInput{
		MessageType:   message.MessageTypeTaskDiscussion,
		Content:       params.Content,
		AttachmentIDs: params.AttachmentIDs,
		Metadata:      encodedMetadata,
	})
	if err != nil {
		return message.SendMessageResult{}, false, err
	}

	var result message.SendMessageResult
	found := false
	err = coredb.WithTx(ctx, store.db, func(tx coredb.Tx) error {
		if err := taskboard.New(store.db).InTx(tx).AuthorizeAgentTaskScopeDiscussionWrite(ctx, taskboard.AgentActor{
			WorkspaceID: strings.TrimSpace(authorAgent.WorkspaceID),
			MemberID:    strings.TrimSpace(authorAgent.MemberID),
		}, params.TaskScopeID); err != nil {
			return mapTaskScopeMessageError(err)
		}
		record, err := message.NewScopeRepositories(tx).Messages.LoadByClientMessageID(
			ctx,
			params.TaskScopeID,
			authorAgent.MemberID,
			authorAgent.MemberID,
			params.ClientMessageID,
		)
		if errors.Is(err, message.ErrNotFound) {
			return nil
		}
		if err != nil {
			return err
		}
		if strings.TrimSpace(record.WorkspaceID) != strings.TrimSpace(authorAgent.WorkspaceID) ||
			strings.TrimSpace(record.MessageType) != message.MessageTypeTaskDiscussion {
			return conflict("clientMessageId was already used with a different message payload")
		}
		if !strings.EqualFold(strings.TrimSpace(record.IdempotencyPayloadDigest), strings.TrimSpace(requestedDigest)) {
			return conflict("clientMessageId was already used with a different message payload")
		}
		appMessage, delivery, err := loadTaskScopeMessageReplay(ctx, tx, authorAgent.WorkspaceID, params.TaskScopeID, record.ID)
		if err != nil {
			return err
		}
		delivery.IdempotentReplay = true
		delivery.AppMessage = &appMessage
		result = message.SendMessageResult{
			State:           message.SendStateAlreadySent,
			MessageID:       record.ID,
			TargetKey:       params.TaskScopeID,
			ClientMessageID: params.ClientMessageID,
			SeenUpToSeq:     params.Freshness.SeenUpToSeq,
			AcceptedSeq:     record.Seq,
			LatestSeq:       record.Seq,
			Delivery:        delivery,
		}
		found = true
		return nil
	})
	if err != nil {
		return message.SendMessageResult{}, false, err
	}
	return result, found, nil
}

func taskScopeAgentSendResult(response taskboard.SendTaskScopeMessageResponse, requestedTaskScopeID string) message.SendMessageResult {
	taskScopeID := strings.TrimSpace(response.Task.TaskScopeID)
	if taskScopeID == "" {
		taskScopeID = strings.TrimSpace(requestedTaskScopeID)
	}
	messageID := strings.TrimSpace(response.MessageID)
	if messageID == "" {
		messageID = strings.TrimSpace(response.Message.ID)
	}
	return message.SendMessageResult{
		State:                firstNonEmpty(response.State, message.SendStateSent),
		MessageID:            messageID,
		TargetKey:            firstNonEmpty(response.TargetKey, taskScopeID),
		ClientMessageID:      response.ClientMessageID,
		SeenUpToSeq:          response.SeenUpToSeq,
		AcceptedSeq:          response.AcceptedSeq,
		LatestSeq:            response.LatestSeq,
		NeedsRefreshUntilSeq: response.NeedsRefreshUntilSeq,
	}
}

func fillTaskScopeAgentSendResultFromDelivery(result *message.SendMessageResult, delivery message.MessageDelivery) {
	if result == nil {
		return
	}
	result.MessageID = firstNonEmpty(delivery.MessageID, result.MessageID)
	if result.AcceptedSeq == 0 {
		result.AcceptedSeq = delivery.Seq
	}
	if result.LatestSeq == 0 {
		result.LatestSeq = delivery.Seq
	}
	if result.ClientMessageID == "" {
		result.ClientMessageID = delivery.ClientMessageID
	}
}
