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/access"
)

type attachmentAuthorityScope struct {
	ID               string
	Type             string
	ParentID         string
	RootMessageID    string
	RootMessageShort string
}

// resolveAgentAttachmentUploadScopeForMember authorizes an agent upload to the
// target scope where the unclaimed attachment will be stored. The target scope
// must remain the requested scope: reply-thread uploads store on the thread
// scope, while the parent channel/DM is used only as an authorization basis.
func resolveAgentAttachmentUploadScopeForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, targetScopeID string) (string, error) {
	target, err := resolveAgentAttachmentAuthorityTargetForMember(ctx, queryer, workspaceID, memberID, targetScopeID, access.ScopeTargetOperationAttachmentUpload)
	if err != nil {
		return "", err
	}
	if target.StorageScope.Empty() {
		return "", ErrNotFound
	}
	return target.StorageScope.ScopeID, nil
}

// authorizeAgentAttachmentReadScopeForMember authorizes an agent download from
// the attachment storage scope. Reply-thread attachments stay stored on the
// thread scope, while the parent channel/DM is only an authorization basis.
func authorizeAgentAttachmentReadScopeForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, targetScopeID string) error {
	_, err := resolveAgentAttachmentAuthorityTargetForMember(ctx, queryer, workspaceID, memberID, targetScopeID, access.ScopeTargetOperationAttachmentRead)
	return err
}

func resolveAgentAttachmentAuthorityTargetForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, targetScopeID string, operation access.ScopeTargetOperation) (access.ResolvedScopeTarget, error) {
	trimmedScopeID := strings.TrimSpace(targetScopeID)
	if trimmedScopeID == "" {
		return access.ResolvedScopeTarget{}, badRequest("scopeId is required")
	}

	target, err := loadAttachmentAuthorityScope(ctx, queryer, workspaceID, trimmedScopeID)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	targetRef, err := attachmentAuthorityScopeRef(target)
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}

	resolved := access.ResolvedScopeTarget{
		WorkspaceID: strings.TrimSpace(workspaceID),
		Operation:   operation,
		Requested: access.ScopeTargetRef{
			Kind: targetRef.ScopeType,
			Ref:  targetRef.ScopeID,
		},
		Subjects: []access.ScopeAuthoritySubject{agentAttachmentAuthoritySubject(operation, memberID)},
	}
	switch operation {
	case access.ScopeTargetOperationAttachmentUpload:
		resolved.StorageScope = targetRef
	case access.ScopeTargetOperationAttachmentRead:
		resolved.ReadScope = targetRef
	default:
		return access.ResolvedScopeTarget{}, badRequest("unsupported attachment target operation")
	}

	var basis access.ScopePermissionBasis
	switch target.Type {
	case string(domain.ThreadTypeChannel):
		basis, err = authorizeAgentAttachmentChannelScope(ctx, queryer, workspaceID, memberID, target.ID)
	case string(domain.ThreadTypeDM):
		basis, err = authorizeAgentAttachmentDMScope(ctx, queryer, memberID, target.ID, access.ScopePermissionBasisDirectMembership, access.DecisionSourceDMParticipant, "dm attachment target membership")
	case "thread":
		var parent access.ScopeRef
		basis, parent, err = authorizeAgentAttachmentReplyThreadScope(ctx, queryer, workspaceID, memberID, target)
		resolved.ParentScope = parent
		resolved.RootMessageID = strings.TrimSpace(target.RootMessageID)
		resolved.RootMessageShort = strings.TrimSpace(target.RootMessageShort)
	case "task":
		basis, err = authorizeAgentTaskAttachmentScopeForMember(ctx, queryer, workspaceID, memberID, target.ID, operation)
	default:
		return access.ResolvedScopeTarget{}, ErrNotFound
	}
	if err != nil {
		return access.ResolvedScopeTarget{}, err
	}
	resolved.PermissionBasis = []access.ScopePermissionBasis{basis}

	resolved = resolved.Normalized()
	if err := resolved.Validate(); err != nil {
		return access.ResolvedScopeTarget{}, fmt.Errorf("resolve agent attachment authority target: %w", err)
	}
	return resolved, nil
}

func agentAttachmentAuthoritySubject(operation access.ScopeTargetOperation, memberID string) access.ScopeAuthoritySubject {
	kind := access.ScopeAuthoritySubjectRoutingPrincipal
	if operation == access.ScopeTargetOperationAttachmentUpload {
		kind = access.ScopeAuthoritySubjectDurableAuthor
	}
	return access.ScopeAuthoritySubject{
		Kind:          kind,
		PrincipalType: access.PrincipalTypeAgent,
		MemberID:      strings.TrimSpace(memberID),
	}
}

