package catalogreconcile

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"slices"
	"strings"
	"time"

	"github.com/pingcap-inc/nexuscrm-controlplane/catalogapp"
	"github.com/pingcap-inc/nexuscrm-controlplane/releaseartifact"
)

type ReleaseAuthority interface {
	VerifyReleaseCandidate(context.Context, []byte, []byte, catalogapp.PublisherTrust, string, releaseartifact.HostCompatibility) (catalogapp.VerifiedReleaseCandidate, error)
	ReadPublisher(context.Context, string) (catalogapp.RegisteredPublisher, error)
	RegisterPublisher(context.Context, catalogapp.PublisherTrust, uint64) (uint64, error)
	ReserveAppID(context.Context, string, string, uint64) error
	ImportReleaseArtifact(context.Context, []byte, []byte, releaseartifact.HostCompatibility) (catalogapp.RegisteredRelease, error)
}

type ReleaseReader func(context.Context, string, string) (catalogapp.RegisteredRelease, error)

type ReleaseInput struct {
	Publisher                catalogapp.PublisherTrust
	AppID                    string
	ExpectedAppReleaseDigest string
	PackageBytes             []byte
	Attestation              []byte
	Compatibility            releaseartifact.HostCompatibility
}

// MissingReleaseProvisioner performs the separate release-onboarding flow only
// when the exact immutable release is not already registered in Catalog.
// Callers must not use it to refresh or re-author an existing release.
type MissingReleaseProvisioner func(context.Context) (catalogapp.RegisteredRelease, error)

// ResolveRelease makes the Catalog release registry the lifecycle source of
// truth. Registered releases remain valid operation targets after publisher
// key rotation, revocation, or expiry; signature and trust verification are
// admission-time checks performed only while importing a missing release.
// Current host compatibility is still evaluated on every invocation from the
// immutable package stored by Catalog.
func ResolveRelease(ctx context.Context, reader ReleaseReader, appID, digest string, compatibility releaseartifact.HostCompatibility, provision MissingReleaseProvisioner) (catalogapp.RegisteredRelease, error) {
	if reader == nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("Catalog release reader is required")
	}
	release, err := reader(ctx, appID, digest)
	if err == nil {
		if err := validateResolvedRelease(release, appID, digest, compatibility); err != nil {
			return catalogapp.RegisteredRelease{}, err
		}
		return release, nil
	}
	if !errors.Is(err, catalogapp.ErrNotFound) {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("read registered AppRelease: %w", err)
	}
	if provision == nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("AppRelease %s for App %s is not registered and no import material was provided", digest, appID)
	}
	if err := ctx.Err(); err != nil {
		return catalogapp.RegisteredRelease{}, err
	}
	release, err = provision(ctx)
	if err != nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("provision missing AppRelease: %w", err)
	}
	if err := validateResolvedRelease(release, appID, digest, compatibility); err != nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("provisioned AppRelease is invalid: %w", err)
	}
	return release, nil
}

func validateResolvedRelease(release catalogapp.RegisteredRelease, appID, digest string, compatibility releaseartifact.HostCompatibility) error {
	if release.AppID != appID || release.AppReleaseDigest != digest {
		return fmt.Errorf("Catalog AppRelease does not match the pinned release identity")
	}
	if err := releaseartifact.VerifyCompatibility(release.Package, compatibility); err != nil {
		return fmt.Errorf("registered AppRelease is incompatible with this host: %w", err)
	}
	return nil
}

