package app

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
	"github.com/tidbcloud/AgentTeam/internal/store/tasktimeline"
)

// serviceEventTaskDiscussionAppendOptions carries the authoring facts for one
// service/app-authored Task discussion append. It mirrors the producer surface
// of the shared Runtime App/App seam so provider events and external App
// runtime replies declare the same durable service author identity, notify
// principal, idempotency subject/client facts, and contribution source.
type serviceEventTaskDiscussionAppendOptions struct {
	AuthorMemberID             string
	AuthorPrincipalID          string
	AuthorTargetID             string
	AuthorDisplayName          string
	AuthorServiceSlug          string
	AuthorServiceBuiltin       bool
	NotifyPrincipalMemberID    string
	ClientMessageID            string
	IdempotencySubjectMemberID string
	IdempotencyPayloadDigest   string
	Source                     message.ContributionSource
	Metadata                   map[string]any
}

// appendServiceEventTaskDiscussionTx is the canonical service/app-authored Task
// discussion append seam. Provider service events (GitHub/Gitea) and external
// App runtime mention/reply effects both append a visible task_discussion
// scope message through the shared task scope discussion delivery boundary
// (projectTaskScopeDiscussionDeliveryTx) with an ordinary scope_fanout outbox,
// frozen targeted Human/Agent recipients, notify-principal and idempotency
// subject/client facts, the provider/App contribution source, search/sync,
// task discussion_count/last_activity, and the discussion timeline.
//
// The caller owns the post-commit fanout decision: provider webhook deliveries
// are caller-owned (the delivery is returned through AppWebhookResult.Deliveries
// and existing FollowupDeliverer performs the first fanout), while external App
// runtime replies prepare a store-owned recoverable post-commit token and call
// DeliverCommittedMessage only after commit.
func appendServiceEventTaskDiscussionTx(
	ctx context.Context,
	store *Store,
	tx coredb.Tx,
	workspaceID string,
	binding serviceEventDeliveryTargetBinding,
	content string,
	options serviceEventTaskDiscussionAppendOptions,
	now time.Time,
) (message.AppMessage, message.MessageDelivery, error) {
	delivery, err := lockServiceEventTaskDiscussionDelivery(ctx, tx, workspaceID, binding)
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	normalizedSource, err := options.Source.NormalizeForWrite()
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	options.Source = normalizedSource
	options.AuthorMemberID = strings.TrimSpace(options.AuthorMemberID)
	if options.AuthorMemberID == "" {
		options.AuthorMemberID = delivery.appMemberID
	}

	plan, messageID, err := buildServiceEventTaskMessagePlan(ctx, tx, delivery, content, options, now)
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	applied, err := message.ApplyMessagePlanTx(ctx, tx, plan)
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, fmt.Errorf("apply service event task scope discussion message: %w", err)
	}
	if !applied.Message.Inserted {
		// The same client command already authored this message. Return the frozen
		// outbox receipt without re-touching task counters, timeline, or the
		// outbox payload.
		replayed, replayDelivery, replayErr := loadTaskScopeMessageReplay(
			ctx,
			tx,
			delivery.workspaceID,
			delivery.taskScopeID,
			applied.Message.Message.ID,
		)
		if replayErr != nil {
			return message.AppMessage{}, message.MessageDelivery{}, replayErr
		}
		if !options.Source.IsZero() {
			replayed.Source = options.Source.Normalize()
			replayDelivery.Message.Source = options.Source.Normalize()
		}
		replayDelivery.Recipients = nil
		replayDelivery.WakeRecipients = nil
		replayDelivery.IdempotentReplay = true
		replayDelivery.AppMessage = &replayed
		return replayed, replayDelivery, nil
	}
	if err := delivery.touchTaskAfterDiscussion(ctx, tx, now); err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	if err := delivery.writeTimelinePlan(ctx, tx, "discussion_added", messageID, now); err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	appMessage, taskDelivery, err := store.projectTaskScopeDiscussionDeliveryTx(ctx, tx, taskScopeDiscussionDeliveryParams{
		WorkspaceID:             delivery.workspaceID,
		TaskScopeID:             delivery.taskScopeID,
		MessageID:               messageID,
		AuthorMemberID:          options.AuthorMemberID,
		AuthorType:              domain.MemberTypeService,
		AuthorPrincipalID:       firstNonEmpty(options.AuthorPrincipalID, options.AuthorMemberID),
		AuthorTargetID:          options.AuthorTargetID,
		AuthorDisplayName:       options.AuthorDisplayName,
		AuthorServiceKind:       message.AuthorServiceKindApp,
		AuthorServiceSlug:       options.AuthorServiceSlug,
		AuthorServiceBuiltin:    options.AuthorServiceBuiltin,
		NotifyPrincipalMemberID: options.NotifyPrincipalMemberID,
		Source:                  options.Source,
	})
	if err != nil {
		return message.AppMessage{}, message.MessageDelivery{}, err
	}
	return appMessage, taskDelivery, nil
}

