package extension

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"fmt"
	"strings"

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

// AppHostedConnectionIdentity is the raw identity of one active connection
// owned by an App Installation. Only the opaque subject alias and one-way
// credential-generation digest cross the persistence boundary; neither
// reaches Agent output.
type AppHostedConnectionIdentity struct {
	ID                      string
	WorkspaceID             string
	ServiceDefinitionID     string
	ServiceInstallationID   string
	ServiceKind             string
	Status                  string
	ExternalSubjectRef      string
	CurrentCredentialDigest string
	Config                  []byte
}

// AppHostedExtensionSelection is the raw, joined persistence projection needed
// to resolve one active Service Installation Extension selection. Host and
// capability identities remain server-internal and are validated before the
// selection is flattened for Desktop.
type AppHostedExtensionSelection struct {
	Selection serviceactor.InstallationExtension

	DefinitionID     string
	DefinitionKind   string
	DefinitionStatus string

	InstallationID            string
	InstallationWorkspaceID   string
	InstallationDefinitionID  string
	InstallationKind          string
	InstallationStatus        string
	InstallationOwnershipKind string

	CapabilityDefinitionID string
	CapabilityKind         string
	CapabilityKey          string
	CapabilityConfig       []byte
	CapabilityEnabled      bool

	ExecutionCapabilityConfig  []byte
	ExecutionCapabilityEnabled bool

	Connections []AppHostedConnectionIdentity
}

