package app

import (
	"context"
	"database/sql"
	"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/access"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
)

type channelMemberEventTarget struct {
	Kind        string
	MemberID    string
	DisplayName string
}

// channelMemberEventActor is the canonical server-derived event author profile.
// Callers only supply a memberID; the type, principal, display name, and avatar
// are all derived from active member state so no caller can spoof the author.
type channelMemberEventActor struct {
	MemberID    string
	Type        string
	PrincipalID string
	DisplayName string
	AvatarURL   string
}

// resolveChannelMemberEventActor server-derives the event author profile from
// actorMemberID on the caller's transaction. Only active human/agent members
// are accepted; the canonical principal (user_id for human, agent_id for agent)
// must be non-empty (fail-closed), otherwise a stable validation error is
// returned — no system message or projection is written for an untraceable
// author. Display name preserves the legacy fallback chain.
func (store *Store) resolveChannelMemberEventActor(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string) (channelMemberEventActor, error) {
	row := queryer.QueryRow(ctx, `
SELECT m.type,
       COALESCE(NULLIF(u.display_name, ''), NULLIF(m.display_name, ''), NULLIF(u.name, ''), m.id),
       COALESCE(NULLIF(m.display_name, ''), NULLIF(a.name, ''), m.id),
       COALESCE(u.avatar_url, ''),
       COALESCE(a.avatar_url, ''),
       COALESCE(m.user_id, ''),
       COALESCE(a.id, '')
FROM members m
LEFT JOIN users u
  ON m.type = 'human'
 AND u.id = m.user_id
LEFT JOIN agents a
  ON m.type = 'agent'
 AND a.workspace_id = m.workspace_id
 AND a.member_id = m.id
 AND a.status <> 'archived'
WHERE m.workspace_id = $1
  AND m.id = $2
  AND m.status = 'active'
`, strings.TrimSpace(workspaceID), strings.TrimSpace(memberID))

	var (
		memberType, humanDisplayName, agentDisplayName, userAvatar, agentAvatar, userID, agentID string
	)
	if err := row.Scan(&memberType, &humanDisplayName, &agentDisplayName, &userAvatar, &agentAvatar, &userID, &agentID); err != nil {
		if coredb.IsNotFound(err) {
			return channelMemberEventActor{}, badRequest("channel member event actor is not an active member")
		}
		return channelMemberEventActor{}, fmt.Errorf("resolve channel member event actor: %w", err)
	}
	memberType = strings.TrimSpace(memberType)
	mt := domain.MemberType(memberType)
	actor := channelMemberEventActor{MemberID: strings.TrimSpace(memberID), Type: memberType}
	switch mt {
	case domain.MemberTypeHuman:
		actor.PrincipalID = strings.TrimSpace(userID)
		actor.DisplayName = preferredDisplayName(humanDisplayName, memberID)
		actor.AvatarURL = strings.TrimSpace(userAvatar)
	case domain.MemberTypeAgent:
		actor.PrincipalID = strings.TrimSpace(agentID)
		actor.DisplayName = preferredDisplayName(agentDisplayName, memberID)
		actor.AvatarURL = strings.TrimSpace(agentAvatar)
	default:
		return channelMemberEventActor{}, badRequest("channel member event actor must be an active human or agent member")
	}
	if actor.PrincipalID == "" {
		return channelMemberEventActor{}, badRequest("channel member event actor has no canonical principal")
	}
	return actor, nil
}

