package catalogapp

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

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

// verifyStoppedSourceReleaseGraph checks the immutable query projections that
// accompany the canonical package stored and reconstructed by ReadRelease. It
// never reconstructs a second package: ReadRelease remains the sole package
// read path, while this verifier fails closed if any derived child row was
// lost, duplicated, or changed.
func (a *Authority) verifyStoppedSourceReleaseGraph(
	ctx context.Context,
	release RegisteredRelease,
) error {
	if err := a.verifyStoppedSourceDependencies(ctx, release); err != nil {
		return err
	}
	if err := a.verifyStoppedSourceNamespaces(ctx, release); err != nil {
		return err
	}
	if err := a.verifyStoppedSourceCapabilities(ctx, release); err != nil {
		return err
	}
	return a.verifyStoppedSourceComponents(ctx, release)
}

func (a *Authority) verifyStoppedSourceDependencies(
	ctx context.Context,
	release RegisteredRelease,
) error {
	rows, err := a.db.QueryContext(ctx, `SELECT Alias, DependencyAppId, MinimumVersionInclusive,
MaximumVersionExclusive, RequiredCapabilities
FROM _System_CatalogAppReleaseDependency
WHERE AppId = ? AND AppReleaseDigest = ? ORDER BY Alias`, release.AppID, release.AppReleaseDigest)
	if err != nil {
		return err
	}
	defer rows.Close()
	actual := make([]appauthority.Dependency, 0, len(release.Package.Manifest.Dependencies))
	for rows.Next() {
		var dependency appauthority.Dependency
		var required []byte
		if err := rows.Scan(
			&dependency.Alias,
			&dependency.AppID,
			&dependency.MinimumVersionInclusive,
			&dependency.MaximumVersionExclusive,
			&required,
		); err != nil {
			return err
		}
		if err := json.Unmarshal(required, &dependency.RequiredCapabilities); err != nil {
			return fmt.Errorf("%w: stopped-source dependency capabilities are invalid JSON", ErrCorrupt)
		}
		actual = append(actual, dependency)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	expected := release.Package.Manifest.Dependencies
	if len(actual) != len(expected) {
		return fmt.Errorf("%w: stopped-source dependency graph is incomplete", ErrCorrupt)
	}
	for i := range expected {
		if actual[i].Alias != expected[i].Alias ||
			actual[i].AppID != expected[i].AppID ||
			actual[i].MinimumVersionInclusive != expected[i].MinimumVersionInclusive ||
			actual[i].MaximumVersionExclusive != expected[i].MaximumVersionExclusive ||
			!slices.Equal(actual[i].RequiredCapabilities, expected[i].RequiredCapabilities) {
			return fmt.Errorf("%w: stopped-source dependency graph differs from its package", ErrCorrupt)
		}
	}
	return nil
}

func (a *Authority) verifyStoppedSourceNamespaces(
	ctx context.Context,
	release RegisteredRelease,
) error {
	rows, err := a.db.QueryContext(ctx, `SELECT NamespaceId
FROM _System_CatalogAppReleaseNamespace
WHERE AppId = ? AND AppReleaseDigest = ? ORDER BY NamespaceId`, release.AppID, release.AppReleaseDigest)
	if err != nil {
		return err
	}
	defer rows.Close()
	actual := make([]string, 0, len(release.Package.Manifest.OwnedNamespaces))
	for rows.Next() {
		var namespaceID string
		if err := rows.Scan(&namespaceID); err != nil {
			return err
		}
		actual = append(actual, namespaceID)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	if !slices.Equal(actual, release.Package.Manifest.OwnedNamespaces) {
		return fmt.Errorf("%w: stopped-source namespace graph differs from its package", ErrCorrupt)
	}
	return nil
}

func (a *Authority) verifyStoppedSourceCapabilities(
	ctx context.Context,
	release RegisteredRelease,
) error {
	rows, err := a.db.QueryContext(ctx, `SELECT CapabilityId, Kind, HandlerRef, RequiredRuntimeCapabilities
FROM _System_CatalogAppReleaseCapability
WHERE AppId = ? AND AppReleaseDigest = ? ORDER BY CapabilityId`, release.AppID, release.AppReleaseDigest)
	if err != nil {
		return err
	}
	defer rows.Close()
	actual := make([]appauthority.ProvidedCapability, 0, len(release.Package.Manifest.ProvidedCapabilities))
	for rows.Next() {
		var capability appauthority.ProvidedCapability
		var handlerRef sql.NullString
		var required []byte
		if err := rows.Scan(
			&capability.CapabilityID,
			&capability.Kind,
			&handlerRef,
			&required,
		); err != nil {
			return err
		}
		// insertReleaseGraphTx stores an absent metadata-capability handler as
		// SQL NULL and every non-empty service handler as a string. Keep that
		// exact representation while reconstructing the manifest value.
		if handlerRef.Valid && handlerRef.String == "" {
			return fmt.Errorf("%w: stopped-source capability has a non-canonical handler reference", ErrCorrupt)
		}
		if handlerRef.Valid {
			capability.HandlerRef = handlerRef.String
		}
		if err := json.Unmarshal(required, &capability.RequiredRuntimeCapabilities); err != nil {
			return fmt.Errorf("%w: stopped-source capability requirements are invalid JSON", ErrCorrupt)
		}
		actual = append(actual, capability)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	expected := release.Package.Manifest.ProvidedCapabilities
	if len(actual) != len(expected) {
		return fmt.Errorf("%w: stopped-source capability graph is incomplete", ErrCorrupt)
	}
	for i := range expected {
		if actual[i].CapabilityID != expected[i].CapabilityID ||
			actual[i].Kind != expected[i].Kind ||
			actual[i].HandlerRef != expected[i].HandlerRef ||
			!slices.Equal(actual[i].RequiredRuntimeCapabilities, expected[i].RequiredRuntimeCapabilities) {
			return fmt.Errorf("%w: stopped-source capability graph differs from its package", ErrCorrupt)
		}
	}
	return nil
}

func (a *Authority) verifyStoppedSourceComponents(
	ctx context.Context,
	release RegisteredRelease,
) error {
	rows, err := a.db.QueryContext(ctx, `SELECT ComponentId, NamespaceId, ComponentKind,
BusinessKey, DefinitionVersion, ContentDigest
FROM _System_CatalogAppReleaseComponent
WHERE AppId = ? AND AppReleaseDigest = ? ORDER BY ComponentId`, release.AppID, release.AppReleaseDigest)
	if err != nil {
		return err
	}
	defer rows.Close()
	type storedComponent struct {
		componentID       string
		namespaceID       string
		componentKind     string
		businessKey       string
		definitionVersion string
		contentDigest     string
	}
	actual := make([]storedComponent, 0, len(release.Package.Bundle.Components))
	for rows.Next() {
		var component storedComponent
		if err := rows.Scan(
			&component.componentID,
			&component.namespaceID,
			&component.componentKind,
			&component.businessKey,
			&component.definitionVersion,
			&component.contentDigest,
		); err != nil {
			return err
		}
		actual = append(actual, component)
	}
	if err := rows.Err(); err != nil {
		return err
	}
	expected := release.Package.Bundle.Components
	if len(actual) != len(expected) {
		return fmt.Errorf("%w: stopped-source component graph is incomplete", ErrCorrupt)
	}
	for i := range expected {
		if actual[i].componentID != expected[i].ComponentID ||
			actual[i].namespaceID != expected[i].NamespaceID ||
			actual[i].componentKind != expected[i].ComponentKind ||
			actual[i].businessKey != expected[i].BusinessKey ||
			actual[i].definitionVersion != expected[i].DefinitionVersion ||
			actual[i].contentDigest != expected[i].ContentDigest {
			return fmt.Errorf("%w: stopped-source component graph differs from its package", ErrCorrupt)
		}
	}
	return nil
}
