package access

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

	"github.com/tidbcloud/AgentTeam/internal/core/domain"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
)

// activeHumanChannelScopePredicate is the shared predicate for an active,
// non-archived, same-workspace channel scope used by the human locking fence.
// Params: $1 workspaceID, $2 channelID.
const activeHumanChannelScopePredicate = `
	s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL`

// activeHumanMembershipPredicate is the shared predicate for an active,
// non-archived, non-effectively-closed human board membership. It mirrors the
// membership fact that makes a human Joined in LoadHumanChannelAccess (public
// visibility) plus the archived/effective-close guards the write policy needs.
// Params: $1 channelID, $2 memberID.
const activeHumanMembershipPredicate = `
	sm.scope_id = $1
  AND sm.member_id = $2
  AND sm.visibility = 'public'
  AND sm.removed_at IS NULL
  AND sm.archived_at IS NULL
  AND sm.effective_to_seq IS NULL`

// LookupChannelForUpdate is the locking read of the channel scope row (the
// active, non-archived channel with its profile) so a concurrent Channel
// delete/status-change/archive serializes against the human mutation fence. It
// mirrors LookupChannel but only for the channel kind.
func LookupChannelForUpdate(ctx context.Context, queryer coredb.QueryRower, workspaceID string, channelID string) (Channel, error) {
	row := queryer.QueryRow(ctx, `
SELECT
    s.id,
    s.id,
    s.type,
    COALESCE(NULLIF(p.slug, ''), CONCAT('thread-', COALESCE(s.root_message_short_id, s.id))),
    COALESCE(NULLIF(p.display_name, ''), NULLIF(p.slug, ''), CONCAT('thread-', COALESCE(s.root_message_short_id, s.id))),
    COALESCE(p.description, ''),
    COALESCE(p.visibility, 'private'),
    COALESCE(NULLIF(p.subsystem, ''), 'im'),
    p.owner_member_id
FROM scopes s
LEFT JOIN scope_channel_profiles p ON p.scope_id = s.id
WHERE s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL
  AND p.scope_id IS NOT NULL
FOR UPDATE
`, workspaceID, channelID)

	channel, err := scanChannel(row, "lock channel scope")
	if err != nil {
		return Channel{}, err
	}
	if err := annotateChannelTeam(ctx, queryer, workspaceID, &channel); err != nil {
		return Channel{}, err
	}
	return channel, nil
}

// LockActiveHumanChannelMembership acquires the locking read on the human's
// active board-channel membership row. A concurrent membership revoke /
// archive / effective-close serializes against this lock, and a missing or
// already-closed membership fails closed non-enumerating.
func LockActiveHumanChannelMembership(ctx context.Context, queryer coredb.QueryRower, channelID string, memberID string) (bool, error) {
	channelID = strings.TrimSpace(channelID)
	memberID = strings.TrimSpace(memberID)
	if channelID == "" || memberID == "" {
		return false, ErrForbidden
	}
	var one int
	if err := queryer.QueryRow(ctx, `
SELECT 1
FROM scope_memberships sm
WHERE`+activeHumanMembershipPredicate+`
FOR UPDATE
`, channelID, memberID).Scan(&one); err != nil {
		if coredb.IsNotFound(err) {
			return false, ErrForbidden
		}
		return false, fmt.Errorf("lock active human channel membership: %w", err)
	}
	return true, nil
}

// activeHumanMemberPredicate is the shared predicate for an active human
// workspace member row. Params: $1 workspaceID, $2 userID, $3 memberID.
const activeHumanMemberPredicate = `
	m.workspace_id = $1
  AND m.id = $3
  AND m.user_id = $2
  AND m.type = 'human'
  AND m.status = 'active'`

// resolveHumanWorkspaceMemberID resolves the stable human member ID with a
// NON-LOCKING lookup only. The authoritative locked re-check happens last in
// LockActiveHumanChannelAuthority (after scope and membership locks), so a
// concurrent member deactivation/reassignment between ID resolution and the
// ordered locks fails closed. Absent/deactivated/cross-workspace members fail
// closed non-enumerating.
func resolveHumanWorkspaceMemberID(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string) (HumanWorkspaceMember, error) {
	membership, err := LoadHumanWorkspaceMember(ctx, queryer, userID, workspaceID)
	if err != nil {
		return HumanWorkspaceMember{}, err
	}
	return membership, nil
}

