package app

import (
	"context"
	"fmt"
	"strings"

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

type agentScopeTargetRequest struct {
	Raw         string
	Requested   access.ScopeTargetRef
	TaskScopeID string
	IsRawTask   bool
	Reference   domain.ThreadReference
	Team        domain.TeamTargetReference
}

type agentTargetResolutionRole string

const (
	agentTargetResolutionRoleWrite agentTargetResolutionRole = "write"
	agentTargetResolutionRoleRead  agentTargetResolutionRole = "read"
)

// ResolveWritableAgentMessageTarget resolves an agent-facing send/upload target
// to the shared scope target/access contract. It preserves the existing daemon
// target behavior while making the storage, delivery, and authorization roles
// explicit for new callers.
func (store *Store) ResolveWritableAgentMessageTarget(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (access.ResolvedScopeTarget, error) {
	request, err := parseAgentScopeTargetRequest(target)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	if request.IsRawTask {
		task, err := store.ResolveTaskForAgentTaskScopeTarget(ctx, authenticatedAgent, request.TaskScopeID)
		if err != nil {
			return access.ResolvedScopeTarget{}, err
		}
		return newAgentTaskScopeTarget(authenticatedAgent, request, task.TaskScopeID, task.ChannelID, access.ScopeTargetOperationTaskDiscussion, agentTargetResolutionRoleWrite)
	}

	tx, err := store.db.BeginTx(ctx, coredb.TxOptions{})
	if err != nil {
		return access.ResolvedScopeTarget{}, fmt.Errorf("begin resolve target tx: %w", err)
	}
	defer rollback(tx, ctx)

	threadTarget, basis, err := store.resolveAgentThreadTarget(ctx, tx, authenticatedAgent, request, agentTargetResolveOptions{
		Role:              agentTargetResolutionRoleWrite,
		CreatePlainDM:     request.Reference.Type == domain.ThreadTypeDM && strings.TrimSpace(request.Reference.ShortID) == "",
		CreateReplyThread: true,
	})
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	threadTarget, err = hydrateAgentReplyThreadTargetMetadata(ctx, tx, authenticatedAgent.WorkspaceID, request, threadTarget)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	resolved, err := newAgentThreadScopeTarget(authenticatedAgent, request, threadTarget, basis, access.ScopeTargetOperationMessageSend, agentTargetResolutionRoleWrite)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return access.ResolvedScopeTarget{}, fmt.Errorf("commit resolve target tx: %w", err)
	}
	return resolved, nil
}

// ResolveReadableAgentMessageTarget resolves an agent-facing history/search
// target to the shared scope target/access contract. Read resolution must not
// create DM or reply-thread scopes.
func (store *Store) ResolveReadableAgentMessageTarget(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (access.ResolvedScopeTarget, error) {
	request, err := parseAgentScopeTargetRequest(target)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	if request.IsRawTask {
		task, err := store.ResolveTaskForAgentTaskScopeTarget(ctx, authenticatedAgent, request.TaskScopeID)
		if err != nil {
			return access.ResolvedScopeTarget{}, err
		}
		return newAgentTaskScopeTarget(authenticatedAgent, request, task.TaskScopeID, task.ChannelID, access.ScopeTargetOperationMessageRead, agentTargetResolutionRoleRead)
	}

	threadTarget, basis, err := store.resolveAgentThreadTarget(ctx, store.db, authenticatedAgent, request, agentTargetResolveOptions{Role: agentTargetResolutionRoleRead})
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	threadTarget, err = hydrateAgentReplyThreadTargetMetadata(ctx, store.db, authenticatedAgent.WorkspaceID, request, threadTarget)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	return newAgentThreadScopeTarget(authenticatedAgent, request, threadTarget, basis, access.ScopeTargetOperationMessageRead, agentTargetResolutionRoleRead)
}

// ResolveAgentReviewSubjectTarget resolves a review-context subject to the
// shared scope target/access contract. Review contexts are anchored to concrete
// DM or reply-thread scopes, not plain channels or raw task scopes.
func (store *Store) ResolveAgentReviewSubjectTarget(ctx context.Context, authenticatedAgent agent.AuthenticatedAgent, target string) (access.ResolvedScopeTarget, error) {
	request, err := parseAgentScopeTargetRequest(target)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	if request.IsRawTask || (request.Reference.Type == domain.ThreadTypeChannel && strings.TrimSpace(request.Reference.ShortID) == "") {
		return access.ResolvedScopeTarget{}, badRequest("channel review context is not supported; use a DM or reply-thread target")
	}
	if strings.TrimSpace(request.Team.TeamID) != "" && strings.TrimSpace(request.Team.ShortID) == "" {
		return access.ResolvedScopeTarget{}, badRequest("channel review context is not supported; use a DM or reply-thread target")
	}
	threadTarget, basis, err := store.resolveAgentThreadTarget(ctx, store.db, authenticatedAgent, request, agentTargetResolveOptions{Role: agentTargetResolutionRoleRead})
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	threadTarget, err = hydrateAgentReplyThreadTargetMetadata(ctx, store.db, authenticatedAgent.WorkspaceID, request, threadTarget)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	return newAgentThreadScopeTarget(authenticatedAgent, request, threadTarget, basis, access.ScopeTargetOperationMessageRead, agentTargetResolutionRoleRead)
}

func parseAgentScopeTargetRequest(target string) (agentScopeTargetRequest, error) {
	trimmed := strings.TrimSpace(target)
	if trimmed == "" {
		return agentScopeTargetRequest{}, badRequest("target is required")
	}
	parsed, err := domain.ParseMessageTargetReference(trimmed)
	if err != nil {
		return agentScopeTargetRequest{}, mapMessagingError(err)
	}
	switch parsed.Kind {
	case domain.MessageTargetKindTeam:
		kind := access.ScopeTargetKindChannel
		if strings.TrimSpace(parsed.Team.ShortID) != "" {
			kind = access.ScopeTargetKindThread
		}
		return agentScopeTargetRequest{
			Raw:  trimmed,
			Team: parsed.Team,
			Requested: access.ScopeTargetRef{
				Kind: kind,
				Ref:  parsed.Team.String(),
			},
		}, nil
	case domain.MessageTargetKindTask:
		return agentScopeTargetRequest{
			Raw:         trimmed,
			TaskScopeID: parsed.TaskScopeID,
			IsRawTask:   true,
			Requested: access.ScopeTargetRef{
				Kind: access.ScopeTargetKindTask,
				Ref:  parsed.TaskScopeID,
			},
		}, nil
	case domain.MessageTargetKindChannel, domain.MessageTargetKindReplyThread, domain.MessageTargetKindDM:
	default:
		return agentScopeTargetRequest{}, badRequest("unsupported message target")
	}
	kind := access.ScopeTargetKindChannel
	switch parsed.Kind {
	case domain.MessageTargetKindReplyThread:
		kind = access.ScopeTargetKindThread
	case domain.MessageTargetKindDM:
		kind = access.ScopeTargetKindDM
	}
	return agentScopeTargetRequest{
		Raw:       trimmed,
		Reference: parsed.Thread,
		Requested: access.ScopeTargetRef{
			Kind: kind,
			Ref:  parsed.Thread.String(),
		},
	}, nil
}

func newAgentTaskScopeTarget(authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, taskScopeID string, taskBoardChannelID string, operation access.ScopeTargetOperation, role agentTargetResolutionRole) (access.ResolvedScopeTarget, error) {
	scope := access.ScopeRef{ScopeID: strings.TrimSpace(taskScopeID), ScopeType: access.ScopeTargetKindTask}
	resolved := baseAgentResolvedScopeTarget(authenticatedAgent, operation, request.Requested)
	switch role {
	case agentTargetResolutionRoleWrite:
		resolved.StorageScope = scope
		resolved.DeliveryScope = scope
	case agentTargetResolutionRoleRead:
		resolved.ReadScope = scope
	default:
		return access.ResolvedScopeTarget{}, fmt.Errorf("unknown agent target resolution role %q", role)
	}
	// The permission basis records the actual authoritative board Channel scope
	// (ActiveTaskBoardChannel.ChannelID derived from tasks.board_id ->
	// task_boards), never the Task scope. The Task scope is only target identity.
	// This keeps the audit basis truthful: a retained Task membership with no
	// Channel membership grants nothing, and the basis scope names the Channel
	// whose membership actually authorized the operation.
	resolved.PermissionBasis = []access.ScopePermissionBasis{{
		Kind:     access.ScopePermissionBasisTaskBoardChannel,
		Scope:    access.ScopeRef{ScopeID: strings.TrimSpace(taskBoardChannelID), ScopeType: access.ScopeTargetKindChannel},
		MemberID: strings.TrimSpace(authenticatedAgent.MemberID),
		Reason:   "task board channel membership",
	}}
	return validateAgentScopeTarget(resolved)
}

func newAgentThreadScopeTarget(authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, target message.ThreadAccess, basis agentTargetBasis, operation access.ScopeTargetOperation, role agentTargetResolutionRole) (access.ResolvedScopeTarget, error) {
	targetScope := access.ScopeRef{
		ScopeID:   message.ScopeIDForThreadAccess(target),
		ScopeType: agentThreadScopeKind(request, target),
	}
	resolved := baseAgentResolvedScopeTarget(authenticatedAgent, operation, request.Requested)
	switch role {
	case agentTargetResolutionRoleWrite:
		resolved.StorageScope = targetScope
		resolved.DeliveryScope = targetScope
	case agentTargetResolutionRoleRead:
		resolved.ReadScope = targetScope
	default:
		return access.ResolvedScopeTarget{}, fmt.Errorf("unknown agent target resolution role %q", role)
	}

	if target.ReplyThread != nil {
		resolved.RootMessageID = strings.TrimSpace(target.ReplyThread.ParentMessageID)
		resolved.RootMessageShort = strings.TrimSpace(target.ReplyThread.ShortID)
		if parentScope := agentReplyThreadParentScope(target); !parentScope.Empty() {
			resolved.ParentScope = parentScope
		}
	}
	resolved.PermissionBasis = []access.ScopePermissionBasis{agentThreadPermissionBasis(authenticatedAgent, request, target, targetScope, resolved.ParentScope, basis)}
	return validateAgentScopeTarget(resolved)
}

func baseAgentResolvedScopeTarget(authenticatedAgent agent.AuthenticatedAgent, operation access.ScopeTargetOperation, requested access.ScopeTargetRef) access.ResolvedScopeTarget {
	resolved := access.ResolvedScopeTarget{
		WorkspaceID: strings.TrimSpace(authenticatedAgent.WorkspaceID),
		Operation:   operation,
		Requested:   requested,
		Subjects: []access.ScopeAuthoritySubject{{
			Kind:          access.ScopeAuthoritySubjectRoutingPrincipal,
			PrincipalType: access.PrincipalTypeAgent,
			PrincipalID:   strings.TrimSpace(authenticatedAgent.AgentID),
			MemberID:      strings.TrimSpace(authenticatedAgent.MemberID),
		}},
	}
	if operation == access.ScopeTargetOperationMessageSend || operation == access.ScopeTargetOperationTaskDiscussion {
		resolved.Subjects = append([]access.ScopeAuthoritySubject{{
			Kind:          access.ScopeAuthoritySubjectDurableAuthor,
			PrincipalType: access.PrincipalTypeAgent,
			PrincipalID:   strings.TrimSpace(authenticatedAgent.AgentID),
			MemberID:      strings.TrimSpace(authenticatedAgent.MemberID),
		}}, resolved.Subjects...)
	}
	return resolved
}

func agentThreadScopeKind(request agentScopeTargetRequest, target message.ThreadAccess) access.ScopeTargetKind {
	if request.Requested.Kind == access.ScopeTargetKindThread || target.ReplyThread != nil {
		return access.ScopeTargetKindThread
	}
	if request.Requested.Kind == access.ScopeTargetKindDM || target.ThreadType == domain.ThreadTypeDM || strings.TrimSpace(target.DMThreadID) != "" {
		return access.ScopeTargetKindDM
	}
	return access.ScopeTargetKindChannel
}

func agentReplyThreadParentScope(target message.ThreadAccess) access.ScopeRef {
	if target.ReplyThread == nil || strings.TrimSpace(target.ReplyThread.ParentChannelID) == "" {
		return access.ScopeRef{}
	}
	parentKind := access.ScopeTargetKindChannel
	if target.ThreadType == domain.ThreadTypeDM {
		parentKind = access.ScopeTargetKindDM
	}
	return access.ScopeRef{
		ScopeID:   strings.TrimSpace(target.ReplyThread.ParentChannelID),
		ScopeType: parentKind,
	}
}

func hydrateAgentReplyThreadTargetMetadata(ctx context.Context, queryer coredb.QueryRower, workspaceID string, request agentScopeTargetRequest, target message.ThreadAccess) (message.ThreadAccess, error) {
	if request.Requested.Kind != access.ScopeTargetKindThread || agentReplyThreadTargetMetadataComplete(target) {
		return target, nil
	}
	targetScopeID := message.ScopeIDForThreadAccess(target)
	row := queryer.QueryRow(ctx, `
SELECT COALESCE(child.parent_scope_id, ''),
       COALESCE(parent.type, ''),
       COALESCE(child.root_message_id, ''),
       COALESCE(child.root_message_short_id, '')
FROM scopes child
LEFT JOIN scopes parent
  ON parent.workspace_id = child.workspace_id
 AND parent.id = child.parent_scope_id
WHERE child.workspace_id = $1
  AND child.id = $2
  AND child.type = 'thread'
  AND child.status = 'active'
  AND child.archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID), targetScopeID)
	var parentScopeID string
	var parentScopeType string
	var rootMessageID string
	var rootMessageShort string
	if err := row.Scan(&parentScopeID, &parentScopeType, &rootMessageID, &rootMessageShort); err != nil {
		if coredb.IsNotFound(err) {
			return message.ThreadAccess{}, ErrNotFound
		}
		return message.ThreadAccess{}, fmt.Errorf("hydrate agent reply-thread target metadata: %w", err)
	}
	if strings.TrimSpace(parentScopeID) == "" {
		return target, nil
	}
	hydrated := message.ReplyThreadTarget{
		ParentMessageID: strings.TrimSpace(rootMessageID),
		ShortID:         strings.TrimSpace(rootMessageShort),
		ParentChannelID: strings.TrimSpace(parentScopeID),
	}
	if target.ReplyThread != nil {
		hydrated.ParentChannelName = strings.TrimSpace(target.ReplyThread.ParentChannelName)
	}
	target.ReplyThread = &hydrated
	if strings.TrimSpace(parentScopeType) == string(domain.ThreadTypeDM) {
		target.ThreadType = domain.ThreadTypeDM
	}
	return target, nil
}

func agentReplyThreadTargetMetadataComplete(target message.ThreadAccess) bool {
	if target.ReplyThread == nil {
		return false
	}
	return strings.TrimSpace(target.ReplyThread.ParentChannelID) != "" &&
		strings.TrimSpace(target.ReplyThread.ParentMessageID) != "" &&
		strings.TrimSpace(target.ReplyThread.ShortID) != ""
}

func agentThreadPermissionBasis(authenticatedAgent agent.AuthenticatedAgent, request agentScopeTargetRequest, target message.ThreadAccess, targetScope access.ScopeRef, parentScope access.ScopeRef, basis agentTargetBasis) access.ScopePermissionBasis {
	memberID := strings.TrimSpace(authenticatedAgent.MemberID)
	if basis.Private() {
		scope := parentScope
		if scope.Empty() {
			scope = targetScope
		}
		return access.ScopePermissionBasis{
			Kind:           access.ScopePermissionBasisOwnerPrivateMembership,
			Scope:          scope,
			DecisionSource: access.DecisionSourceChannelMembership,
			MemberID:       memberID,
			Reason:         "owner-private agent membership",
		}
	}
	if request.Requested.Kind == access.ScopeTargetKindThread {
		kind := access.ScopePermissionBasisParentMembership
		if target.ThreadType == domain.ThreadTypeDM {
			kind = access.ScopePermissionBasisParentDMParticipant
		}
		return access.ScopePermissionBasis{
			Kind:           kind,
			Scope:          parentScope,
			DecisionSource: access.DecisionSourceThreadInherit,
			MemberID:       memberID,
			Reason:         "reply-thread access inherits from parent conversation",
		}
	}
	if request.Requested.Kind == access.ScopeTargetKindDM || target.ThreadType == domain.ThreadTypeDM {
		return access.ScopePermissionBasis{
			Kind:           access.ScopePermissionBasisDirectMembership,
			Scope:          targetScope,
			DecisionSource: access.DecisionSourceDMParticipant,
			MemberID:       memberID,
		}
	}
	return access.ScopePermissionBasis{
		Kind:           access.ScopePermissionBasisDirectMembership,
		Scope:          targetScope,
		DecisionSource: access.DecisionSourceChannelMembership,
		MemberID:       memberID,
	}
}

func validateAgentScopeTarget(target access.ResolvedScopeTarget) (access.ResolvedScopeTarget, error) {
	normalized := target.Normalized()
	if err := normalized.Validate(); err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	return normalized, nil
}