func (store *Store) channelMemberEventTargetByMemberID(ctx context.Context, queryer coredb.QueryRower, workspaceID string, targetKind string, memberID string) (channelMemberEventTarget, error) {
	trimmedKind := strings.TrimSpace(targetKind)
	trimmedMemberID := strings.TrimSpace(memberID)
	row := queryer.QueryRow(ctx, `
SELECT
    m.id,
    COALESCE(
      NULLIF(u.display_name, ''),
      NULLIF(m.display_name, ''),
      NULLIF(u.name, ''),
      NULLIF(a.name, ''),
      NULLIF(definition.name, ''),
      NULLIF(service_member.handle, ''),
      m.id
    )
FROM members m
LEFT JOIN users u
  ON m.type = 'human'
 AND u.id = m.user_id
LEFT JOIN agents a
  ON m.type = 'agent'
 AND a.member_id = m.id
LEFT JOIN service_members service_member
  ON m.type = 'service'
 AND service_member.workspace_id = m.workspace_id
 AND service_member.member_id = m.id
 AND service_member.service_kind = 'app'
LEFT JOIN service_installations service_installation
  ON service_installation.id = service_member.service_installation_id
 AND service_installation.service_kind = service_member.service_kind
LEFT JOIN service_definitions definition
  ON definition.id = service_member.service_definition_id
 AND definition.service_kind = service_member.service_kind
WHERE m.workspace_id = $1
  AND m.id = $2
  AND m.type = $3
  AND m.status = 'active'
`, strings.TrimSpace(workspaceID), trimmedMemberID, trimmedKind)

	var target channelMemberEventTarget
	if err := row.Scan(&target.MemberID, &target.DisplayName); err != nil {
		if coredb.IsNotFound(err) {
			return channelMemberEventTarget{}, ErrNotFound
		}
		return channelMemberEventTarget{}, fmt.Errorf("load channel member event target: %w", err)
	}
	target.Kind = trimmedKind
	target.DisplayName = preferredDisplayName(target.DisplayName, target.MemberID)
	return target, nil
}

func activeChannelMemberEventMembership(ctx context.Context, queryer coredb.QueryRower, channelID string, memberID string, visibility string, ownerMemberID string) (bool, error) {
	trimmedVisibility := strings.TrimSpace(visibility)
	if trimmedVisibility == "" {
		trimmedVisibility = "public"
	}
	query := `
SELECT 1
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND visibility = $3
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
`
	args := []any{strings.TrimSpace(channelID), strings.TrimSpace(memberID), trimmedVisibility}
	if trimmedOwnerMemberID := strings.TrimSpace(ownerMemberID); trimmedOwnerMemberID != "" {
		query += "  AND COALESCE(owner_member_id, '') = $4\n"
		args = append(args, trimmedOwnerMemberID)
	}
	query += "ORDER BY effective_from_seq DESC\nLIMIT 1\nFOR UPDATE\n"
	row := queryer.QueryRow(ctx, query, args...)
	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return false, nil
		}
		return false, fmt.Errorf("check active channel member event membership: %w", err)
	}
	return true, nil
}

func (store *Store) ensurePrivateChannelAgentEntryProjection(ctx context.Context, tx coredb.Tx, workspaceID string, channelID string, agent agentEntryOwner, createdAt time.Time) error {
	if _, err := store.ensureChannelScopeProjection(ctx, tx, channelID); err != nil {
		return err
	}
	entry, ok, err := store.loadActiveChannelAgentEntry(ctx, tx, channelID, agent.AgentMemberID)
	if err != nil {
		return err
	}
	if ok {
		if strings.TrimSpace(entry.Visibility) != channelAgentEntryVisibilityPrivate {
			return conflict("agent already has an active public entry in this channel")
		}
		if strings.TrimSpace(entry.OwnerMemberID.String) != strings.TrimSpace(agent.OwnerMemberID) {
			return conflict("agent already has an active private entry for another owner")
		}
	} else if err := store.ensureMemberInWorkspace(ctx, tx, workspaceID, agent.AgentMemberID); err != nil {
		return err
	}
	if err := message.EnsureScopeMembership(ctx, tx, strings.TrimSpace(channelID), strings.TrimSpace(agent.AgentMemberID), channelAgentEntryVisibilityPrivate, strings.TrimSpace(agent.OwnerMemberID), "", 0, 0, createdAt.UTC()); err != nil {
		return fmt.Errorf("insert private channel agent scope membership: %w", err)
	}
	return nil
}

