package access

import (
	"context"
	"fmt"
	"strings"

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

// ---------------------------------------------------------------------------
// Single-source active-Agent authority predicates.
//
// AuthorizeActiveAgentChannelMembership (the one-shot read JOIN) and
// LockActiveAgentChannelAuthority (the ordered per-row locking fence) build
// from THIS shared set of security predicates. A security fact added to one
// path but not the other — an independent `status = 'active'` on a different
// alias, a new `type`/`archived_at`/`removed_at`/`effective_to_seq`/`visibility`
// condition, or a new join — fails the structural parity test
// (TestAgentReadLockPredicateParity) because the security-condition token sets
// diverge. Only the structural linkage (join vs ordered lock) differs, and only
// in non-security fragments (e.g. `sm.scope_id = s.id` vs `sm.scope_id = $2`).
// ---------------------------------------------------------------------------

// activeAgentChannelScopePredicate is the shared scope fact: active, non-archived,
// same-workspace channel scope. Params: $1 workspaceID, $2 channelID.
const activeAgentChannelScopePredicate = `
	s.workspace_id = $1
  AND s.id = $2
  AND s.type = 'channel'
  AND s.status = 'active'
  AND s.archived_at IS NULL`

// activeAgentMembershipSecurity is the shared membership security fact:
// active, non-archived, non-effectively-closed board membership. Params:
// $3 memberID. The scope linkage differs between read (`sm.scope_id = s.id`)
// and lock (`sm.scope_id = $2`) but every security condition is shared.
const activeAgentMembershipSecurity = `
	sm.member_id = $3
  AND sm.visibility IN ('public', 'private')
  AND sm.removed_at IS NULL
  AND sm.archived_at IS NULL
  AND sm.effective_to_seq IS NULL`

// activeAgentMembershipReadScope is the read-JOIN scope linkage only.
const activeAgentMembershipReadScope = `sm.scope_id = s.id`

// activeAgentMembershipLockScope is the lock-WHERE scope linkage only.
const activeAgentMembershipLockScope = `sm.scope_id = $2`

// activeAgentMemberSecurity is the shared member fact: active, same-workspace
// member of type agent. Params: $1 workspaceID, $3 memberID.
const activeAgentMemberSecurity = `
	m.workspace_id = $1
  AND m.id = $3
  AND m.type = 'agent'
  AND m.status = 'active'`

// activeAgentSecurity is the shared same-workspace agent fact. Durable Agent
// status is not an authority or liveness signal.
// Params: $1 workspaceID.
// No Agent status predicate is intentionally applied.
const activeAgentSecurity = `
	a.workspace_id = $1`

// activeAgentReadMemberLinkage is the read-JOIN agent member linkage.
const activeAgentReadMemberLinkage = `a.member_id = $3`

// activeAgentLockMemberLinkage is the lock-WHERE agent member linkage.
const activeAgentLockMemberLinkage = `a.member_id = $2`

// activeMachineStatusSecurity is the shared machine status fact. The workspace/
// machine-id linkage differs between read (`mac.workspace_id = a.workspace_id
// AND mac.id = a.machine_id`) and lock (`mac.workspace_id = $1 AND mac.id = $2`)
// but the active-status security fact is shared.
const activeMachineStatusSecurity = `mac.status = 'active'`

// LockActiveAgentThreadAuthority is the Agent-scope thread fence: locks the
// active non-archived thread scope and its active parent scope in deterministic
// order (thread, then parent), then revalidates the active Agent+Machine+Member
// chain against the PARENT authority (channel-parent threads authorize active
// parent Channel membership; DM-parent threads authorize parent DM
// participation) — matching the human thread fence and the read semantics of
// LoadHumanThreadAccess/AuthorizeHumanThreadAction. A thread is never a channel,
// so the channel-only LockActiveAgentChannelAuthority cannot be used. Fails
// closed non-enumerating.
func LockActiveAgentThreadAuthority(ctx context.Context, queryer coredb.QueryRower, workspaceID string, memberID string, threadScopeID string) error {
	workspaceID = strings.TrimSpace(workspaceID)
	memberID = strings.TrimSpace(memberID)
	threadScopeID = strings.TrimSpace(threadScopeID)
	if workspaceID == "" || memberID == "" || threadScopeID == "" {
		return ErrForbidden
	}
	// 1. Lock the active non-archived thread scope and read its parent.
	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 agent thread scope: %w", err)
	}
	parentScopeID = strings.TrimSpace(parentScopeID)
	if parentScopeID == "" {
		return ErrForbidden
	}
	// 2. Lock the active parent scope (FOR UPDATE) in deterministic order.
	var one 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(&one); err != nil {
		if coredb.IsNotFound(err) {
			return ErrForbidden
		}
		return fmt.Errorf("lock active agent thread parent scope: %w", err)
	}
	// 3. Revalidate the active Agent+Machine+Member chain against the parent
	//    scope (channel or DM) — never the thread scope directly.
	switch strings.TrimSpace(parentType) {
	case string(domain.ThreadTypeChannel), string(domain.ThreadTypeDM):
		if err := LockActiveAgentChannelAuthority(ctx, queryer, workspaceID, memberID, parentScopeID); err != nil {
			return err
		}
	default:
		return ErrForbidden
	}
	return nil
}
