package taskboard

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/access"
)

// normalizeAuthorityFenceError maps the access package's closed non-enumerating
// errors to the taskboard sentinels so every Task mutation entry fails with the
// same ErrForbidden/ErrNotFound contract regardless of whether the fence ran
// through access.LockActiveAgentChannelAuthority or the human lock helper.
func normalizeAuthorityFenceError(err error) error {
	if err == nil {
		return nil
	}
	if errors.Is(err, access.ErrForbidden) {
		return ErrForbidden
	}
	if errors.Is(err, access.ErrNotFound) {
		return ErrNotFound
	}
	return err
}

// taskAuthorityFencer is the principal-typed in-transaction authority fence
// carried by the shared mutation cores. It locks the canonical Task/board row
// (deriving the board Channel server-side — never a caller-supplied Channel)
// and then revalidates the actor's active authority chain under the fixed lock
// order Task/board → scope → membership → member → agent → machine (or human
// member → scope → membership). The Agent and Human paths share one interface so
// create, existing-Task, collaborator, and read-state cores cannot drift.
type taskAuthorityFencer interface {
	// lockExistingTaskChannel locks the canonical Task row FOR UPDATE (deriving
	// the board Channel server-side from the locked task_boards row), then
	// revalidates the actor's active authority chain, returning the server-derived
	// Channel. Serializes against Task delete/move, board-membership revoke,
	// Channel delete/archive/status change, and member/Agent/Machine deactivation.
	lockExistingTaskChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskID string) (string, error)
	// lockBoardChannelForCreate locks the authoritative task_boards row FOR
	// UPDATE (no Task row exists yet for create), re-derives the board Channel
	// server-side, and revalidates the actor's active authority chain before any
	// Task/event/scope/participant row is inserted.
	lockBoardChannelForCreate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, boardID string) (string, error)
	// lockChannelAuthority revalidates the actor's active authority chain on an
	// already server-derived Channel (used by collaborator thread/channel-scope
	// targets where the scope itself is the authority, not a Task board).
	lockChannelAuthority(ctx context.Context, queryer coredb.QueryRower, channelID string) error
}

// agentTaskAuthorityFencer fences an Agent mutation: the active Agent+Machine+
// Member chain must hold active membership on the server-derived board Channel.
type agentTaskAuthorityFencer struct {
	workspaceID string
	memberID    string
}

func (f agentTaskAuthorityFencer) lockExistingTaskChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskID string) (string, error) {
	channelID, _, _, err := lockTaskRowForFence(ctx, queryer, workspaceID, taskID)
	if err != nil {
		return "", err
	}
	if err := access.LockActiveAgentChannelAuthority(ctx, queryer, f.workspaceID, f.memberID, channelID); err != nil {
		return "", normalizeAuthorityFenceError(err)
	}
	return channelID, nil
}

func (f agentTaskAuthorityFencer) lockBoardChannelForCreate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, boardID string) (string, error) {
	channelID, err := lockTaskBoardChannelRow(ctx, queryer, workspaceID, boardID)
	if err != nil {
		return "", err
	}
	// The create fence uses the principal-agnostic membership lock: a freshly
	// created (not-yet-activated) Agent whose member row and scope membership are
	// active may create tasks on a channel where it is a member — the authority
	// boundary is channel membership, not the Agent runtime activation state.
	// Mutating an EXISTING task still requires the full active Agent authority
	// (lockExistingTaskChannel), which retains agents.status='active'.
	if err := access.LockActiveMemberChannelAuthority(ctx, queryer, f.workspaceID, f.memberID, channelID); err != nil {
		return "", normalizeAuthorityFenceError(err)
	}
	return channelID, nil
}

func (f agentTaskAuthorityFencer) lockChannelAuthority(ctx context.Context, queryer coredb.QueryRower, channelID string) error {
	if strings.TrimSpace(channelID) == "" {
		return ErrForbidden
	}
	return normalizeAuthorityFenceError(access.LockActiveAgentChannelAuthority(ctx, queryer, f.workspaceID, f.memberID, channelID))
}

// humanTaskAuthorityFencer fences a human mutation: the active human member,
// the active non-archived channel scope, and the active non-archived
// non-effectively-closed membership must satisfy the write authorization.
type humanTaskAuthorityFencer struct {
	userID      string
	workspaceID string
	action      access.HumanAction
}

func (f humanTaskAuthorityFencer) lockExistingTaskChannel(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskID string) (string, error) {
	channelID, _, _, err := lockTaskRowForFence(ctx, queryer, workspaceID, taskID)
	if err != nil {
		return "", err
	}
	if _, err := access.LockActiveHumanChannelAuthority(ctx, queryer, f.userID, f.workspaceID, channelID, f.action); err != nil {
		return "", normalizeAuthorityFenceError(err)
	}
	return channelID, nil
}