// appendChannelMemberEventMessage resolves the actor from actorMemberID and
// persists the canonical system-message projection in the caller transaction.
func (store *Store) appendChannelMemberEventMessage(ctx context.Context, tx coredb.Tx, workspaceID string, channelAccess access.HumanChannelAccess, actorMemberID string, action string, target channelMemberEventTarget, allTargets ...channelMemberEventTarget) (message.ChannelMemberEventResult, error) {
	actor, err := store.resolveChannelMemberEventActor(ctx, tx, workspaceID, actorMemberID)
	if err != nil {
		return message.ChannelMemberEventResult{}, err
	}

	trimmedAction := strings.TrimSpace(action)
	channelID := strings.TrimSpace(channelAccess.Channel.ChannelID)
	channelDisplayName := preferredDisplayName(channelAccess.Channel.DisplayName, channelAccess.Channel.Slug, channelID)
	content := channelMemberEventFallbackContent(trimmedAction, actor.DisplayName, target.DisplayName)
	eventMetadata := map[string]any{
		"action":             trimmedAction,
		"actorMemberId":      actorMemberID,
		"actorDisplayName":   actor.DisplayName,
		"targetKind":         strings.TrimSpace(target.Kind),
		"targetMemberId":     strings.TrimSpace(target.MemberID),
		"targetDisplayName":  strings.TrimSpace(target.DisplayName),
		"channelId":          channelID,
		"channelDisplayName": channelDisplayName,
	}
	if targets := normalizeChannelMemberEventTargetsForMetadata(allTargets, target); len(targets) > 1 {
		eventMetadata["targets"] = targets
	}
	metadata := map[string]any{
		message.ChannelMemberEventMetadataKey: eventMetadata,
	}
	encodedMetadata, normalizedMetadata, err := message.EncodeMessageMetadata(metadata)
	if err != nil {
		return message.ChannelMemberEventResult{}, err
	}

	threadID := strings.TrimSpace(channelAccess.Channel.ThreadID)
	if threadID == "" {
		threadID = channelID
	}
	targetAccess := message.ThreadAccess{
		ThreadID:    threadID,
		ThreadType:  domain.ThreadTypeChannel,
		ChannelID:   channelID,
		ChannelName: channelDisplayName,
	}

	scopeSeq, err := message.AllocateNextScopeMessageSeq(ctx, tx, channelID)
	if err != nil {
		return message.ChannelMemberEventResult{}, err
	}
	createdAt, err := channelMemberEventMessageCreatedAt(ctx, tx, channelID, time.Now().UTC())
	if err != nil {
		return message.ChannelMemberEventResult{}, err
	}
	messageID, err := identifiers.NewMessageID()
	if err != nil {
		return message.ChannelMemberEventResult{}, fmt.Errorf("generate channel member event message id: %w", err)
	}
	plan := message.MessagePlan{
		SideEffectProfile: message.MessagePlanSideEffectProfileNoOutbox,
		MessageWrite: message.ScopeMessageRecord{
			ID:                 messageID,
			WorkspaceID:        strings.TrimSpace(workspaceID),
			ScopeID:            channelID,
			Seq:                scopeSeq,
			WorkspaceGlobalSeq: scopeSeq,
			AuthorMemberID:     actorMemberID,
			AuthorType:         actor.Type,
			MessageType:        message.MessageTypeSystem,
			Content:            content,
			Metadata:           encodedMetadata,
			CreatedAt:          createdAt,
			UpdatedAt:          createdAt,
		},
	}
	applied, err := message.ApplyMessagePlanTx(ctx, tx, plan)
	if err != nil {
		return message.ChannelMemberEventResult{}, err
	}

	authorProfile := message.AuthorProfile{
		ActorType:   actor.Type,
		PrincipalID: actor.PrincipalID,
		MemberID:    strings.TrimSpace(actorMemberID),
		TargetID:    strings.TrimSpace(actorMemberID),
		DisplayName: actor.DisplayName,
		AvatarURL:   actor.AvatarURL,
	}
	delivery := message.MessageDelivery{
		ThreadID:       targetAccess.ThreadID,
		Seq:            applied.Message.Message.Seq,
		GlobalSeq:      applied.Message.Message.WorkspaceGlobalSeq,
		MessageID:      applied.Message.Message.ID,
		Message:        channelMemberEventReceiveMessage(targetAccess, applied.Message.Message, authorProfile, normalizedMetadata),
		MentionContext: message.MentionContextForTarget(targetAccess),
	}
	appMessage := appMessageFromDelivery(channelID, authorProfile, delivery)
	delivery.AppMessage = &appMessage
	result := message.ChannelMemberEventResult{Changed: true, ChangedCount: 1, Message: appMessage, Delivery: delivery}
	if memberID := strings.TrimSpace(target.MemberID); memberID != "" {
		result.AffectedMemberIDs = []string{memberID}
	}
	return result, nil
}