func buildServiceEventTaskMessagePlan(
	ctx context.Context,
	tx coredb.Tx,
	delivery serviceEventTaskDiscussionDelivery,
	content string,
	options serviceEventTaskDiscussionAppendOptions,
	now time.Time,
) (message.MessagePlan, string, error) {
	var emptyPlan message.MessagePlan
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return emptyPlan, "", fmt.Errorf("generate service event task discussion message id: %w", err)
	}
	seq, err := message.AllocateNextScopeMessageSeq(ctx, tx, delivery.taskScopeID)
	if err != nil {
		return emptyPlan, "", err
	}
	encodedMetadata, err := encodeTaskScopeDiscussionMetadata(options.Metadata)
	if err != nil {
		return emptyPlan, "", err
	}
	// Idempotency describes the client-owned command, not the current result of
	// mutable mention resolution. Compute the durable message digest before
	// routing so a replayed App command resolves to the exact same message.
	messageCreateDigest := strings.TrimSpace(options.IdempotencyPayloadDigest)
	if strings.TrimSpace(options.ClientMessageID) != "" && messageCreateDigest == "" {
		messageCreateDigest, err = message.ComputeMessageCreatePayloadDigest(message.MessageCreateDigestInput{
			MessageType:   message.MessageTypeTaskDiscussion,
			Content:       strings.TrimSpace(content),
			AttachmentIDs: nil,
			Metadata:      encodedMetadata,
		})
		if err != nil {
			return emptyPlan, "", err
		}
	}
	plan := message.MessagePlan{
		SideEffectProfile: message.MessagePlanSideEffectProfileScopeFanout,
		MessageWrite: message.ScopeMessageRecord{
			ID:                         messageID,
			WorkspaceID:                delivery.workspaceID,
			ScopeID:                    delivery.taskScopeID,
			Seq:                        seq,
			WorkspaceGlobalSeq:         seq,
			AuthorMemberID:             options.AuthorMemberID,
			AuthorType:                 string(domain.MemberTypeService),
			NotifyPrincipalMemberID:    firstNonEmpty(options.NotifyPrincipalMemberID, options.AuthorMemberID),
			IdempotencySubjectMemberID: firstNonEmpty(options.IdempotencySubjectMemberID, options.AuthorMemberID),
			ClientMessageID:            strings.TrimSpace(options.ClientMessageID),
			IdempotencyPayloadDigest:   messageCreateDigest,
			MessageType:                message.MessageTypeTaskDiscussion,
			Content:                    strings.TrimSpace(content),
			Metadata:                   encodedMetadata,
			CreatedAt:                  now,
			UpdatedAt:                  now,
		},
		OutboxEvents: []message.ScopeFanoutOutboxRecord{
			message.BuildScopeMessageFanoutOutboxRecord(message.ScopeMessageFanoutOutboxRecordParams{
				WorkspaceID:           delivery.workspaceID,
				ScopeID:               delivery.taskScopeID,
				MessageID:             messageID,
				TargetedHumanDelivery: true,
				CreatedAt:             now,
				NextAttemptAt:         now.Add(taskScopeFanoutInlineDeliveryGrace),
			}),
		},
	}
	return plan, messageID, nil
}

