package taskboard

import (
	"context"
	"strings"
)

type taskDiscussionWriteAuthority struct {
	TaskID                 string
	TaskScopeID            string
	ChannelID              string
	ActorMemberID          string
	ExecutionOwnerMemberID string
}

// AuthorizeUserTaskScopeDiscussionWrite verifies, without creating a message
// or changing Task state, that the Human currently has the same authority
// required by SendTaskScopeMessage. The Task row remains locked because Task
// discussion writes already serialize through that row; member, scope, and
// collaborator authority are evaluated with the ordinary writer reads in the
// caller's transaction snapshot rather than adding a second lock order.
func (txs TxStore) AuthorizeUserTaskScopeDiscussionWrite(
	ctx context.Context,
	userID string,
	workspaceID string,
	expectedMemberID string,
	taskScopeID string,
) error {
	_, err := txs.authorizeUserTaskScopeDiscussionWrite(
		ctx,
		userID,
		workspaceID,
		TaskRef{},
		taskScopeID,
		expectedMemberID,
	)
	return err
}

func (txs TxStore) authorizeUserTaskScopeDiscussionWrite(
	ctx context.Context,
	userID string,
	workspaceID string,
	taskRef TaskRef,
	taskScopeID string,
	expectedMemberID string,
) (taskDiscussionWriteAuthority, error) {
	tx := txs.tx
	taskID := strings.TrimSpace(taskRef.TaskID)
	if taskID == "" {
		var err error
		taskID, err = txs.loadTaskIDForTaskScope(ctx, tx, workspaceID, taskScopeID)
		if err != nil {
			return taskDiscussionWriteAuthority{}, err
		}
	}
	// TOCTOU fence: lock the canonical Task->board Channel row and revalidate the
	// full human Channel authority in a fixed order, so a concurrent
	// board-membership revoke / archive / effective-close / Channel delete /
	// member deactivate / Task delete / board move serializes deterministically
	// and the authority is revalidated at the same lock/version boundary that
	// changes it.
	channelID, err := txs.lockHumanTaskBoardChannelAuthority(ctx, tx, userID, workspaceID, taskID)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	resolvedTaskScopeID, err := txs.loadTaskScopeID(ctx, tx, taskID)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	if requested := strings.TrimSpace(taskScopeID); requested != "" && requested != strings.TrimSpace(resolvedTaskScopeID) {
		return taskDiscussionWriteAuthority{}, ErrForbidden
	}
	channelAccess, err := txs.loadAuthorizedUserTaskChannelAccess(ctx, tx, userID, workspaceID, channelID, taskWriteChannelAction)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	actorMemberID := strings.TrimSpace(channelAccess.WorkspaceMember.Member.ID)
	if expected := strings.TrimSpace(expectedMemberID); expected != "" && actorMemberID != expected {
		return taskDiscussionWriteAuthority{}, ErrForbidden
	}
	executionOwnerMemberID, _, _, err := txs.taskDiscussionAuthorizationAudit(
		ctx,
		tx,
		workspaceID,
		taskID,
		actorMemberID,
		false,
	)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	return taskDiscussionWriteAuthority{
		TaskID:                 taskID,
		TaskScopeID:            strings.TrimSpace(resolvedTaskScopeID),
		ChannelID:              strings.TrimSpace(channelID),
		ActorMemberID:          actorMemberID,
		ExecutionOwnerMemberID: strings.TrimSpace(executionOwnerMemberID),
	}, nil
}

// AuthorizeAgentTaskScopeDiscussionWrite verifies, without creating a message
// or changing Task state, that the Agent currently has the same active Task
// membership and author authority required by SendTaskScopeMessage.
func (txs TxStore) AuthorizeAgentTaskScopeDiscussionWrite(
	ctx context.Context,
	agent AgentActor,
	taskScopeID string,
) error {
	_, err := txs.authorizeAgentTaskScopeDiscussionWrite(
		ctx,
		agent,
		TaskRef{},
		taskScopeID,
	)
	return err
}

func (txs TxStore) authorizeAgentTaskScopeDiscussionWrite(
	ctx context.Context,
	agent AgentActor,
	taskRef TaskRef,
	taskScopeID string,
) (taskDiscussionWriteAuthority, error) {
	tx := txs.tx
	taskID := strings.TrimSpace(taskRef.TaskID)
	if taskID == "" {
		var err error
		taskID, err = txs.loadTaskIDForTaskScope(ctx, tx, agent.WorkspaceID, taskScopeID)
		if err != nil {
			return taskDiscussionWriteAuthority{}, err
		}
	}
	// TOCTOU fence: lock the canonical Task->board Channel row and the agent's
	// active membership on it in a fixed order, so a concurrent board-membership
	// revoke / Task delete / board move serializes deterministically and the
	// authority is revalidated at the same lock/version boundary that changes it.
	channelID, err := txs.lockTaskBoardChannelAuthority(ctx, tx, agent.WorkspaceID, taskID, agent.MemberID)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	resolvedTaskScopeID, err := txs.loadTaskScopeID(ctx, tx, taskID)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	if requested := strings.TrimSpace(taskScopeID); requested != "" && requested != strings.TrimSpace(resolvedTaskScopeID) {
		return taskDiscussionWriteAuthority{}, ErrForbidden
	}
	// Authorize through the single canonical Agent Task-channel authorizer: the
	// server resolved channelID from the active Task/board authority, and the
	// same-workspace active Agent+Machine+Member must currently hold active
	// membership on that channel scope. A client-supplied taskScope is only a
	// location/consistency hint, never the authorization fact.
	if err := txs.ensureAgentCanAccessTaskChannel(ctx, tx, agent, channelID); err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	executionOwnerMemberID, _, _, err := txs.taskDiscussionAuthorizationAudit(
		ctx,
		tx,
		agent.WorkspaceID,
		taskID,
		agent.MemberID,
		true,
	)
	if err != nil {
		return taskDiscussionWriteAuthority{}, err
	}
	return taskDiscussionWriteAuthority{
		TaskID:                 taskID,
		TaskScopeID:            strings.TrimSpace(resolvedTaskScopeID),
		ChannelID:              strings.TrimSpace(channelID),
		ActorMemberID:          strings.TrimSpace(agent.MemberID),
		ExecutionOwnerMemberID: strings.TrimSpace(executionOwnerMemberID),
	}, nil
}