func channelMemberEventReceiveMessage(target message.ThreadAccess, record message.ScopeMessageRecord, authorProfile message.AuthorProfile, metadata map[string]any) message.ReceiveMessage {
	return message.ReceiveMessage{
		MessageID:          record.ID,
		TargetKey:          record.ScopeID,
		TargetSeq:          record.Seq,
		ChannelType:        string(domain.ThreadTypeChannel),
		ChannelName:        target.ChannelName,
		ChannelDisplayName: target.ChannelName,
		AuthorProfile:      authorProfile.Normalize(),
		MessageType:        record.MessageType,
		Content:            record.Content,
		Metadata:           message.CopyMessageMetadata(metadata),
		Timestamp:          record.CreatedAt,
	}
}

func channelMemberEventMessageCreatedAt(ctx context.Context, tx coredb.Tx, scopeID string, requestedCreatedAt time.Time) (time.Time, error) {
	createdAt := requestedCreatedAt
	if createdAt.IsZero() {
		createdAt = time.Now().UTC()
	} else {
		createdAt = createdAt.UTC()
	}

	var latestMessageAt sql.NullTime
	if err := tx.QueryRow(ctx, `
SELECT latest_message_at
FROM scopes
WHERE id = $1
`, strings.TrimSpace(scopeID)).Scan(&latestMessageAt); err != nil {
		return time.Time{}, fmt.Errorf("load channel member event latest message time: %w", err)
	}
	if latestMessageAt.Valid {
		latest := latestMessageAt.Time.UTC()
		if createdAt.Before(latest) {
			return latest, nil
		}
	}
	return createdAt, nil
}

func channelMemberEventFallbackContent(action string, actorDisplayName string, targetDisplayName string) string {
	actor := channelMemberEventFallbackName(preferredDisplayName(actorDisplayName, "Someone"))
	target := channelMemberEventFallbackName(preferredDisplayName(targetDisplayName, "someone"))
	switch strings.TrimSpace(action) {
	case message.ChannelMemberEventActionRemoved:
		return fmt.Sprintf("%s removed %s from the channel", actor, target)
	case message.ChannelMemberEventActionUpdated:
		return fmt.Sprintf("%s updated %s in the channel", actor, target)
	default:
		return fmt.Sprintf("%s added %s to the channel", actor, target)
	}
}

func channelMemberEventFallbackName(name string) string {
	return strings.ReplaceAll(strings.TrimSpace(name), "@", "＠")
}

// normalizeChannelMemberEventTargetsForMetadata collapses the raw target list
// for inclusion in event metadata. Returns nil when there is nothing to expand,
// so callers can decide whether to attach the `targets` field at all.
func normalizeChannelMemberEventTargetsForMetadata(allTargets []channelMemberEventTarget, fallback channelMemberEventTarget) []map[string]string {
	if len(allTargets) == 0 {
		return nil
	}
	seen := make(map[string]struct{}, len(allTargets))
	out := make([]map[string]string, 0, len(allTargets))
	for _, target := range allTargets {
		memberID := strings.TrimSpace(target.MemberID)
		if memberID == "" {
			continue
		}
		if _, ok := seen[memberID]; ok {
			continue
		}
		seen[memberID] = struct{}{}
		out = append(out, map[string]string{
			"kind":        strings.TrimSpace(target.Kind),
			"memberId":    memberID,
			"displayName": strings.TrimSpace(target.DisplayName),
		})
	}
	if len(out) == 0 {
		fallbackMemberID := strings.TrimSpace(fallback.MemberID)
		if fallbackMemberID == "" {
			return nil
		}
		return []map[string]string{{
			"kind":        strings.TrimSpace(fallback.Kind),
			"memberId":    fallbackMemberID,
			"displayName": strings.TrimSpace(fallback.DisplayName),
		}}
	}
	return out
}
