package app

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

const agentThreadSessionChannelDisplayName = "Agent Sessions"

func (store *Store) ResolveAgentThreadSessionContextForAgent(ctx context.Context, authenticated agent.AuthenticatedAgent, threadChannelID string) (message.AgentThreadSessionContext, error) {
	return store.ResolveAgentThreadTargetContextForAgent(ctx, authenticated, message.AgentThreadTarget{TargetKey: threadChannelID})
}

func (store *Store) ResolveAgentThreadTargetContextForAgent(ctx context.Context, authenticated agent.AuthenticatedAgent, target message.AgentThreadTarget) (message.AgentThreadSessionContext, error) {
	workspaceID := strings.TrimSpace(authenticated.WorkspaceID)
	target.TargetKey = strings.TrimSpace(target.TargetKey)
	target.TargetType = strings.TrimSpace(target.TargetType)
	target.MessageID = strings.TrimSpace(target.MessageID)
	if workspaceID == "" || (target.TargetKey == "" && target.MessageID == "") {
		return message.AgentThreadSessionContext{}, badRequest("agent target is required")
	}
	if target.TargetKey != "" {
		attestation, err := store.resolveHiddenAgentThreadSessionContextForAgent(ctx, authenticated, workspaceID, target.TargetKey)
		if err == nil {
			attestation.TargetKey = target.TargetKey
			attestation.TargetType = "thread"
			attestation.MessageID = attestation.RootMessageID
			return attestation, nil
		}
		if !errors.Is(err, ErrNotFound) {
			return message.AgentThreadSessionContext{}, err
		}
	}
	if target.MessageID != "" {
		return store.resolveReadableAgentTargetContext(ctx, authenticated, workspaceID, target)
	}
	if target.TargetType != "" && target.TargetType != "thread" {
		return message.AgentThreadSessionContext{}, badRequest("message_id is required for non-thread agent targets")
	}
	return store.resolveReadableReplyThreadSessionContextForAgent(ctx, authenticated, workspaceID, target.TargetKey)
}

func (store *Store) resolveReadableAgentTargetContext(ctx context.Context, authenticated agent.AuthenticatedAgent, workspaceID string, target message.AgentThreadTarget) (message.AgentThreadSessionContext, error) {
	if strings.TrimSpace(authenticated.MemberID) == "" {
		return message.AgentThreadSessionContext{}, forbidden("agent target attestation requires an agent member")
	}
	readable, err := message.NewScopeAPIService(store.messageStore()).CanReadMessage(ctx, message.ScopeAPIReadCheckRequest{
		WorkspaceID:    workspaceID,
		MessageID:      target.MessageID,
		ViewerMemberID: authenticated.MemberID,
		ViewerType:     message.ScopeActorAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
	})
	if err != nil {
		return message.AgentThreadSessionContext{}, err
	}
	if !readable.Allowed || readable.Message == nil {
		return message.AgentThreadSessionContext{}, forbidden("agent cannot read the current target message")
	}
	messageTargetKey := strings.TrimSpace(readable.Message.TargetKey)
	if messageTargetKey == "" {
		return message.AgentThreadSessionContext{}, forbidden("current message target is unavailable")
	}
	if target.TargetKey == "" {
		target.TargetKey = messageTargetKey
	}
	scope, err := message.NewScopeRepositories(store.db).Scopes.LoadScope(ctx, target.TargetKey)
	if err != nil {
		if errors.Is(err, message.ErrNotFound) {
			return message.AgentThreadSessionContext{}, ErrNotFound
		}
		return message.AgentThreadSessionContext{}, err
	}
	if strings.TrimSpace(scope.WorkspaceID) != workspaceID || strings.TrimSpace(scope.Status) != "active" || scope.ArchivedAt != nil {
		return message.AgentThreadSessionContext{}, ErrNotFound
	}
	scopeType := strings.TrimSpace(scope.Type)
	if !supportedAgentTargetType(scopeType) {
		return message.AgentThreadSessionContext{}, badRequest("agent target type is not supported")
	}
	if target.TargetType != "" && target.TargetType != scopeType {
		return message.AgentThreadSessionContext{}, forbidden("agent target type does not match the current message scope")
	}
	if messageTargetKey != target.TargetKey {
		return message.AgentThreadSessionContext{}, forbidden("current message does not belong to the agent target")
	}
	author := readable.Message.AuthorProfile.Normalize()
	if author.ActorType != message.ScopeActorHuman || strings.TrimSpace(author.PrincipalID) == "" || strings.TrimSpace(author.MemberID) == "" {
		return message.AgentThreadSessionContext{}, forbidden("agent target context must be initiated by a human")
	}
	return message.AgentThreadSessionContext{
		WorkspaceID:       workspaceID,
		TargetKey:         target.TargetKey,
		TargetType:        scopeType,
		MessageID:         target.MessageID,
		ThreadChannelID:   target.TargetKey,
		ParentChannelID:   strings.TrimSpace(scope.ParentScopeID),
		RootMessageID:     strings.TrimSpace(scope.RootMessageID),
		AgentID:           strings.TrimSpace(authenticated.AgentID),
		AgentMemberID:     strings.TrimSpace(authenticated.MemberID),
		ActingUserID:      strings.TrimSpace(author.PrincipalID),
		ActingMemberID:    strings.TrimSpace(author.MemberID),
		ActingDisplayName: strings.TrimSpace(author.DisplayName),
	}, nil
}