type serviceEventTaskDiscussionDelivery struct {
	workspaceID string
	taskID      string
	taskScopeID string
	appMemberID string
}

func lockServiceEventTaskDiscussionDelivery(ctx context.Context, execer coredb.QueryRower, workspaceID string, binding serviceEventDeliveryTargetBinding) (serviceEventTaskDiscussionDelivery, error) {
	taskID := strings.TrimSpace(binding.TargetRef)
	appMemberID, err := loadServiceEventVisibleMemberID(ctx, execer, binding)
	if err != nil {
		return serviceEventTaskDiscussionDelivery{}, err
	}
	if taskID == "" || appMemberID == "" {
		return serviceEventTaskDiscussionDelivery{}, badRequest("service event task delivery target is invalid")
	}
	row := execer.QueryRow(ctx, `
SELECT
    t.id,
    t.task_scope_id,
    tb.channel_id
FROM tasks t
JOIN task_boards tb
  ON tb.id = t.board_id
 AND tb.workspace_id = $1
JOIN scopes task_scope
  ON task_scope.id = t.task_scope_id
 AND task_scope.workspace_id = $1
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
JOIN scopes task_channel
  ON task_channel.id = tb.channel_id
 AND task_channel.workspace_id = $1
 AND task_channel.type = 'channel'
 AND task_channel.status = 'active'
 AND task_channel.archived_at IS NULL
WHERE t.channel_id = tb.channel_id
  AND t.id = $2
  AND t.task_scope_id = $3
  AND tb.channel_id = $4
FOR UPDATE
`, strings.TrimSpace(workspaceID), taskID, strings.TrimSpace(binding.DeliveryScopeID), strings.TrimSpace(binding.PermissionCheckScopeID))
	var resolvedTaskID string
	var taskScopeID string
	var channelID string
	if err := row.Scan(&resolvedTaskID, &taskScopeID, &channelID); err != nil {
		if coredb.IsNotFound(err) {
			return serviceEventTaskDiscussionDelivery{}, storeerr.ErrNotFound
		}
		return serviceEventTaskDiscussionDelivery{}, fmt.Errorf("load service event task delivery target: %w", err)
	}
	return serviceEventTaskDiscussionDelivery{
		workspaceID: strings.TrimSpace(workspaceID),
		taskID:      strings.TrimSpace(resolvedTaskID),
		taskScopeID: strings.TrimSpace(taskScopeID),
		appMemberID: strings.TrimSpace(appMemberID),
	}, nil
}

func encodeTaskScopeDiscussionMetadata(metadata map[string]any) ([]byte, error) {
	if len(metadata) == 0 {
		return nil, nil
	}
	encoded, _, err := message.EncodeMessageMetadata(metadata)
	if err != nil {
		return nil, err
	}
	return encoded, nil
}

func (delivery serviceEventTaskDiscussionDelivery) touchTaskAfterDiscussion(ctx context.Context, execer coredb.QueryExecer, now time.Time) error {
	if _, err := execer.Exec(ctx, `
UPDATE tasks
SET discussion_count = discussion_count + 1,
    last_activity_at = $2,
    updated_at = $2
WHERE id = $1
`, delivery.taskID, now); err != nil {
		return fmt.Errorf("touch service event task after discussion: %w", err)
	}
	return nil
}

func (delivery serviceEventTaskDiscussionDelivery) writeTimelinePlan(ctx context.Context, execer coredb.Tx, eventType string, eventPayload string, now time.Time) error {
	intent := tasktimeline.NewTypedActivityIntent(delivery.taskID, delivery.appMemberID, eventType, eventPayload, now)
	intent.ActorParticipantMode = tasktimeline.ActorParticipantModeNone
	if eventType == "discussion_added" {
		intent.SourceMessageID = eventPayload
	}
	if err := tasktimeline.WritePlan(ctx, execer, tasktimeline.TaskTimelinePlan{
		TypedActivityIntents: []tasktimeline.TypedActivityIntent{intent},
	}); err != nil {
		return fmt.Errorf("write service event task timeline plan: %w", err)
	}
	return nil
}
