package packs

import (
	"context"
	"fmt"
	"strings"
)

// Category identifies a pack's role in the platform.
type Category string

const (
	PlatformCore   Category = "platform-core"
	PlatformTenant Category = "platform-tenant"
	PlatformPolicy Category = "platform-policy"
	PlatformConfig Category = "platform-config"
	PlatformUX     Category = "platform-ux"
	AppPack        Category = "app-pack"
	ConnectorPack  Category = "connector-pack"
	DevSample      Category = "dev-sample"
)

// Pack is a typed, ordered unit of metadata that the control plane can apply
// and verify independently.
type Pack interface {
	Name() string
	Category() Category
	Apply(ctx context.Context, deps ApplyDeps) error
	Verify(ctx context.Context, deps VerifyDeps) error
}

// PayloadHasher is implemented by packs that can identify the exact shipped
// payload version they applied. Installed-pack verification uses this to
// distinguish "binary payload changed; re-apply required" from live DB drift.
type PayloadHasher interface {
	PayloadHash() (string, error)
}

// LegacyPayloadHasher is implemented by transitional packs that need to verify
// pre-v1 bare installed-pack hashes during the #89 migration window.
type LegacyPayloadHasher interface {
	LegacyPayloadHash() (string, error)
}

const (
	PackPayloadHashAlgorithmV1 = "nexus-pack-sha256-v1"
	PackPayloadHashV1Prefix    = PackPayloadHashAlgorithmV1 + ":"

	// Packs that do not provide source metadata synthesize manifest fields until
	// they are moved behind a real manifest.
	BundledManifestFormatVersion        = "1"
	BundledPackVersion                  = "0.0.0"
	BundledPlatformCompatibilityPayload = "null"
)

type PayloadHashFormat string

const (
	PayloadHashFormatLegacyV0 PayloadHashFormat = "legacy-v0"
	PayloadHashFormatV1       PayloadHashFormat = "v1"
)

type PackHashMetadata struct {
	PackVersion                string
	ManifestFormatVersion      string
	PlatformCompatibility      string
	PayloadHashAlgorithm       string
	SourceKind                 string
	SourceURI                  string
	SourceRevision             string
	SourceKindValid            bool
	SourceURIValid             bool
	SourceRevisionValid        bool
	PlatformCompatibilityValid bool
}

type PackHashMetadataProvider interface {
	PackHashMetadata() PackHashMetadata
}

// RuntimeRequiredPacks returns packs that must be present for the runtime
// server to function. DevSample and app/distribution packs are excluded.
func RuntimeRequiredPacks() []Pack {
	return []Pack{
		&PlatformCorePack{},
		&PlatformPolicyPack{},
		&PlatformConfigPack{},
		&PlatformUXPack{},
	}
}

// AllPacks returns runtime platform packs. App/distribution packs are installed
// from source through setup install-pack; they are not linked into the platform
// binary.
func AllPacks(includeDevSample bool) []Pack {
	p := RuntimeRequiredPacks()
	if includeDevSample {
		p = append(p, &DevSamplePack{})
	}
	return p
}

// KnownPackByName returns a pack implementation by its stable pack name.
// Installed-pack verification treats an unknown registry row as drift between
// database state and the current binary, not as a silently ignored row.
func KnownPackByName(name string) (Pack, bool) {
	for _, pack := range allKnownPacks() {
		if pack.Name() == name {
			return pack, true
		}
	}
	return nil, false
}

func allKnownPacks() []Pack {
	p := RuntimeRequiredPacks()
	p = append(p, &DevSamplePack{})
	return p
}

// PackPayloadHash returns the current binary payload hash for packs that own
// shipped metadata payload. App packs must implement PayloadHasher before they
// can be written to _System_InstalledPack.
func PackPayloadHash(pack Pack) (string, error) {
	hasher, ok := pack.(PayloadHasher)
	if !ok {
		return "", fmt.Errorf("pack %q does not expose a payload hash", pack.Name())
	}
	hash, err := hasher.PayloadHash()
	if err != nil {
		return "", err
	}
	if hash == "" {
		return "", fmt.Errorf("pack %q returned empty payload hash", pack.Name())
	}
	if !IsPackPayloadHashV1(hash) {
		return "", fmt.Errorf("pack %q returned invalid v1 payload hash %q", pack.Name(), hash)
	}
	return hash, nil
}

func LegacyPackPayloadHash(pack Pack) (string, error) {
	if hasher, ok := pack.(LegacyPayloadHasher); ok {
		hash, err := hasher.LegacyPayloadHash()
		if err != nil {
			return "", err
		}
		if !isLowerHex64(hash) {
			return "", fmt.Errorf("pack %q returned invalid legacy payload hash %q", pack.Name(), hash)
		}
		return hash, nil
	}
	composable, ok := pack.(ComposablePack)
	if !ok {
		return "", fmt.Errorf("pack %q does not expose a legacy payload hash", pack.Name())
	}
	return legacyPackComponentsPayloadHashV0(composable.Components())
}

func ValidateStoredPackPayloadHash(pack Pack, storedHash string) (PayloadHashFormat, error) {
	storedHash = strings.TrimSpace(storedHash)
	if storedHash == "" {
		return "", fmt.Errorf("empty payload hash")
	}
	if strings.HasPrefix(storedHash, PackPayloadHashV1Prefix) {
		if !IsPackPayloadHashV1(storedHash) {
			return "", fmt.Errorf("invalid v1 payload hash format %q", storedHash)
		}
		expected, err := PackPayloadHash(pack)
		if err != nil {
			return "", err
		}
		if storedHash != expected {
			return "", fmt.Errorf("v1 payload hash mismatch: stored=%q current=%q", storedHash, expected)
		}
		return PayloadHashFormatV1, nil
	}
	if isLowerHex64(storedHash) {
		expected, err := LegacyPackPayloadHash(pack)
		if err != nil {
			return "", err
		}
		if storedHash != expected {
			return "", fmt.Errorf("legacy v0 payload hash mismatch: stored=%q current=%q", storedHash, expected)
		}
		return PayloadHashFormatLegacyV0, nil
	}
	return "", fmt.Errorf("unsupported payload hash format %q", storedHash)
}

func IsPackPayloadHashV1(hash string) bool {
	if !strings.HasPrefix(hash, PackPayloadHashV1Prefix) {
		return false
	}
	return isLowerHex64(strings.TrimPrefix(hash, PackPayloadHashV1Prefix))
}

func PackHashMetadataForPack(pack Pack) PackHashMetadata {
	if provider, ok := pack.(PackHashMetadataProvider); ok {
		return normalizePackHashMetadata(provider.PackHashMetadata())
	}
	return normalizePackHashMetadata(PackHashMetadata{})
}

func normalizePackHashMetadata(meta PackHashMetadata) PackHashMetadata {
	if strings.TrimSpace(meta.PackVersion) == "" {
		meta.PackVersion = BundledPackVersion
	}
	if strings.TrimSpace(meta.ManifestFormatVersion) == "" {
		meta.ManifestFormatVersion = BundledManifestFormatVersion
	}
	if !meta.PlatformCompatibilityValid {
		meta.PlatformCompatibility = BundledPlatformCompatibilityPayload
		meta.PlatformCompatibilityValid = true
	}
	if strings.TrimSpace(meta.PayloadHashAlgorithm) == "" {
		meta.PayloadHashAlgorithm = PackPayloadHashAlgorithmV1
	}
	return meta
}