func supportedAgentTargetType(scopeType string) bool {
	switch strings.TrimSpace(scopeType) {
	case "channel", "dm", "thread", "task":
		return true
	default:
		return false
	}
}

func (store *Store) resolveHiddenAgentThreadSessionContextForAgent(ctx context.Context, authenticated agent.AuthenticatedAgent, workspaceID string, threadChannelID string) (message.AgentThreadSessionContext, error) {
	row := store.db.QueryRow(ctx, `
SELECT
  child.id,
  child.parent_scope_id,
  child.root_message_id,
  root.content,
  root.metadata,
  root.author_member_id,
  root.author_type,
  COALESCE(root_author.user_id, ''),
  COALESCE(root_author.display_name, '')
FROM scopes child
JOIN scope_messages root
  ON root.workspace_id = child.workspace_id
 AND root.scope_id = child.parent_scope_id
 AND root.id = child.root_message_id
JOIN members root_author
  ON root_author.id = root.author_member_id
JOIN scope_channel_profiles parent_profile
  ON parent_profile.workspace_id = child.workspace_id
 AND parent_profile.scope_id = child.parent_scope_id
 AND parent_profile.subsystem = $3
WHERE child.workspace_id = $1
  AND child.id = $2
  AND child.type = 'thread'
  AND child.status = 'active'
  AND child.archived_at IS NULL
`, workspaceID, threadChannelID, message.ChannelSubsystemAgentSession)
	var result message.AgentThreadSessionContext
	var content string
	var rawMetadata []byte
	var authorType string
	if err := row.Scan(&result.ThreadChannelID, &result.ParentChannelID, &result.RootMessageID, &content, &rawMetadata, &result.ActingMemberID, &authorType, &result.ActingUserID, &result.ActingDisplayName); err != nil {
		if coredb.IsNotFound(err) {
			return message.AgentThreadSessionContext{}, ErrNotFound
		}
		return message.AgentThreadSessionContext{}, fmt.Errorf("resolve agent thread session context: %w", err)
	}
	if strings.TrimSpace(authorType) != "human" || strings.TrimSpace(result.ActingUserID) == "" {
		return message.AgentThreadSessionContext{}, forbidden("agent thread context must be initiated by a human")
	}
	metadata, err := message.DecodeMessageMetadata(rawMetadata)
	if err != nil {
		return message.AgentThreadSessionContext{}, err
	}
	result.WorkspaceID = workspaceID

	recipient, err := withTxValue(ctx, store.db, func(tx coredb.Tx) (message.AgentRecipient, error) {
		recipients, ok, err := message.LoadScopePersistedMentionedAgentRecipientsWithMetadata(ctx, tx, result.ParentChannelID, result.ActingMemberID, content, metadata)
		if err != nil {
			return message.AgentRecipient{}, err
		}
		if !ok || len(recipients) != 1 {
			return message.AgentRecipient{}, ErrNotFound
		}
		return recipients[0], nil
	})
	if err != nil {
		return message.AgentThreadSessionContext{}, err
	}
	result.AgentID = strings.TrimSpace(recipient.AgentID)
	result.AgentMemberID = strings.TrimSpace(recipient.AgentMemberID)
	if strings.TrimSpace(result.AgentID) != strings.TrimSpace(authenticated.AgentID) {
		return message.AgentThreadSessionContext{}, forbidden("agent thread context belongs to a different agent")
	}
	if strings.TrimSpace(result.AgentMemberID) != "" && strings.TrimSpace(result.AgentMemberID) != strings.TrimSpace(authenticated.MemberID) {
		return message.AgentThreadSessionContext{}, forbidden("agent thread context belongs to a different agent member")
	}
	return result, nil
}