// ProvisionRelease first asks the Catalog owner to verify the complete release
// candidate without durable side effects. Only then may it establish the
// verified publisher/App identity and import the same immutable bytes through
// the existing A1c owner. Existing exact trust is reused. A mismatched trust
// set is never rotation authority: publisher-key rotation has its own explicit
// operator-authorized lifecycle and cannot be smuggled through release import.
func ProvisionRelease(ctx context.Context, authority ReleaseAuthority, input ReleaseInput) (catalogapp.RegisteredRelease, error) {
	if authority == nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("Catalog release authority is required")
	}
	// Freeze caller-owned carriers once so preflight and durable import cannot
	// observe different bytes or compatibility/trust facts.
	packageBytes := bytes.Clone(input.PackageBytes)
	attestation := bytes.Clone(input.Attestation)
	publisher := clonePublisherTrust(input.Publisher)
	compatibility := releaseartifact.HostCompatibility{
		Protocols:          slices.Clone(input.Compatibility.Protocols),
		EngineCapabilities: slices.Clone(input.Compatibility.EngineCapabilities),
		ComponentKinds:     slices.Clone(input.Compatibility.ComponentKinds),
	}
	candidate, err := authority.VerifyReleaseCandidate(ctx, packageBytes, attestation, publisher, input.AppID, compatibility)
	if err != nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("verify immutable AppRelease candidate: %w", err)
	}
	if candidate.AppReleaseDigest != input.ExpectedAppReleaseDigest {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("verified AppRelease digest does not match the pinned release identity")
	}
	epoch, err := ensurePublisher(ctx, authority, publisher)
	if err != nil {
		return catalogapp.RegisteredRelease{}, err
	}
	if err := authority.ReserveAppID(ctx, candidate.PublisherID, candidate.AppID, epoch); err != nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("reserve App identity: %w", err)
	}
	release, err := authority.ImportReleaseArtifact(ctx, packageBytes, attestation, compatibility)
	if err != nil {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("import immutable AppRelease: %w", err)
	}
	if !sameRelease(candidate, release) {
		return catalogapp.RegisteredRelease{}, fmt.Errorf("imported AppRelease does not match the verified release candidate")
	}
	return release, nil
}

func clonePublisherTrust(trust catalogapp.PublisherTrust) catalogapp.PublisherTrust {
	trust.Keys = slices.Clone(trust.Keys)
	for i := range trust.Keys {
		trust.Keys[i].PublicKey = bytes.Clone(trust.Keys[i].PublicKey)
	}
	return trust
}

func sameRelease(left catalogapp.VerifiedReleaseCandidate, right catalogapp.RegisteredRelease) bool {
	return left.AppID == right.AppID && left.PublisherID == right.PublisherID &&
		left.SemanticVersion == right.SemanticVersion && left.AppReleaseDigest == right.AppReleaseDigest &&
		left.MetadataBundleDigest == right.MetadataBundleDigest
}

func ensurePublisher(ctx context.Context, authority ReleaseAuthority, trust catalogapp.PublisherTrust) (uint64, error) {
	existing, err := authority.ReadPublisher(ctx, trust.PublisherID)
	if errors.Is(err, catalogapp.ErrNotFound) {
		epoch, registerErr := authority.RegisterPublisher(ctx, trust, 0)
		if registerErr != nil {
			return 0, fmt.Errorf("register release publisher: %w", registerErr)
		}
		return epoch, nil
	}
	if err != nil {
		return 0, fmt.Errorf("read release publisher: %w", err)
	}
	if !publisherMatches(existing, trust) {
		return 0, fmt.Errorf("release publisher trust differs from registered authority: %w", catalogapp.ErrConflict)
	}
	return existing.OwnershipEpoch, nil
}

func publisherMatches(existing catalogapp.RegisteredPublisher, trust catalogapp.PublisherTrust) bool {
	if existing.PublisherID != trust.PublisherID || existing.Active != trust.Active {
		return false
	}
	active := make([]catalogapp.RegisteredPublisherKey, 0, len(existing.Keys))
	for _, key := range existing.Keys {
		if key.Active {
			active = append(active, key)
		}
	}
	if len(active) != len(trust.Keys) {
		return false
	}
	want := append([]catalogapp.PublisherKey(nil), trust.Keys...)
	slices.SortFunc(want, func(left, right catalogapp.PublisherKey) int { return strings.Compare(left.KeyID, right.KeyID) })
	slices.SortFunc(active, func(left, right catalogapp.RegisteredPublisherKey) int {
		return strings.Compare(left.KeyID, right.KeyID)
	})
	for i, key := range want {
		got := active[i]
		if got.KeyID != key.KeyID || got.Algorithm != key.Algorithm ||
			!bytes.Equal(got.PublicKey, key.PublicKey) || !sameCatalogTime(got.ValidFrom, key.ValidFrom) ||
			!sameCatalogTime(got.ValidUntil, key.ValidUntil) {
			return false
		}
	}
	return true
}

func sameCatalogTime(left, right time.Time) bool {
	return left.UTC().Truncate(time.Microsecond).Equal(right.UTC().Truncate(time.Microsecond))
}