func (f humanTaskAuthorityFencer) lockBoardChannelForCreate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, boardID string) (string, error) {
	channelID, err := lockTaskBoardChannelRow(ctx, queryer, workspaceID, boardID)
	if err != nil {
		return "", err
	}
	if _, err := access.LockActiveHumanChannelAuthority(ctx, queryer, f.userID, f.workspaceID, channelID, f.action); err != nil {
		return "", normalizeAuthorityFenceError(err)
	}
	return channelID, nil
}

func (f humanTaskAuthorityFencer) lockChannelAuthority(ctx context.Context, queryer coredb.QueryRower, channelID string) error {
	if strings.TrimSpace(channelID) == "" {
		return ErrForbidden
	}
	_, err := access.LockActiveHumanChannelAuthority(ctx, queryer, f.userID, f.workspaceID, channelID, f.action)
	return normalizeAuthorityFenceError(err)
}

// agentTaskFencerFor returns the Agent-typed fence for one Agent actor.
func agentTaskFencerFor(agent AgentActor) taskAuthorityFencer {
	return agentTaskAuthorityFencer{workspaceID: agent.WorkspaceID, memberID: agent.MemberID}
}

// humanTaskFencerFor returns the Human-typed fence for one human user acting on
// a Task channel with the given write action.
func humanTaskFencerFor(userID string, workspaceID string, action access.HumanAction) taskAuthorityFencer {
	return humanTaskAuthorityFencer{userID: userID, workspaceID: workspaceID, action: action}
}

// lockTaskRowForFence is the canonical Task+board row lock (FOR UPDATE) that
// derives the board Channel server-side. It is shared by every fencer so the
// lock order is identical.
func lockTaskRowForFence(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskID string) (string, string, string, error) {
	row := queryer.QueryRow(ctx, `
SELECT tb.channel_id, ct.status, COALESCE(ct.assignee_member_id, '')
FROM tasks ct
JOIN scopes task_scope
  ON task_scope.id = ct.task_scope_id
 AND task_scope.workspace_id = $1
 AND task_scope.type = 'task'
 AND task_scope.status = 'active'
 AND task_scope.archived_at IS NULL
JOIN task_boards tb
  ON tb.id = ct.board_id
 AND tb.workspace_id = $1
JOIN scopes channel_scope
  ON channel_scope.id = tb.channel_id
 AND channel_scope.workspace_id = $1
 AND channel_scope.type = 'channel'
 AND channel_scope.status = 'active'
 AND channel_scope.archived_at IS NULL
WHERE ct.channel_id = tb.channel_id
  AND ct.id = $2
FOR UPDATE
`, strings.TrimSpace(workspaceID), strings.TrimSpace(taskID))
	var channelID string
	var status string
	var assigneeMemberID string
	if err := row.Scan(&channelID, &status, &assigneeMemberID); err != nil {
		if coredb.IsNotFound(err) {
			return "", "", "", ErrNotFound
		}
		return "", "", "", fmt.Errorf("lock task: %w", err)
	}
	return channelID, status, assigneeMemberID, nil
}

// lockTaskBoardChannelRow is the create-path board row lock (FOR UPDATE) that
// derives the board Channel server-side. A missing/deleted board fails closed
// non-enumerating.
func lockTaskBoardChannelRow(ctx context.Context, queryer coredb.QueryRower, workspaceID string, boardID string) (string, error) {
	var channelID string
	if err := queryer.QueryRow(ctx, `
SELECT tb.channel_id
FROM task_boards tb
JOIN scopes channel_scope
  ON channel_scope.id = tb.channel_id
 AND channel_scope.workspace_id = tb.workspace_id
 AND channel_scope.type = 'channel'
 AND channel_scope.status = 'active'
 AND channel_scope.archived_at IS NULL
WHERE tb.id = $1
  AND tb.workspace_id = $2
FOR UPDATE
`, strings.TrimSpace(boardID), strings.TrimSpace(workspaceID)).Scan(&channelID); err != nil {
		if coredb.IsNotFound(err) {
			return "", ErrNotFound
		}
		return "", fmt.Errorf("lock task board channel authority for create: %w", err)
	}
	return channelID, nil
}

// lockTaskBoardChannelAuthority is the Agent-typed Task fence used by the
// Agent mutation entries. It locks the canonical Task row then the active
// Agent+Machine+Member authority chain on the server-derived board Channel.
func (txs TxStore) lockTaskBoardChannelAuthority(ctx context.Context, queryer coredb.QueryRower, workspaceID string, taskID string, memberID string) (channelID string, err error) {
	if strings.TrimSpace(taskID) == "" || strings.TrimSpace(memberID) == "" {
		return "", ErrForbidden
	}
	return agentTaskAuthorityFencer{workspaceID: workspaceID, memberID: memberID}.lockExistingTaskChannel(ctx, queryer, workspaceID, taskID)
}