// lockActiveHumanMember is the final ordered locking read on the exact human
// member row resolved before the Channel-membership lock. It re-checks the
// EXACT resolved member ID together with user ID, workspace, type=human, and
// status=active under FOR UPDATE, so any identity replacement/reassignment
// between ID resolution and lock fails closed non-enumerating: if the resolved
// member A was deactivated/reassigned and a different member B was activated for
// the same user, the lock on `id = A` observes the loss and never authorizes B.
func lockActiveHumanMember(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, memberID string) (HumanWorkspaceMember, error) {
	var lockedMemberID string
	var displayName string
	var memberUserID string
	var workspaceRole string
	if err := queryer.QueryRow(ctx, `
SELECT m.id, COALESCE(m.display_name, ''), COALESCE(m.user_id, ''), COALESCE(m.workspace_role, '')
FROM members m
WHERE`+activeHumanMemberPredicate+`
FOR UPDATE
`, workspaceID, userID, memberID).Scan(&lockedMemberID, &displayName, &memberUserID, &workspaceRole); err != nil {
		if coredb.IsNotFound(err) {
			return HumanWorkspaceMember{}, ErrForbidden
		}
		return HumanWorkspaceMember{}, fmt.Errorf("lock active human member: %w", err)
	}
	// The resolved member identity must be preserved: if the locked row is not
	// the exact member we resolved, fail closed (never authorize a replacement).
	if strings.TrimSpace(lockedMemberID) != strings.TrimSpace(memberID) {
		return HumanWorkspaceMember{}, ErrForbidden
	}
	membership := HumanWorkspaceMember{}
	membership.Member.ID = lockedMemberID
	membership.Member.DisplayName = displayName
	membership.Member.WorkspaceID = workspaceID
	membership.Member.Type = "human"
	uid := memberUserID
	membership.Member.UserID = &uid
	membership.WorkspaceRole = workspaceRole
	return membership, nil
}

// LockActiveHumanChannelAuthority is the ordered locking fence that makes the
// transaction-side human Channel authority identical to
// LoadAuthorizedHumanChannelAccess. It implements the documented global lock
// order exactly: the Task/board is already locked by the taskboard caller;
// resolve the stable human member ID with a NON-LOCKING lookup only, then lock
// the active non-archived Channel scope, then the exact active/non-archived/
// non-effectively-closed scope membership, then the human member row LAST (which
// re-checks user ID, workspace, type=human, status=active). Only after all
// ordered facts are locked does it build HumanChannelAccess/team/owner policy
// and authorize the write. Missing/deactivated/deleted/archived/cross-workspace/
// effectively-closed facts and denied write policy fail closed non-enumerating.
func LockActiveHumanChannelAuthority(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, channelID string, action HumanAction) (HumanChannelAccess, error) {
	userID = strings.TrimSpace(userID)
	workspaceID = strings.TrimSpace(workspaceID)
	channelID = strings.TrimSpace(channelID)
	if userID == "" || workspaceID == "" || channelID == "" {
		return HumanChannelAccess{}, ErrForbidden
	}

	// 1. Resolve the stable human member ID with a NON-LOCKING lookup only.
	resolvedMember, err := resolveHumanWorkspaceMemberID(ctx, queryer, userID, workspaceID)
	if err != nil {
		return HumanChannelAccess{}, err
	}
	memberID := resolvedMember.Member.ID

	// 2. Active non-archived channel scope (FOR UPDATE).
	channel, err := LookupChannelForUpdate(ctx, queryer, workspaceID, channelID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return HumanChannelAccess{}, ErrForbidden
		}
		return HumanChannelAccess{}, err
	}

	// 3. Active non-archived non-effectively-closed membership (FOR UPDATE).
	joined, err := LockActiveHumanChannelMembership(ctx, queryer, channelID, memberID)
	if err != nil {
		return HumanChannelAccess{}, err
	}

	// 4. Human member row LAST (FOR UPDATE), re-checking the EXACT resolved
	//    member ID together with user ID / workspace / type=human / status=active
	//    so a concurrent deactivation or identity replacement/reassignment fails
	//    closed (never authorizing a different member row).
	membership, err := lockActiveHumanMember(ctx, queryer, userID, workspaceID, memberID)
	if err != nil {
		return HumanChannelAccess{}, err
	}

	teamManaged := strings.TrimSpace(channel.TeamID) != ""
	isOwner := joined && !teamManaged && channel.OwnerMemberID != nil && *channel.OwnerMemberID == membership.Member.ID
	channelAccess := HumanChannelAccess{
		WorkspaceMember: membership,
		Channel:         channel,
		Joined:          joined,
		CanInvite:       joined && !teamManaged,
		IsOwner:         isOwner,
	}

	// 5. Apply the requested write authorization under the locked facts.
	request := NewHumanAuthorizationRequest(userID, workspaceID, membership.Member.ID, channelID, action)
	if _, err := DecideHumanChannelAuthorization(channelAccess, request); err != nil {
		return HumanChannelAccess{}, err
	}
	return channelAccess, nil
}