// ListActiveAppHostedExtensionSelections returns every active selection in one
// Workspace together with its App host, extensions capability, and active
// connection identities. It intentionally does not lock source rows: Workspace
// source mutations enqueue a newer reconcile generation, and avoiding selection
// locks keeps Agent authorization transactions and batch reconciliation from
// deadlocking on opposite source/catalog lock orders. The scanner preserves raw
// identity strings; semantic validation belongs to the service projector.
func (tx *TxStore) ListActiveAppHostedExtensionSelections(
	ctx context.Context,
	workspaceID string,
) ([]AppHostedExtensionSelection, error) {
	if tx == nil || tx.db == nil {
		return nil, fmt.Errorf("extension store transaction is not configured")
	}
	workspaceID = strings.TrimSpace(workspaceID)
	if workspaceID == "" {
		return nil, fmt.Errorf("workspaceId is required")
	}
	rows, err := tx.db.Query(ctx, `
SELECT selection.id, selection.workspace_id, selection.service_definition_id,
       selection.service_installation_id, selection.extension_name,
       selection.definition_domain, selection.definition_version,
       selection.definition_digest, selection.status, selection.disabled_reason,
       selection.config, selection.selection_source, selection.created_by_member_id,
       selection.created_at, selection.updated_at, selection.disabled_at,
       definition.id, definition.service_kind, definition.status,
       installation.id, installation.workspace_id,
       installation.service_definition_id, installation.service_kind,
       installation.status, installation.ownership_kind,
       COALESCE(capability.service_definition_id, ''),
       COALESCE(capability.service_kind, ''),
       COALESCE(capability.capability_key, ''),
       COALESCE(capability.config, '{}'),
       COALESCE(capability.default_enabled, FALSE),
       COALESCE(execution_capability.config, '{}'),
       COALESCE(execution_capability.default_enabled, FALSE),
       COALESCE(connection.id, ''),
       COALESCE(connection.workspace_id, ''),
       COALESCE(connection.service_definition_id, ''),
       COALESCE(connection.service_installation_id, ''),
       COALESCE(connection.service_kind, ''),
       COALESCE(connection.status, ''),
       COALESCE(connection.external_subject_ref, ''),
       COALESCE(connection.credentials, JSON_OBJECT()),
       COALESCE(connection.config, JSON_OBJECT())
FROM service_installation_extensions selection
JOIN service_definitions definition
  ON definition.id = selection.service_definition_id
JOIN service_installations installation
  ON installation.id = selection.service_installation_id
LEFT JOIN service_capabilities capability
  ON capability.service_definition_id = selection.service_definition_id
 AND capability.capability_key = 'extensions'
LEFT JOIN service_capabilities execution_capability
  ON execution_capability.service_definition_id =
     selection.service_definition_id
 AND execution_capability.service_kind = definition.service_kind
 AND execution_capability.capability_key = 'mcp.tools'
LEFT JOIN service_connections connection
  ON connection.service_installation_id = selection.service_installation_id
 AND connection.status = 'active'
WHERE selection.workspace_id = $1
  AND selection.status = 'active'
ORDER BY selection.extension_name ASC, selection.id ASC, connection.id ASC
`, workspaceID)
	if err != nil {
		return nil, fmt.Errorf("list active App-hosted Extension selections: %w", err)
	}
	defer rows.Close()

	selections := make([]AppHostedExtensionSelection, 0)
	selectionIndexes := make(map[string]int)
	for rows.Next() {
		projected, connection, err := scanAppHostedExtensionSelection(rows)
		if err != nil {
			return nil, fmt.Errorf("scan active App-hosted Extension selection: %w", err)
		}
		index, exists := selectionIndexes[projected.Selection.ID]
		if !exists {
			index = len(selections)
			selectionIndexes[projected.Selection.ID] = index
			selections = append(selections, projected)
		} else if !sameAppHostedSelectionProjection(selections[index], projected) {
			return nil, fmt.Errorf(
				"App-hosted Extension selection %q has inconsistent joined identity",
				projected.Selection.ID,
			)
		}
		if connection.ID != "" {
			selections[index].Connections = append(selections[index].Connections, connection)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate active App-hosted Extension selections: %w", err)
	}
	return selections, nil
}

func scanAppHostedExtensionSelection(
	row coredb.Row,
) (AppHostedExtensionSelection, AppHostedConnectionIdentity, error) {
	var (
		projected            AppHostedExtensionSelection
		disabledAt           sql.NullTime
		connection           AppHostedConnectionIdentity
		encryptedCredentials []byte
		connectionConfig     []byte
	)
	if err := row.Scan(
		&projected.Selection.ID,
		&projected.Selection.WorkspaceID,
		&projected.Selection.ServiceDefinitionID,
		&projected.Selection.ServiceInstallationID,
		&projected.Selection.ExtensionName,
		&projected.Selection.DefinitionDomain,
		&projected.Selection.DefinitionVersion,
		&projected.Selection.DefinitionDigest,
		&projected.Selection.Status,
		&projected.Selection.DisabledReason,
		&projected.Selection.Config,
		&projected.Selection.SelectionSource,
		&projected.Selection.CreatedByMemberID,
		&projected.Selection.CreatedAt,
		&projected.Selection.UpdatedAt,
		&disabledAt,
		&projected.DefinitionID,
		&projected.DefinitionKind,
		&projected.DefinitionStatus,
		&projected.InstallationID,
		&projected.InstallationWorkspaceID,
		&projected.InstallationDefinitionID,
		&projected.InstallationKind,
		&projected.InstallationStatus,
		&projected.InstallationOwnershipKind,
		&projected.CapabilityDefinitionID,
		&projected.CapabilityKind,
		&projected.CapabilityKey,
		&projected.CapabilityConfig,
		&projected.CapabilityEnabled,
		&projected.ExecutionCapabilityConfig,
		&projected.ExecutionCapabilityEnabled,
		&connection.ID,
		&connection.WorkspaceID,
		&connection.ServiceDefinitionID,
		&connection.ServiceInstallationID,
		&connection.ServiceKind,
		&connection.Status,
		&connection.ExternalSubjectRef,
		&encryptedCredentials,
		&connectionConfig,
	); err != nil {
		return AppHostedExtensionSelection{}, AppHostedConnectionIdentity{}, err
	}
	if disabledAt.Valid {
		t := disabledAt.Time
		projected.Selection.DisabledAt = &t
	}
	connection.CurrentCredentialDigest = CredentialEnvelopeDigest(
		encryptedCredentials,
	)
	connection.Config = append([]byte(nil), connectionConfig...)
	return projected, connection, nil
}

func sameAppHostedSelectionProjection(left, right AppHostedExtensionSelection) bool {
	return left.Selection.ID == right.Selection.ID &&
		left.Selection.WorkspaceID == right.Selection.WorkspaceID &&
		left.Selection.ServiceDefinitionID == right.Selection.ServiceDefinitionID &&
		left.Selection.ServiceInstallationID == right.Selection.ServiceInstallationID &&
		left.Selection.ExtensionName == right.Selection.ExtensionName &&
		left.Selection.DefinitionDomain == right.Selection.DefinitionDomain &&
		left.Selection.DefinitionVersion == right.Selection.DefinitionVersion &&
		left.Selection.DefinitionDigest == right.Selection.DefinitionDigest &&
		left.Selection.Status == right.Selection.Status &&
		left.Selection.DisabledReason == right.Selection.DisabledReason &&
		string(left.Selection.Config) == string(right.Selection.Config) &&
		left.Selection.SelectionSource == right.Selection.SelectionSource &&
		left.Selection.CreatedByMemberID == right.Selection.CreatedByMemberID &&
		left.DefinitionID == right.DefinitionID &&
		left.DefinitionKind == right.DefinitionKind &&
		left.DefinitionStatus == right.DefinitionStatus &&
		left.InstallationID == right.InstallationID &&
		left.InstallationWorkspaceID == right.InstallationWorkspaceID &&
		left.InstallationDefinitionID == right.InstallationDefinitionID &&
		left.InstallationKind == right.InstallationKind &&
		left.InstallationStatus == right.InstallationStatus &&
		left.InstallationOwnershipKind == right.InstallationOwnershipKind &&
		left.CapabilityDefinitionID == right.CapabilityDefinitionID &&
		left.CapabilityKind == right.CapabilityKind &&
		left.CapabilityKey == right.CapabilityKey &&
		string(left.CapabilityConfig) == string(right.CapabilityConfig) &&
		left.CapabilityEnabled == right.CapabilityEnabled &&
		string(left.ExecutionCapabilityConfig) ==
			string(right.ExecutionCapabilityConfig) &&
		left.ExecutionCapabilityEnabled ==
			right.ExecutionCapabilityEnabled
}

// CredentialEnvelopeDigest returns the stable one-way identity of one exact
// encrypted credential envelope. It is safe to carry across trusted runtime
// boundaries and never exposes credential plaintext.
func CredentialEnvelopeDigest(encrypted []byte) string {
	digest := sha256.Sum256(encrypted)
	return "sha256:" + hex.EncodeToString(digest[:])
}

// RuntimeConnectionGeneration binds the generic Extension preflight to the
// exact active Connection identity, encrypted credential generation, and
// endpoint configuration it resolved. Builtin executors compare this opaque
// value against their first provider-specific load so a reconnect cannot race
// between generic preflight and provider I/O.
func RuntimeConnectionGeneration(
	connectionID string,
	externalSubjectRef string,
	credentialDigest string,
	config []byte,
) string {
	digest := sha256.New()
	_, _ = digest.Write(
		[]byte("agentteam/extension/runtime-connection-generation/v1\x00"),
	)
	for _, part := range [][]byte{
		[]byte(connectionID),
		[]byte(externalSubjectRef),
		[]byte(credentialDigest),
		config,
	} {
		length := []byte{
			byte(len(part) >> 24),
			byte(len(part) >> 16),
			byte(len(part) >> 8),
			byte(len(part)),
		}
		_, _ = digest.Write(length)
		_, _ = digest.Write(part)
	}
	return "sha256:" + hex.EncodeToString(digest.Sum(nil))
}
