package serviceactor

import (
	"context"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

// Grant records an installation-scoped capability grant.
type Grant struct {
	ID                    string
	WorkspaceID           string
	ServiceDefinitionID   string
	ServiceInstallationID string
	ServiceKind           string
	Scope                 string
	Config                json.RawMessage
	PolicyVersion         int64
	PolicyDigest          string
	GrantedByMemberID     string
	GrantedAt             time.Time
	ExpiresAt             *time.Time
	RevokedAt             *time.Time
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

// ListActiveGrantScopes lists active grant scopes for a service installation.
func (store *Store) ListActiveGrantScopes(ctx context.Context, serviceInstallationID string) ([]string, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return nil, err
	}
	return listActiveGrantScopes(ctx, queryer, serviceInstallationID)
}

// ListActiveGrantScopes lists active grant scopes inside the caller-owned
// transaction.
func (store *TxStore) ListActiveGrantScopes(ctx context.Context, serviceInstallationID string) ([]string, error) {
	queryer, err := queryerFromTxStore(store)
	if err != nil {
		return nil, err
	}
	return listActiveGrantScopes(ctx, queryer, serviceInstallationID)
}

// ListActiveGrantScopesByInstallationIDs lists active grant scopes keyed by
// service installation id.
func (store *Store) ListActiveGrantScopesByInstallationIDs(ctx context.Context, serviceInstallationIDs []string) (map[string][]string, error) {
	queryer, err := queryerFromStore(store)
	if err != nil {
		return nil, err
	}
	return listActiveGrantScopesByInstallationIDs(ctx, queryer, serviceInstallationIDs, false)
}

// ListActiveGrantScopesByInstallationIDs lists active grant scopes keyed by
// service installation id inside the caller's transaction.
func (store *TxStore) ListActiveGrantScopesByInstallationIDs(ctx context.Context, serviceInstallationIDs []string) (map[string][]string, error) {
	queryer, err := queryerFromTxStore(store)
	if err != nil {
		return nil, err
	}
	return listActiveGrantScopesByInstallationIDs(ctx, queryer, serviceInstallationIDs, false)
}

// SyncGrantScopes reconciles active grant scopes for a service installation.
func (store *TxStore) SyncGrantScopes(ctx context.Context, installation Installation, grantedByMemberID string, scopes []string, now time.Time) error {
	execer, err := execerFromTxStore(store)
	if err != nil {
		return err
	}
	return syncGrantScopes(ctx, execer, installation, grantedByMemberID, scopes, now)
}

func listActiveGrantScopes(ctx context.Context, queryer coredb.Queryer, serviceInstallationID string) ([]string, error) {
	rows, err := queryer.Query(ctx, `
SELECT scope
FROM service_grants
WHERE service_installation_id = $1
  AND revoked_at IS NULL
  AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
ORDER BY scope ASC
`, trim(serviceInstallationID))
	if err != nil {
		return nil, fmt.Errorf("list service grant scopes: %w", err)
	}
	defer rows.Close()
	scopes := make([]string, 0)
	for rows.Next() {
		var scope string
		if err := rows.Scan(&scope); err != nil {
			return nil, fmt.Errorf("scan service grant scope: %w", err)
		}
		if trimmed := trim(scope); trimmed != "" {
			scopes = append(scopes, trimmed)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate service grant scopes: %w", err)
	}
	return sortedUnique(scopes), nil
}

func listActiveGrantScopesByInstallationIDs(ctx context.Context, queryer coredb.Queryer, serviceInstallationIDs []string, forUpdate bool) (map[string][]string, error) {
	installationIDs := trimSlice(serviceInstallationIDs)
	grantsByInstallation := make(map[string][]string, len(installationIDs))
	for _, installationID := range installationIDs {
		grantsByInstallation[installationID] = []string{}
	}
	if len(installationIDs) == 0 {
		return grantsByInstallation, nil
	}
	lockClause := ""
	if forUpdate {
		lockClause = "\nFOR UPDATE"
	}
	requestedInstallationsCTE, requestedInstallationArgs, _, err := coredb.BuildStringValuesCTE("requested_service_installations", "service_installation_id", installationIDs, 1)
	if err != nil {
		return nil, fmt.Errorf("build service grants query: %w", err)
	}
	rows, err := queryer.Query(ctx, fmt.Sprintf(`
WITH %s
SELECT requested.service_installation_id, sg.scope
FROM requested_service_installations requested
JOIN service_grants sg
 ON sg.service_installation_id = requested.service_installation_id
 AND sg.revoked_at IS NULL
 AND (sg.expires_at IS NULL OR sg.expires_at > CURRENT_TIMESTAMP)
ORDER BY requested.service_installation_id ASC, sg.scope ASC
`+lockClause, requestedInstallationsCTE), requestedInstallationArgs...)
	if err != nil {
		return nil, fmt.Errorf("list service grants by installation ids: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var installationID string
		var scope string
		if err := rows.Scan(&installationID, &scope); err != nil {
			return nil, fmt.Errorf("scan service grant scope: %w", err)
		}
		installationID = trim(installationID)
		scope = trim(scope)
		if installationID == "" || scope == "" {
			continue
		}
		grantsByInstallation[installationID] = append(grantsByInstallation[installationID], scope)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("iterate service grants: %w", err)
	}
	for installationID, scopes := range grantsByInstallation {
		grantsByInstallation[installationID] = sortedUnique(scopes)
	}
	return grantsByInstallation, nil
}

func syncGrantScopes(ctx context.Context, execer coredb.QueryExecer, installation Installation, grantedByMemberID string, scopes []string, now time.Time) error {
	installationID := trim(installation.ID)
	workspaceID := trim(installation.WorkspaceID)
	definitionID := trim(installation.ServiceDefinitionID)
	serviceKind := trim(installation.ServiceKind)
	grantedByMemberID = trim(grantedByMemberID)
	if installationID == "" || workspaceID == "" || definitionID == "" || serviceKind == "" || grantedByMemberID == "" {
		return badRequest("installation and grantedByMemberId are required")
	}
	now = now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	desiredScopes := sortedUnique(scopes)
	desiredSet := make(map[string]struct{}, len(desiredScopes))
	for _, scope := range desiredScopes {
		desiredSet[scope] = struct{}{}
	}

	rows, err := execer.Query(ctx, `
SELECT scope
FROM service_grants
WHERE service_installation_id = $1
  AND revoked_at IS NULL
FOR UPDATE
`, installationID)
	if err != nil {
		return fmt.Errorf("list active service grants: %w", err)
	}
	defer rows.Close()

	activeScopes := make([]string, 0)
	for rows.Next() {
		var scope string
		if err := rows.Scan(&scope); err != nil {
			return fmt.Errorf("scan active service grant: %w", err)
		}
		activeScopes = append(activeScopes, trim(scope))
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate active service grants: %w", err)
	}

	revokeScopes := make([]string, 0)
	for _, scope := range activeScopes {
		if scope == "" {
			continue
		}
		if _, keep := desiredSet[scope]; keep {
			delete(desiredSet, scope)
			continue
		}
		revokeScopes = append(revokeScopes, scope)
	}
	if len(revokeScopes) > 0 {
		revokeArgs := []any{installationID, now}
		revokePlaceholders := make([]string, 0, len(revokeScopes))
		for _, scope := range revokeScopes {
			revokeArgs = append(revokeArgs, scope)
			revokePlaceholders = append(revokePlaceholders, fmt.Sprintf("$%d", len(revokeArgs)))
		}
		if _, err := execer.Exec(ctx, fmt.Sprintf(`
UPDATE service_grants
SET revoked_at = $2,
    updated_at = $2
WHERE service_installation_id = $1
  AND revoked_at IS NULL
  AND scope IN (%s)
`, strings.Join(revokePlaceholders, ", ")), revokeArgs...); err != nil {
			return fmt.Errorf("revoke service grants: %w", err)
		}
	}

	insertValues := make([]string, 0)
	insertArgs := make([]any, 0)
	for _, scope := range desiredScopes {
		if _, missing := desiredSet[scope]; !missing {
			continue
		}
		restored, err := restoreServiceGrantScope(ctx, execer, installationID, scope, grantedByMemberID, now)
		if err != nil {
			return err
		}
		if restored {
			continue
		}
		grantID, err := identifiers.NewEntityID("svcgr")
		if err != nil {
			return fmt.Errorf("generate service grant id: %w", err)
		}
		base := len(insertArgs) + 1
		insertValues = append(insertValues, fmt.Sprintf("($%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d, $%d)", base, base+1, base+2, base+3, base+4, base+5, base+6, base+7, base+8, base+9, base+10, base+11, base+12, base+13))
		insertArgs = append(insertArgs, grantID, workspaceID, definitionID, installationID, serviceKind, scope, []byte(`{}`), int64(1), "", grantedByMemberID, now, nil, now, now)
	}
	if len(insertValues) > 0 {
		if _, err := execer.Exec(ctx, fmt.Sprintf(`
INSERT INTO service_grants (
    id, workspace_id, service_definition_id, service_installation_id,
    service_kind, scope, config, policy_version, policy_digest,
    granted_by_member_id, granted_at, expires_at, created_at, updated_at
) VALUES %s
`, strings.Join(insertValues, ",\n")), insertArgs...); err != nil {
			return fmt.Errorf("insert service grants: %w", err)
		}
	}
	return nil
}

func restoreServiceGrantScope(ctx context.Context, execer coredb.QueryExecer, installationID string, scope string, grantedByMemberID string, now time.Time) (bool, error) {
	var grantID string
	err := execer.QueryRow(ctx, `
SELECT id
FROM service_grants
WHERE service_installation_id = $1
  AND scope = $2
  AND revoked_at IS NOT NULL
ORDER BY policy_version DESC, revoked_at DESC, id DESC
LIMIT 1
FOR UPDATE
`, installationID, scope).Scan(&grantID)
	if err != nil {
		if coredb.IsNotFound(err) {
			return false, nil
		}
		return false, fmt.Errorf("load revoked service grant for restore: %w", err)
	}
	result, err := execer.Exec(ctx, `
UPDATE service_grants
SET granted_by_member_id = $1,
    granted_at = $2,
    revoked_at = NULL,
    updated_at = $2
WHERE id = $3
  AND service_installation_id = $4
  AND scope = $5
  AND revoked_at IS NOT NULL
`, grantedByMemberID, now, trim(grantID), installationID, scope)
	if err != nil {
		return false, fmt.Errorf("restore service grant: %w", err)
	}
	return result.RowsAffected() == 1, nil
}

// SetGrantPolicy replaces one active grant's structured policy under an exact
// current-version fence. Revoked grants cannot be mutated through this API.
func (store *TxStore) SetGrantPolicy(
	ctx context.Context,
	serviceInstallationID string,
	scope string,
	expectedPolicyVersion int64,
	config json.RawMessage,
	policyDigest string,
	expiresAt *time.Time,
	now time.Time,
) (Grant, error) {
	execer, err := execerFromTxStore(store)
	if err != nil {
		return Grant{}, err
	}
	serviceInstallationID = trim(serviceInstallationID)
	scope = trim(scope)
	policyDigest = trim(policyDigest)
	if serviceInstallationID == "" || scope == "" || expectedPolicyVersion <= 0 {
		return Grant{}, badRequest("installation, scope, and expected policy version are required")
	}
	var configObject map[string]any
	if err := model.UnmarshalJSONObject("service grant policy config", config, &configObject); err != nil || configObject == nil {
		return Grant{}, badRequest("service grant policy config must be a JSON object")
	}
	canonicalConfig, err := model.MarshalJSONObject("service grant policy config", configObject)
	if err != nil {
		return Grant{}, badRequest("service grant policy config must be a JSON object")
	}
	if len(policyDigest) != 64 {
		return Grant{}, badRequest("service grant policy digest must be a SHA-256 hex digest")
	}
	if _, err := hex.DecodeString(policyDigest); err != nil {
		return Grant{}, badRequest("service grant policy digest must be a SHA-256 hex digest")
	}
	now = now.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	if expiresAt != nil {
		normalizedExpiry := expiresAt.UTC()
		expiresAt = &normalizedExpiry
		if !normalizedExpiry.After(now) {
			return Grant{}, badRequest("service grant policy expiry must be in the future")
		}
	}

	current, err := loadActiveGrantForUpdate(ctx, execer, serviceInstallationID, scope)
	if err != nil {
		return Grant{}, err
	}
	if current.PolicyVersion != expectedPolicyVersion {
		return Grant{}, conflict("service grant policy version conflict")
	}
	result, err := execer.Exec(ctx, `
UPDATE service_grants
SET config = $1,
    policy_version = $2,
    policy_digest = $3,
    expires_at = $4,
    updated_at = $5
WHERE id = $6
  AND service_installation_id = $7
  AND scope = $8
  AND policy_version = $9
  AND revoked_at IS NULL
`, canonicalConfig, expectedPolicyVersion+1, policyDigest, nullTimePtr(expiresAt), now,
		current.ID, serviceInstallationID, scope, expectedPolicyVersion)
	if err != nil {
		return Grant{}, fmt.Errorf("set service grant policy: %w", err)
	}
	if result.RowsAffected() != 1 {
		return Grant{}, conflict("service grant policy version conflict")
	}
	current.Config = append(json.RawMessage(nil), canonicalConfig...)
	current.PolicyVersion = expectedPolicyVersion + 1
	current.PolicyDigest = policyDigest
	current.ExpiresAt = expiresAt
	current.UpdatedAt = now
	return current, nil
}

// EnsureGrantPolicy installs the exact next-version policy snapshot once and
// treats an already-matching active grant as an idempotent replay. Callers pass
// a self-describing policy document whose embedded version is expectedVersion
// + 1 and whose digest is persisted alongside it.
func (store *TxStore) EnsureGrantPolicy(
	ctx context.Context,
	serviceInstallationID string,
	scope string,
	expectedPolicyVersion int64,
	config json.RawMessage,
	policyDigest string,
	expiresAt *time.Time,
	now time.Time,
) (Grant, bool, error) {
	execer, err := execerFromTxStore(store)
	if err != nil {
		return Grant{}, false, err
	}
	serviceInstallationID = trim(serviceInstallationID)
	scope = trim(scope)
	policyDigest = trim(policyDigest)
	if serviceInstallationID == "" || scope == "" || expectedPolicyVersion <= 0 {
		return Grant{}, false, badRequest("installation, scope, and expected policy version are required")
	}
	current, err := loadActiveGrantForUpdate(ctx, execer, serviceInstallationID, scope)
	if err != nil {
		return Grant{}, false, err
	}
	var configObject map[string]any
	if err := model.UnmarshalJSONObject("service grant policy config", config, &configObject); err != nil || configObject == nil {
		return Grant{}, false, badRequest("service grant policy config must be a JSON object")
	}
	canonicalConfig, err := model.MarshalJSONObject("service grant policy config", configObject)
	if err != nil {
		return Grant{}, false, badRequest("service grant policy config must be a JSON object")
	}
	if current.PolicyVersion == expectedPolicyVersion+1 &&
		current.PolicyDigest == policyDigest &&
		model.JSONDocumentsEqual(current.Config, canonicalConfig) &&
		timesEqual(current.ExpiresAt, expiresAt) {
		return current, false, nil
	}
	if current.PolicyVersion != expectedPolicyVersion {
		return Grant{}, false, conflict("service grant policy version conflict")
	}
	updated, err := store.SetGrantPolicy(ctx, serviceInstallationID, scope, expectedPolicyVersion, canonicalConfig, policyDigest, expiresAt, now)
	if err != nil {
		return Grant{}, false, err
	}
	return updated, true, nil
}

func timesEqual(left, right *time.Time) bool {
	if left == nil || right == nil {
		return left == nil && right == nil
	}
	return left.UTC().Equal(right.UTC())
}

func loadActiveGrantForUpdate(ctx context.Context, queryer coredb.QueryRower, serviceInstallationID string, scope string) (Grant, error) {
	grant, err := scanGrant(queryer.QueryRow(ctx, `
SELECT
    id, workspace_id, service_definition_id, service_installation_id,
    service_kind, scope, config, policy_version, policy_digest,
    granted_by_member_id, granted_at, expires_at, revoked_at, created_at, updated_at
FROM service_grants
WHERE service_installation_id = $1
  AND scope = $2
  AND revoked_at IS NULL
ORDER BY policy_version DESC, granted_at DESC, id DESC
LIMIT 1
FOR UPDATE
`, trim(serviceInstallationID), trim(scope)))
	if err != nil {
		if coredb.IsNotFound(err) {
			return Grant{}, notFound("active service grant not found")
		}
		return Grant{}, fmt.Errorf("load active service grant for update: %w", err)
	}
	return grant, nil
}

func scanGrant(row coredb.Row) (Grant, error) {
	var grant Grant
	var config []byte
	var expiresAt, revokedAt sql.NullTime
	if err := row.Scan(
		&grant.ID,
		&grant.WorkspaceID,
		&grant.ServiceDefinitionID,
		&grant.ServiceInstallationID,
		&grant.ServiceKind,
		&grant.Scope,
		&config,
		&grant.PolicyVersion,
		&grant.PolicyDigest,
		&grant.GrantedByMemberID,
		&grant.GrantedAt,
		&expiresAt,
		&revokedAt,
		&grant.CreatedAt,
		&grant.UpdatedAt,
	); err != nil {
		return Grant{}, err
	}
	grant.ID = trim(grant.ID)
	grant.WorkspaceID = trim(grant.WorkspaceID)
	grant.ServiceDefinitionID = trim(grant.ServiceDefinitionID)
	grant.ServiceInstallationID = trim(grant.ServiceInstallationID)
	grant.ServiceKind = trim(grant.ServiceKind)
	grant.Scope = trim(grant.Scope)
	grant.Config = append(json.RawMessage(nil), config...)
	grant.PolicyDigest = trim(grant.PolicyDigest)
	grant.GrantedByMemberID = trim(grant.GrantedByMemberID)
	grant.ExpiresAt = ptrFromNullTime(expiresAt)
	grant.RevokedAt = ptrFromNullTime(revokedAt)
	return grant, nil
}

func sortedUnique(values []string) []string {
	trimmed := trimSlice(values)
	sort.Strings(trimmed)
	return trimmed
}

// ListActiveGrantScopesByInstallationIDsForUpdate is the locking current-read
// batch loader for write transactions that may have waited on other locks:
// share snapshots, ACL grants, and agent authorizations must pin the
// current scope set instead of a pre-wait RR snapshot.
func (store *TxStore) ListActiveGrantScopesByInstallationIDsForUpdate(ctx context.Context, serviceInstallationIDs []string) (map[string][]string, error) {
	queryer, err := queryerFromTxStore(store)
	if err != nil {
		return nil, err
	}
	return listActiveGrantScopesByInstallationIDs(ctx, queryer, serviceInstallationIDs, true)
}
