package catalogapp

import (
	"context"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"slices"

	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
)

// validateActiveHandlerProjectionCapabilityTx fences the service-only binding
// rotation path with the exact serving projection committed by the operation
// that owns the active Catalog revision. Installations activated before the
// projection grammar have no matching durable operation authority and retain
// the historical all-handler contract.
func validateActiveHandlerProjectionCapabilityTx(
	ctx context.Context,
	tx *sql.Tx,
	installation installationRecord,
	capabilityID string,
) error {
	operationID, found, err := activeHandlerProjectionOperationIDTx(
		ctx, tx, installation,
	)
	if err != nil {
		return err
	}
	if !found {
		return nil
	}
	selected, err := operationHandlerProjectionCapabilitiesTx(
		ctx, tx, operationID, installation.AppID,
		installation.ActiveRelease, installation.ActiveWorkloads,
		installation.ActiveCapabilities, "",
	)
	if err != nil {
		return err
	}
	if !slices.Contains(selected, capabilityID) {
		return fmt.Errorf(
			"%w: handler capability is outside the active Catalog serving projection",
			ErrConflict,
		)
	}
	return nil
}

func activeHandlerProjectionOperationIDTx(
	ctx context.Context,
	tx *sql.Tx,
	installation installationRecord,
) (string, bool, error) {
	rows, err := tx.QueryContext(ctx, `SELECT OperationId FROM _System_CatalogAppOperation
WHERE AppInstallationId = ? AND TargetReleaseDigest = ? AND ExpectedCatalogRevision = ?
AND State = 'ACTIVE' AND Step = 'ACTIVATED' FOR UPDATE`,
		installation.ID, installation.ActiveRelease,
		epochText(installation.CatalogRevision),
	)
	if err != nil {
		return "", false, err
	}
	var operationIDs []string
	for rows.Next() {
		var operationID string
		if err := rows.Scan(&operationID); err != nil {
			rows.Close()
			return "", false, err
		}
		operationIDs = append(operationIDs, operationID)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return "", false, err
	}
	if err := rows.Close(); err != nil {
		return "", false, err
	}
	if len(operationIDs) == 0 {
		return "", false, nil
	}
	if len(operationIDs) != 1 {
		return "", false, fmt.Errorf("%w: active Catalog revision has ambiguous handler projection authority", ErrCorrupt)
	}
	return operationIDs[0], true, nil
}

func operationHandlerProjectionCapabilitiesTx(
	ctx context.Context,
	tx *sql.Tx,
	operationID string,
	appID string,
	releaseDigest string,
	authorizedWorkloads []string,
	grantedCapabilities []string,
	candidateInstallationID string,
) ([]string, error) {
	var selectedJSON, admissionsJSON []byte
	err := tx.QueryRowContext(ctx, `SELECT SelectedHandlerCapabilities, WorkloadHandlerAdmissions
FROM _System_CatalogAppOperation WHERE OperationId = ? FOR UPDATE`,
		operationID,
	).Scan(&selectedJSON, &admissionsJSON)
	if errors.Is(err, sql.ErrNoRows) {
		return nil, ErrNotFound
	}
	if err != nil {
		return nil, err
	}
	available, provided, err := serviceHandlerCapabilitiesTx(
		ctx, tx, appID, releaseDigest,
	)
	if err != nil {
		return nil, err
	}
	if len(selectedJSON) == 0 {
		if len(admissionsJSON) != 0 {
			return nil, fmt.Errorf("%w: legacy HandlerBinding set has workload admissions", ErrCorrupt)
		}
		return available, nil
	}
	if candidateInstallationID != "" {
		var workloadsJSON, grantsJSON []byte
		if err := tx.QueryRowContext(ctx, `SELECT CandidateAuthorizedWorkloads, CandidateGrantedCapabilities
FROM _System_CatalogAppInstallation WHERE AppInstallationId = ? FOR UPDATE`,
			candidateInstallationID,
		).Scan(&workloadsJSON, &grantsJSON); err != nil {
			return nil, err
		}
		if json.Unmarshal(workloadsJSON, &authorizedWorkloads) != nil ||
			json.Unmarshal(grantsJSON, &grantedCapabilities) != nil {
			return nil, fmt.Errorf(
				"%w: candidate Runtime security grant is invalid",
				ErrCorrupt,
			)
		}
	}
	var selected []string
	var admissions []WorkloadHandlerAdmission
	if err := json.Unmarshal(selectedJSON, &selected); err != nil ||
		len(admissionsJSON) == 0 || json.Unmarshal(admissionsJSON, &admissions) != nil {
		return nil, fmt.Errorf("%w: selected HandlerBinding set is invalid", ErrCorrupt)
	}
	canonicalSelected, canonicalAdmissions, err := canonicalHandlerProjectionAuthority(
		provided, selected, admissions, authorizedWorkloads, grantedCapabilities,
	)
	if err != nil || !slices.Equal(selected, canonicalSelected) ||
		!equalWorkloadHandlerAdmissions(admissions, canonicalAdmissions) {
		return nil, fmt.Errorf(
			"%w: handler projection authority disagrees with the AppRelease and Runtime grant",
			ErrCorrupt,
		)
	}
	return canonicalSelected, nil
}

// AppRelease capability rows are immutable after release registration. Keep
// this enumeration non-locking: StageHandlerBinding already locks the requested
// capability row, so taking every release capability lock here would invert the
// lock order between concurrent rotations of different handlers.
const serviceHandlerCapabilitiesQuery = `SELECT CapabilityId, RequiredRuntimeCapabilities FROM _System_CatalogAppReleaseCapability
WHERE AppId = ? AND AppReleaseDigest = ? AND Kind = ? ORDER BY CapabilityId`

func serviceHandlerCapabilitiesTx(
	ctx context.Context,
	tx *sql.Tx,
	appID string,
	releaseDigest string,
) ([]string, []appauthority.ProvidedCapability, error) {
	rows, err := tx.QueryContext(ctx, serviceHandlerCapabilitiesQuery,
		appID, releaseDigest, appauthority.CapabilityKindServiceHandler,
	)
	if err != nil {
		return nil, nil, err
	}
	var available []string
	var provided []appauthority.ProvidedCapability
	for rows.Next() {
		var capability string
		var requiredJSON []byte
		if err := rows.Scan(&capability, &requiredJSON); err != nil {
			rows.Close()
			return nil, nil, err
		}
		var requiredRuntimeCapabilities []string
		if err := json.Unmarshal(requiredJSON, &requiredRuntimeCapabilities); err != nil {
			rows.Close()
			return nil, nil, fmt.Errorf(
				"%w: AppRelease handler dependency closure is invalid",
				ErrCorrupt,
			)
		}
		available = append(available, capability)
		provided = append(provided, appauthority.ProvidedCapability{
			CapabilityID: capability, Kind: appauthority.CapabilityKindServiceHandler,
			RequiredRuntimeCapabilities: requiredRuntimeCapabilities,
		})
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return nil, nil, err
	}
	if err := rows.Close(); err != nil {
		return nil, nil, err
	}
	return available, provided, nil
}