// LockActiveHumanThreadAuthority is the ordered locking fence for a THREAD scope
// collaborator target (add/remove/leave). A thread scope is its own authority:
// the actor must hold active membership on the active non-archived thread scope
// (or, for a channel-parent thread, the active parent channel). It locks the
// thread scope row FOR UPDATE, then the actor's active membership on the
// thread's own scope. Missing/inactive/archived/removed membership fails closed
// non-enumerating.
func LockActiveHumanThreadAuthority(ctx context.Context, queryer coredb.QueryRower, userID string, workspaceID string, threadScopeID string) error {
	userID = strings.TrimSpace(userID)
	workspaceID = strings.TrimSpace(workspaceID)
	threadScopeID = strings.TrimSpace(threadScopeID)
	if userID == "" || workspaceID == "" || threadScopeID == "" {
		return ErrForbidden
	}
	// Resolve the member id non-locking; the authoritative locked re-check is the
	// parent authorization below.
	membership, err := resolveHumanWorkspaceMemberID(ctx, queryer, userID, workspaceID)
	if err != nil {
		return err
	}
	// 1. Lock the active non-archived thread scope and read its parent (FOR
	//    UPDATE).
	var parentScopeID string
	var parentType string
	if err := queryer.QueryRow(ctx, `
SELECT COALESCE(scopes.parent_scope_id, ''), COALESCE(p.type, '')
FROM scopes
LEFT JOIN scopes p ON p.id = scopes.parent_scope_id AND p.workspace_id = scopes.workspace_id
WHERE scopes.workspace_id = $1
  AND scopes.id = $2
  AND scopes.type = 'thread'
  AND scopes.status = 'active'
  AND scopes.archived_at IS NULL
FOR UPDATE
`, workspaceID, threadScopeID).Scan(&parentScopeID, &parentType); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active thread scope: %w", err)
	}
	parentScopeID = strings.TrimSpace(parentScopeID)
	// 2. Lock the active parent scope (FOR UPDATE) in deterministic order
	//    (thread, then parent).
	var parentActive int
	if err := queryer.QueryRow(ctx, `
SELECT 1
FROM scopes
WHERE workspace_id = $1
  AND id = $2
  AND status = 'active'
  AND archived_at IS NULL
FOR UPDATE
`, workspaceID, parentScopeID).Scan(&parentActive); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active thread parent scope: %w", err)
	}
	// 3. Authorize exactly as LoadHumanThreadAccess / AuthorizeHumanThreadAction:
	//    a channel-parent thread authorizes active parent Channel membership; a
	//    DM-parent thread authorizes active parent DM participation (or the
	//    canonical active child-thread participation).
	switch strings.TrimSpace(parentType) {
	case string(domain.ThreadTypeChannel):
		// Channel-parent: the actor must hold active membership on the PARENT
		// channel (never a direct thread-scope membership).
		if _, err := LockActiveHumanChannelMembership(ctx, queryer, parentScopeID, membership.Member.ID); err != nil {
			return err
		}
	case string(domain.ThreadTypeDM):
		// DM-parent: the actor must participate in the parent DM. A DM scope's
		// membership IS the participation record; lock it on the parent DM.
		if _, err := LockActiveHumanChannelMembership(ctx, queryer, parentScopeID, membership.Member.ID); err != nil {
			return err
		}
	default:
		return ErrForbidden
	}
	return nil
}
