package extension

import (
	"context"
	"database/sql"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

// ListAgentAppAuthorizationDecisions lists every explicit App authorization
// decision for one Agent, including disabled opt-outs. An absent row means the
// current App policy is default-allow; an active row is an explicit allow; a
// disabled or removed row suppresses default-allow.
func (tx *TxStore) ListAgentAppAuthorizationDecisions(
	ctx context.Context,
	workspaceID string,
	agentID string,
) ([]serviceactor.PrincipalAuthorization, error) {
	return tx.listAgentAppAuthorizationDecisions(ctx, workspaceID, agentID, true)
}

// ListAgentAppAuthorizationDecisionsSnapshot is the non-locking variant used
// by the invocation/reauthorization path. Locking these rows FOR UPDATE
// deadlocks with a concurrent authorization lifecycle UPDATE: the principal
// index carries the status column, so a revoke/disable must delete and
// re-insert index entries the ordered locking scan holds. The caller fences
// staleness with the durable catalog-state compare instead of row locks.
func (tx *TxStore) ListAgentAppAuthorizationDecisionsSnapshot(
	ctx context.Context,
	workspaceID string,
	agentID string,
) ([]serviceactor.PrincipalAuthorization, error) {
	return tx.listAgentAppAuthorizationDecisions(ctx, workspaceID, agentID, false)
}

func (tx *TxStore) listAgentAppAuthorizationDecisions(
	ctx context.Context,
	workspaceID string,
	agentID string,
	forUpdate bool,
) ([]serviceactor.PrincipalAuthorization, error) {
	if tx == nil || tx.db == nil {
		return nil, fmt.Errorf("extension store transaction is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	agentID = strings.TrimSpace(agentID)
	if workspaceID == "" || agentID == "" {
		return nil, fmt.Errorf("workspaceId and agentId are required")
	}
	if err := tx.AssertWorkspaceAgent(ctx, workspaceID, agentID); err != nil {
		return nil, err
	}
	lockClause := ""
	if forUpdate {
		lockClause = "FOR UPDATE"
	}
	rows, err := tx.db.Query(ctx, `
SELECT authorization.id, authorization.workspace_id,
       authorization.service_definition_id,
       authorization.service_installation_id, authorization.service_kind,
       authorization.principal_kind, authorization.principal_ref,
       authorization.status, authorization.disabled_reason,
       authorization.scopes_json, authorization.authorized_by_member_id,
       authorization.policy_version, authorization.metadata,
       authorization.created_at, authorization.updated_at,
       authorization.disabled_at, authorization.removed_at,
       authorization.active_authorization_key
FROM service_principal_authorizations authorization
JOIN service_installations installation
  ON installation.id = authorization.service_installation_id
 AND installation.service_kind = 'app'
WHERE authorization.workspace_id = $1
  AND authorization.principal_kind = $2
  AND authorization.principal_ref = $3
ORDER BY authorization.service_installation_id ASC,
         authorization.created_at ASC,
         authorization.id ASC
`+lockClause, workspaceID, PrincipalKindAgent, agentID)
	if err != nil {
		return nil, fmt.Errorf(
			"list Agent App authorization decisions: %w",
			err,
		)
	}
	defer rows.Close()
	authorizations := make([]serviceactor.PrincipalAuthorization, 0)
	for rows.Next() {
		authorization, err := scanAgentAuthorization(rows)
		if err != nil {
			return nil, fmt.Errorf(
				"scan Agent App authorization decision: %w",
				err,
			)
		}
		authorizations = append(authorizations, authorization)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf(
			"iterate Agent App authorization decisions: %w",
			err,
		)
	}
	return authorizations, nil
}

// ListAgentAppAuthorizationDecisionsForAgents batch-loads every explicit App
// authorization decision for the requested Agents. It is used by the Workspace
// reconcile worker so App/connection/selection changes do not pay one
// authorization query per Agent.
func (tx *TxStore) ListAgentAppAuthorizationDecisionsForAgents(
	ctx context.Context,
	workspaceID string,
	agentIDs []string,
) (map[string][]serviceactor.PrincipalAuthorization, error) {
	if tx == nil || tx.db == nil {
		return nil, fmt.Errorf("extension store transaction is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	agentIDs = normalizeNonEmptyStrings(agentIDs)
	if workspaceID == "" {
		return nil, fmt.Errorf("workspaceId is required")
	}
	decisions := make(map[string][]serviceactor.PrincipalAuthorization, len(agentIDs))
	for _, agentID := range agentIDs {
		decisions[agentID] = []serviceactor.PrincipalAuthorization{}
	}
	if len(agentIDs) == 0 {
		return decisions, nil
	}
	requestedAgentsCTE, requestedAgentArgs, _, err := coredb.BuildStringValuesCTE("requested_agents", "agent_id", agentIDs, 3)
	if err != nil {
		return nil, fmt.Errorf("build requested Agent authorization CTE: %w", err)
	}
	args := append([]any{workspaceID, PrincipalKindAgent}, requestedAgentArgs...)
	rows, err := tx.db.Query(ctx, fmt.Sprintf(`
WITH %s
SELECT authorization.id, authorization.workspace_id,
       authorization.service_definition_id,
       authorization.service_installation_id, authorization.service_kind,
       authorization.principal_kind, authorization.principal_ref,
       authorization.status, authorization.disabled_reason,
       authorization.scopes_json, authorization.authorized_by_member_id,
       authorization.policy_version, authorization.metadata,
       authorization.created_at, authorization.updated_at,
       authorization.disabled_at, authorization.removed_at,
       authorization.active_authorization_key
FROM requested_agents requested
JOIN service_principal_authorizations authorization
  ON authorization.workspace_id = $1
 AND authorization.principal_kind = $2
 AND authorization.principal_ref = requested.agent_id
JOIN service_installations installation
  ON installation.id = authorization.service_installation_id
 AND installation.service_kind = 'app'
ORDER BY authorization.principal_ref ASC,
         authorization.service_installation_id ASC,
         authorization.created_at ASC,
         authorization.id ASC
FOR UPDATE`, requestedAgentsCTE), args...)
	if err != nil {
		return nil, fmt.Errorf("list Agent App authorization decisions batch: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		authorization, err := scanAgentAuthorization(rows)
		if err != nil {
			return nil, fmt.Errorf("scan Agent App authorization decision batch: %w", err)
		}
		if authorization.WorkspaceID != workspaceID {
			return nil, fmt.Errorf("App authorization %q Workspace does not match the batch", authorization.ID)
		}
		if _, ok := decisions[authorization.PrincipalRef]; !ok {
			return nil, fmt.Errorf("App authorization %q Agent does not match the batch", authorization.ID)
		}
		decisions[authorization.PrincipalRef] = append(decisions[authorization.PrincipalRef], authorization)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate Agent App authorization decisions batch: %w", err)
	}
	return decisions, nil
}

// AssertWorkspaceAgent verifies that agentID is a fully-realized active Agent
// of workspaceID. Raw selected identities are compared byte-for-byte so a
// TiDB/MySQL PAD SPACE match cannot hide a trailing-space tamper.
func (tx *TxStore) AssertWorkspaceAgent(
	ctx context.Context,
	workspaceID string,
	agentID string,
) error {
	if tx == nil || tx.db == nil {
		return fmt.Errorf("extension store transaction is not configured")
	}
	if workspaceID == "" || agentID == "" {
		return storeerr.Forbidden("agent membership could not be verified")
	}
	var (
		persistedWorkspaceID string
		persistedAgentID     string
		agentStatus          string
		memberWorkspaceID    string
		memberType           string
		memberStatus         string
		machineWorkspaceID   string
		machineStatus        string
	)
	err := tx.db.QueryRow(ctx, `
SELECT agent.workspace_id, agent.id, agent.status,
       member.workspace_id, member.type, member.status,
       machine.workspace_id, machine.status
FROM agents agent
JOIN members member
  ON member.id = agent.member_id
JOIN machines machine
  ON machine.id = agent.machine_id
WHERE agent.workspace_id = $1
  AND agent.id = $2
LIMIT 1`, workspaceID, agentID).Scan(
		&persistedWorkspaceID,
		&persistedAgentID,
		&agentStatus,
		&memberWorkspaceID,
		&memberType,
		&memberStatus,
		&machineWorkspaceID,
		&machineStatus,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return storeerr.Forbidden(
				"agent does not belong to this workspace",
			)
		}
		return fmt.Errorf("verify workspace agent: %w", err)
	}
	if persistedWorkspaceID != workspaceID ||
		persistedAgentID != agentID ||
		agentStatus == "archived" ||
		memberWorkspaceID != workspaceID ||
		memberType != "agent" ||
		memberStatus != serviceactor.StatusActive ||
		machineWorkspaceID != workspaceID ||
		machineStatus != serviceactor.StatusActive {
		return storeerr.Forbidden(
			"agent identity does not match the Workspace",
		)
	}
	return nil
}

// AssertWorkspaceAgentMember verifies the complete authenticated Agent
// principal identity and locks it for the authoritative invocation preflight.
func (tx *TxStore) AssertWorkspaceAgentMember(
	ctx context.Context,
	workspaceID string,
	agentID string,
	memberID string,
) error {
	if tx == nil || tx.db == nil {
		return fmt.Errorf("extension store transaction is not configured")
	}
	if workspaceID == "" || agentID == "" || memberID == "" {
		return fmt.Errorf(
			"workspaceId, agentId, and agentMemberId are required",
		)
	}
	var (
		persistedWorkspaceID string
		persistedAgentID     string
		persistedMemberID    string
		memberWorkspaceID    string
		memberType           string
		memberStatus         string
		machineWorkspaceID   string
		machineStatus        string
		agentStatus          string
	)
	err := tx.db.QueryRow(ctx, `
SELECT agent.workspace_id, agent.id, agent.member_id, agent.status,
       member.workspace_id, member.type, member.status,
       machine.workspace_id, machine.status
FROM agents agent
JOIN members member
  ON member.id = agent.member_id
JOIN machines machine
  ON machine.id = agent.machine_id
WHERE agent.workspace_id = $1
  AND agent.id = $2
  AND agent.member_id = $3
LIMIT 1
FOR UPDATE`, workspaceID, agentID, memberID).Scan(
		&persistedWorkspaceID,
		&persistedAgentID,
		&persistedMemberID,
		&agentStatus,
		&memberWorkspaceID,
		&memberType,
		&memberStatus,
		&machineWorkspaceID,
		&machineStatus,
	)
	if err != nil {
		if coredb.IsNotFound(err) {
			return fmt.Errorf(
				"authenticated Agent principal is not active in the Workspace",
			)
		}
		return fmt.Errorf("verify authenticated Agent principal: %w", err)
	}
	if persistedWorkspaceID != workspaceID ||
		persistedAgentID != agentID ||
		persistedMemberID != memberID ||
		agentStatus == "archived" ||
		memberWorkspaceID != workspaceID ||
		memberType != "agent" ||
		memberStatus != serviceactor.StatusActive ||
		machineWorkspaceID != workspaceID ||
		machineStatus != serviceactor.StatusActive {
		return fmt.Errorf(
			"authenticated Agent principal identity is inconsistent",
		)
	}
	return nil
}

func scanAgentAuthorization(
	row coredb.Row,
) (serviceactor.PrincipalAuthorization, error) {
	var authorization serviceactor.PrincipalAuthorization
	var disabledAt sql.NullTime
	var removedAt sql.NullTime
	var activeKey sql.NullString
	if err := row.Scan(
		&authorization.ID,
		&authorization.WorkspaceID,
		&authorization.ServiceDefinitionID,
		&authorization.ServiceInstallationID,
		&authorization.ServiceKind,
		&authorization.PrincipalKind,
		&authorization.PrincipalRef,
		&authorization.Status,
		&authorization.DisabledReason,
		&authorization.ScopesJSON,
		&authorization.AuthorizedByMemberID,
		&authorization.PolicyVersion,
		&authorization.Metadata,
		&authorization.CreatedAt,
		&authorization.UpdatedAt,
		&disabledAt,
		&removedAt,
		&activeKey,
	); err != nil {
		return serviceactor.PrincipalAuthorization{}, err
	}
	if activeKey.Valid {
		authorization.ActiveAuthorizationKey = activeKey.String
	}
	if disabledAt.Valid {
		value := disabledAt.Time
		authorization.DisabledAt = &value
	}
	if removedAt.Valid {
		value := removedAt.Time
		authorization.RemovedAt = &value
	}
	return authorization, nil
}