func (store *Store) resolveReadableReplyThreadSessionContextForAgent(ctx context.Context, authenticated agent.AuthenticatedAgent, workspaceID string, threadChannelID string) (message.AgentThreadSessionContext, error) {
	if strings.TrimSpace(authenticated.MemberID) == "" {
		return message.AgentThreadSessionContext{}, forbidden("agent thread attestation requires an agent member")
	}
	shape, err := message.LoadReplyThreadShape(ctx, store.db, workspaceID, threadChannelID)
	if err != nil {
		if errors.Is(err, message.ErrNotFound) {
			return message.AgentThreadSessionContext{}, ErrNotFound
		}
		return message.AgentThreadSessionContext{}, err
	}
	root, err := message.NewScopeAPIService(store.messageStore()).CanReadMessage(ctx, message.ScopeAPIReadCheckRequest{
		WorkspaceID:    workspaceID,
		MessageID:      shape.ParentMessageID,
		ViewerMemberID: authenticated.MemberID,
		ViewerType:     message.ScopeActorAgent,
		RequestKind:    message.ScopeVisibilityRequestHistory,
	})
	if err != nil {
		return message.AgentThreadSessionContext{}, err
	}
	if !root.Allowed || root.Message == nil {
		return message.AgentThreadSessionContext{}, forbidden("agent cannot read reply thread root")
	}
	var actingUserID string
	var actingMemberID string
	var actingDisplayName string
	if err := store.db.QueryRow(ctx, `
SELECT COALESCE(author.user_id, ''), author.id, COALESCE(author.display_name, '')
FROM scope_messages root
JOIN members author
  ON author.workspace_id = root.workspace_id
 AND author.id = root.author_member_id
WHERE root.workspace_id = $1
  AND root.id = $2
  AND root.author_type = 'human'
`, workspaceID, shape.ParentMessageID).Scan(&actingUserID, &actingMemberID, &actingDisplayName); err != nil {
		if coredb.IsNotFound(err) {
			return message.AgentThreadSessionContext{}, forbidden("reply thread root must be initiated by a human")
		}
		return message.AgentThreadSessionContext{}, fmt.Errorf("load reply thread root author: %w", err)
	}
	if strings.TrimSpace(actingUserID) == "" {
		return message.AgentThreadSessionContext{}, forbidden("reply thread root must be initiated by a human")
	}
	return message.AgentThreadSessionContext{
		WorkspaceID:       workspaceID,
		TargetKey:         threadChannelID,
		TargetType:        "thread",
		MessageID:         shape.ParentMessageID,
		ThreadChannelID:   threadChannelID,
		ParentChannelID:   shape.ParentScopeID,
		RootMessageID:     shape.ParentMessageID,
		AgentID:           strings.TrimSpace(authenticated.AgentID),
		AgentMemberID:     strings.TrimSpace(authenticated.MemberID),
		ActingUserID:      strings.TrimSpace(actingUserID),
		ActingMemberID:    strings.TrimSpace(actingMemberID),
		ActingDisplayName: strings.TrimSpace(actingDisplayName),
	}, nil
}