// lockHumanTaskBoardChannelAuthority is the Human-typed Task fence used by the
// human mutation entries: lock the canonical Task row then revalidate the active
// human member + active non-archived channel scope + active membership under the
// write authorization.
func (txs TxStore) lockHumanTaskBoardChannelAuthority(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, taskID string) (channelID string, err error) {
	if strings.TrimSpace(taskID) == "" || strings.TrimSpace(userID) == "" {
		return "", ErrForbidden
	}
	return humanTaskFencerFor(userID, workspaceID, taskWriteChannelAction).lockExistingTaskChannel(ctx, queryer, workspaceID, taskID)
}

// lockTaskScopeCollaboratorAuthority is the in-transaction fence for Task
// collaborator mutations (add/remove/leave). For a task-scope target it locks
// the canonical Task row (deriving the board Channel server-side) then the
// actor's active authority chain — the same shared lock order as every other
// Task mutation. For a channel-scope target it locks the actor's active
// authority chain on the target channel scope directly (that scope is its own
// authority, not a Task board). For a thread-scope target it locks the active
// thread scope + the actor's active membership on it (a thread is NOT a
// channel; the channel authority would over-deny). A missing/revoked/
// deactivated/archived fact fails closed non-enumerating.
func (txs TxStore) lockTaskScopeCollaboratorAuthority(ctx context.Context, tx coredb.Tx, workspaceID string, target taskCollaboratorTarget, fencer taskAuthorityFencer) error {
	if target.ScopeType == taskCollaboratorScopeTask {
		if target.TaskID == "" {
			return ErrForbidden
		}
		if _, err := fencer.lockExistingTaskChannel(ctx, tx, workspaceID, target.TaskID); err != nil {
			return err
		}
		return nil
	}
	scopeID := strings.TrimSpace(target.ScopeID)
	if scopeID == "" {
		return ErrForbidden
	}
	if target.ScopeType == taskCollaboratorScopeThread {
		// A thread scope is its own authority: lock the active thread scope + the
		// actor's active membership on it, never the channel authority.
		return txs.lockThreadCollaboratorAuthority(ctx, tx, workspaceID, scopeID, fencer)
	}
	return fencer.lockChannelAuthority(ctx, tx, scopeID)
}

// lockThreadCollaboratorAuthority locks the active thread scope and the actor's
// active membership on it. The Agent fencer uses the Agent channel authority's
// scope+membership prefix via a thread-scope variant; the human fencer uses the
// human thread fence. Both fail closed non-enumerating.
func (txs TxStore) lockThreadCollaboratorAuthority(ctx context.Context, tx coredb.Tx, workspaceID string, threadScopeID string, fencer taskAuthorityFencer) error {
	switch f := fencer.(type) {
	case humanTaskAuthorityFencer:
		if err := access.LockActiveHumanThreadAuthority(ctx, tx, f.userID, f.workspaceID, threadScopeID); err != nil {
			return normalizeAuthorityFenceError(err)
		}
		return nil
	case agentTaskAuthorityFencer:
		if err := access.LockActiveAgentThreadAuthority(ctx, tx, f.workspaceID, f.memberID, threadScopeID); err != nil {
			return normalizeAuthorityFenceError(err)
		}
		return nil
	}
	return ErrForbidden
}

// lockActiveAgentBoardChannelAuthority is the Agent-typed membership fence for
// the direct agent call sites (claim/unclaim/delete agent paths) that already
// hold the Task row lock. It revalidates the full active Agent+Machine+Member
// chain on the server-derived board Channel — semantically identical to
// AuthorizeActiveAgentChannelMembership under lock.
func (txs TxStore) lockActiveAgentBoardChannelAuthority(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelID string, memberID string) error {
	if strings.TrimSpace(channelID) == "" || strings.TrimSpace(memberID) == "" {
		return ErrForbidden
	}
	return normalizeAuthorityFenceError(access.LockActiveAgentChannelAuthority(ctx, queryer, workspaceID, memberID, channelID))
}

// lockActiveHumanBoardChannelAuthority is the Human-typed membership fence for
// the human mutation paths that already hold the Task row lock. It revalidates
// the active human member + active non-archived channel scope + active
// membership under the write authorization.
func (txs TxStore) lockActiveHumanBoardChannelAuthority(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, channelID string) error {
	if strings.TrimSpace(channelID) == "" || strings.TrimSpace(userID) == "" {
		return ErrForbidden
	}
	_, err := access.LockActiveHumanChannelAuthority(ctx, queryer, userID, workspaceID, channelID, taskWriteChannelAction)
	return normalizeAuthorityFenceError(err)
}