func loadAttachmentAuthorityScope(ctx context.Context, queryer coredb.QueryRower, workspaceID string, scopeID string) (attachmentAuthorityScope, error) {
	row := queryer.QueryRow(ctx, `
SELECT id, type, COALESCE(parent_scope_id, ''), COALESCE(root_message_id, ''), COALESCE(root_message_short_id, '')
FROM scopes
WHERE workspace_id = $1
  AND id = $2
  AND status = 'active'
  AND archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(scopeID))

	var scope attachmentAuthorityScope
	if err := row.Scan(&scope.ID, &scope.Type, &scope.ParentID, &scope.RootMessageID, &scope.RootMessageShort); err != nil {
		if coredb.IsNotFound(err) {
			return attachmentAuthorityScope{}, ErrNotFound
		}
		return attachmentAuthorityScope{}, fmt.Errorf("load attachment authority scope: %w", err)
	}
	scope.ID = strings.TrimSpace(scope.ID)
	scope.Type = strings.TrimSpace(scope.Type)
	scope.ParentID = strings.TrimSpace(scope.ParentID)
	scope.RootMessageID = strings.TrimSpace(scope.RootMessageID)
	scope.RootMessageShort = strings.TrimSpace(scope.RootMessageShort)
	return scope, nil
}

func attachmentAuthorityScopeRef(scope attachmentAuthorityScope) (access.ScopeRef, error) {
	scopeType := access.ScopeTargetKind(strings.TrimSpace(scope.Type))
	switch scopeType {
	case access.ScopeTargetKindChannel, access.ScopeTargetKindDM, access.ScopeTargetKindThread, access.ScopeTargetKindTask:
		return access.ScopeRef{ScopeID: strings.TrimSpace(scope.ID), ScopeType: scopeType}, nil
	default:
		return access.ScopeRef{}, ErrNotFound
	}
}

func authorizeAgentAttachmentChannelScope(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, channelScopeID string) (access.ScopePermissionBasis, error) {
	row := queryer.QueryRow(ctx, `
SELECT sm.visibility, COALESCE(sm.owner_member_id, '')
FROM scopes s
JOIN members m
  ON m.workspace_id = s.workspace_id
 AND m.id = $3
 AND m.type = 'agent'
JOIN scope_memberships sm
  ON sm.scope_id = s.id
 AND sm.member_id = m.id
 AND sm.removed_at IS NULL
 AND sm.archived_at IS NULL
 AND sm.effective_to_seq IS NULL
 AND (
      sm.visibility = 'public'
      OR (sm.visibility = 'private' AND sm.owner_member_id IS NOT NULL)
 )
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
LIMIT 1
`, strings.TrimSpace(workspaceID), strings.TrimSpace(channelScopeID), strings.TrimSpace(memberID))

	var visibility string
	var ownerMemberID string
	if err := row.Scan(&visibility, &ownerMemberID); err != nil {
		if coredb.IsNotFound(err) {
			return access.ScopePermissionBasis{}, ErrForbidden
		}
		return access.ScopePermissionBasis{}, fmt.Errorf("authorize channel attachment scope: %w", err)
	}

	basisKind := access.ScopePermissionBasisDirectMembership
	if strings.TrimSpace(visibility) == "private" && strings.TrimSpace(ownerMemberID) != "" {
		basisKind = access.ScopePermissionBasisOwnerPrivateMembership
	}
	return attachmentPermissionBasis(
		basisKind,
		access.ScopeRef{ScopeID: strings.TrimSpace(channelScopeID), ScopeType: access.ScopeTargetKindChannel},
		access.DecisionSourceChannelMembership,
		memberID,
		ownerMemberID,
		"channel attachment target membership",
	), nil
}

func authorizeAgentAttachmentDMScope(ctx context.Context, queryer coredb.QueryRower, memberID string, dmScopeID string, basisKind access.ScopePermissionBasisKind, decisionSource access.DecisionSource, reason string) (access.ScopePermissionBasis, error) {
	basis, allowed, err := activeAttachmentScopeMembershipBasis(
		ctx,
		queryer,
		memberID,
		access.ScopeRef{ScopeID: strings.TrimSpace(dmScopeID), ScopeType: access.ScopeTargetKindDM},
		basisKind,
		decisionSource,
		reason,
	)
	if err != nil {
		return access.ScopePermissionBasis{}, err
	}
	if !allowed {
		return access.ScopePermissionBasis{}, ErrForbidden
	}
	return basis, nil
}

func authorizeAgentAttachmentReplyThreadScope(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, thread attachmentAuthorityScope) (access.ScopePermissionBasis, access.ScopeRef, error) {
	if thread.ParentID == "" {
		return access.ScopePermissionBasis{}, access.ScopeRef{}, ErrNotFound
	}
	parent, err := loadAttachmentAuthorityScope(ctx, queryer, workspaceID, thread.ParentID)
	if err != nil {
		return access.ScopePermissionBasis{}, access.ScopeRef{}, err
	}
	parentRef, err := attachmentAuthorityScopeRef(parent)
	if err != nil {
		return access.ScopePermissionBasis{}, access.ScopeRef{}, err
	}

	threadRef := access.ScopeRef{ScopeID: strings.TrimSpace(thread.ID), ScopeType: access.ScopeTargetKindThread}
	if basis, allowed, err := activeAttachmentScopeMembershipBasis(
		ctx,
		queryer,
		memberID,
		threadRef,
		access.ScopePermissionBasisDirectMembership,
		access.DecisionSourceThreadInherit,
		"reply-thread attachment target membership",
	); err != nil {
		return access.ScopePermissionBasis{}, access.ScopeRef{}, err
	} else if allowed {
		return basis, parentRef, nil
	}
	switch parent.Type {
	case string(domain.ThreadTypeChannel):
		basis, err := authorizeAgentAttachmentChannelScope(ctx, queryer, workspaceID, memberID, parent.ID)
		if err != nil {
			return access.ScopePermissionBasis{}, access.ScopeRef{}, err
		}
		if basis.Kind == access.ScopePermissionBasisDirectMembership {
			basis.Kind = access.ScopePermissionBasisParentMembership
		}
		basis.DecisionSource = access.DecisionSourceThreadInherit
		basis.Scope = parentRef
		basis.Reason = "parent channel authorizes reply-thread attachment target"
		return basis, parentRef, nil
	case string(domain.ThreadTypeDM):
		basis, err := authorizeAgentAttachmentDMScope(ctx, queryer, memberID, parent.ID, access.ScopePermissionBasisParentDMParticipant, access.DecisionSourceThreadInherit, "parent dm authorizes reply-thread attachment target")
		if err != nil {
			return access.ScopePermissionBasis{}, access.ScopeRef{}, err
		}
		return basis, parentRef, nil
	default:
		return access.ScopePermissionBasis{}, access.ScopeRef{}, ErrForbidden
	}
}

func activeAttachmentScopeMembershipBasis(ctx context.Context, queryer coredb.QueryRower, memberID string, scope access.ScopeRef, basisKind access.ScopePermissionBasisKind, decisionSource access.DecisionSource, reason string) (access.ScopePermissionBasis, bool, error) {
	row := queryer.QueryRow(ctx, `
SELECT 1
FROM scope_memberships
WHERE scope_id = $1
  AND member_id = $2
  AND removed_at IS NULL
  AND archived_at IS NULL
  AND effective_to_seq IS NULL
  AND visibility IN ('public', 'private')
LIMIT 1
`, strings.TrimSpace(scope.ScopeID), strings.TrimSpace(memberID))

	var marker int
	if err := row.Scan(&marker); err != nil {
		if coredb.IsNotFound(err) {
			return access.ScopePermissionBasis{}, false, nil
		}
		return access.ScopePermissionBasis{}, false, fmt.Errorf("check attachment scope membership: %w", err)
	}
	return attachmentPermissionBasis(basisKind, scope, decisionSource, memberID, "", reason), true, nil
}

func authorizeAgentTaskAttachmentScopeForMember(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, taskScopeID string, operation access.ScopeTargetOperation) (access.ScopePermissionBasis, error) {
	taskRef := access.ScopeRef{ScopeID: strings.TrimSpace(taskScopeID), ScopeType: access.ScopeTargetKindTask}
	if operation == access.ScopeTargetOperationAttachmentUpload {
		allowed, err := resolveAgentTaskUploadScopeForMember(ctx, queryer, workspaceID, memberID, taskScopeID)
		if err != nil {
			return access.ScopePermissionBasis{}, err
		}
		if !allowed {
			return access.ScopePermissionBasis{}, ErrForbidden
		}
		return attachmentPermissionBasis(access.ScopePermissionBasisTaskBoardChannel, taskRef, "", memberID, "", "task board channel authorizes attachment upload"), nil
	}

	// Read authorization resolves the task's canonical channel from the active
	// Task/board authority (tasks.board_id -> task_boards), then verifies the
	// same-workspace active Agent+Machine+Member holds active membership on that
	// channel. Task scope_memberships is not an authorization fact here; it only
	// feeds watch/notification/participant state after authorization.
	// Client-supplied taskScope is only a location/consistency hint.
	if _, err := access.AuthorizeActiveAgentTaskBoardChannel(ctx, queryer, workspaceID, memberID, taskScopeID); err != nil {
		if errors.Is(err, access.ErrForbidden) || errors.Is(err, access.ErrNotFound) {
			return access.ScopePermissionBasis{}, ErrForbidden
		}
		return access.ScopePermissionBasis{}, err
	}
	return attachmentPermissionBasis(access.ScopePermissionBasisTaskBoardChannel, taskRef, "", memberID, "", "task board channel authorizes attachment read"), nil
}

func attachmentPermissionBasis(kind access.ScopePermissionBasisKind, scope access.ScopeRef, decisionSource access.DecisionSource, memberID string, ownerMemberID string, reason string) access.ScopePermissionBasis {
	return access.ScopePermissionBasis{
		Kind:           kind,
		Scope:          scope,
		DecisionSource: decisionSource,
		MemberID:       strings.TrimSpace(memberID),
		OwnerMemberID:  strings.TrimSpace(ownerMemberID),
		Reason:         strings.TrimSpace(reason),
	}
}