func (store *Store) EnsureAgentThreadSessionChannelForUser(ctx context.Context, userID string, workspaceID string) (message.ChannelSummary, error) {
	member, err := store.requireWorkspaceAccess(ctx, userID, workspaceID)
	if err != nil {
		return message.ChannelSummary{}, err
	}
	if existing, ok, err := store.loadAgentThreadSessionChannel(ctx, workspaceID, member.Member.ID); err != nil || ok {
		return existing, err
	}
	now := time.Now().UTC()
	channelID, err := identifiers.NewEntityID("chn")
	if err != nil {
		return message.ChannelSummary{}, fmt.Errorf("generate agent thread session channel id: %w", err)
	}
	slug := agentThreadSessionChannelSlug(member.Member.ID, channelID)
	if err := domain.ValidateChannelSlug(slug); err != nil {
		return message.ChannelSummary{}, err
	}
	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return message.ChannelSummary{}, fmt.Errorf("begin create agent thread session channel tx: %w", err)
	}
	defer func() { _ = tx.Rollback(ctx) }()

	repos := message.NewScopeRepositories(tx)
	if err := repos.Scopes.UpsertScope(ctx, message.ScopeRecord{
		ID:                channelID,
		WorkspaceID:       workspaceID,
		Type:              string(domain.ThreadTypeChannel),
		IdentityKey:       "channel:" + channelID,
		Status:            "active",
		CreatedByMemberID: member.Member.ID,
		CreatedAt:         now,
		UpdatedAt:         now,
	}); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("insert agent thread session channel scope: %w", err)
	}
	insertProfile := func(profileSlug string) error {
		_, err := tx.Exec(ctx, `
INSERT INTO scope_channel_profiles (
  scope_id, workspace_id, slug, display_name, description, visibility, subsystem,
  owner_member_id, is_all_channel, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, 'private', $6, $7, FALSE, $8, $8)
`, channelID, workspaceID, profileSlug, agentThreadSessionChannelDisplayName, "Hidden agent-thread session carrier.", message.ChannelSubsystemAgentSession, member.Member.ID, now)
		return err
	}
	if err := insertProfile(slug); err != nil {
		if coredb.IsUniqueViolation(err) {
			fallbackSlug := agentThreadSessionChannelSlug("", channelID)
			if fallbackSlug == slug {
				fallbackSlug = "agent-session"
			}
			if retryErr := insertProfile(fallbackSlug); retryErr != nil {
				if coredb.IsUniqueViolation(retryErr) {
					return message.ChannelSummary{}, conflict("agent thread session channel slug already exists")
				}
				return message.ChannelSummary{}, fmt.Errorf("insert fallback agent thread session channel profile: %w", retryErr)
			}
			slug = fallbackSlug
		} else {
			return message.ChannelSummary{}, fmt.Errorf("insert agent thread session channel profile: %w", err)
		}
	}
	if err := store.ensureChannelScopeParticipantProjection(ctx, tx, channelID, member.Member.ID, "no-wake", now); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("insert agent thread session channel participant: %w", err)
	}
	if err := tx.Commit(ctx); err != nil {
		return message.ChannelSummary{}, fmt.Errorf("commit agent thread session channel create: %w", err)
	}
	return message.ChannelSummary{
		ChannelID:   channelID,
		Slug:        slug,
		DisplayName: agentThreadSessionChannelDisplayName,
		Description: "Hidden agent-thread session carrier.",
		Joined:      true,
		CanInvite:   true,
		CanManage:   true,
	}, nil
}

func (store *Store) loadAgentThreadSessionChannel(ctx context.Context, workspaceID string, memberID string) (message.ChannelSummary, bool, error) {
	row := store.db.QueryRow(ctx, `
SELECT s.id, p.slug, COALESCE(NULLIF(p.display_name, ''), p.slug), COALESCE(p.description, '')
FROM scopes s
JOIN scope_channel_profiles p
  ON p.scope_id = s.id
 AND p.workspace_id = s.workspace_id
WHERE s.workspace_id = $1
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND p.subsystem = $2
  AND p.owner_member_id = $3
ORDER BY p.created_at ASC, s.id ASC
LIMIT 1
`, strings.TrimSpace(workspaceID), message.ChannelSubsystemAgentSession, strings.TrimSpace(memberID))
	var summary message.ChannelSummary
	if err := row.Scan(&summary.ChannelID, &summary.Slug, &summary.DisplayName, &summary.Description); err != nil {
		if coredb.IsNotFound(err) {
			return message.ChannelSummary{}, false, nil
		}
		return message.ChannelSummary{}, false, fmt.Errorf("load agent thread session channel: %w", err)
	}
	summary.Joined = true
	summary.CanInvite = true
	summary.CanManage = true
	return summary, true, nil
}

func agentThreadSessionChannelSlug(memberID string, channelID string) string {
	suffix := strings.ToLower(strings.TrimSpace(memberID))
	if suffix == "" {
		suffix = strings.ToLower(strings.TrimSpace(channelID))
	}
	suffix = strings.Map(func(r rune) rune {
		switch {
		case r >= 'a' && r <= 'z':
			return r
		case r >= '0' && r <= '9':
			return r
		default:
			return '-'
		}
	}, suffix)
	suffix = strings.Trim(suffix, "-")
	for strings.Contains(suffix, "--") {
		suffix = strings.ReplaceAll(suffix, "--", "-")
	}
	if len(suffix) > 40 {
		suffix = suffix[:40]
	}
	if suffix == "" {
		suffix = "session"
	}
	return "agent-session-" + suffix
}

func stringValueFromMetadata(value any) string {
	if text, ok := value.(string); ok {
		return text
	}
	return ""
}
