package catalogsnapshot

import (
	"encoding/json"
	"fmt"
)

// ServingCompatibilityReceiptInput is the closed set of immutable facts that
// binds one Catalog HandlerBinding to one exact Runtime metadata projection.
// Catalog owns selection by publishing the resulting digest on the active
// HandlerBinding; Runtime configuration may only assert that selection.
type ServingCompatibilityReceiptInput struct {
	OrgID                string
	AppInstallationID    string
	AppReleaseDigest     Digest
	AppCapabilityID      string
	RuntimeCapabilityID  string
	WorkloadID           string
	ServiceReleaseDigest Digest
	// MetadataSourceDigest identifies the immutable operator-installed source
	// from which Catalog read effective metadata. It proves input lineage; it
	// is not a second serving selector and Runtime still reads only the Catalog
	// snapshot/view digests below.
	MetadataSourceDigest      Digest
	OwnershipEpoch            uint64
	CatalogSnapshotDigest     Digest
	RuntimeMetadataViewDigest Digest
	Lineage                   Lineage
	SecurityEpoch             uint64
}

type servingCompatibilityReceiptDocument struct {
	Schema                    string  `json:"schema"`
	OrgID                     string  `json:"orgId"`
	AppInstallationID         string  `json:"appInstallationId"`
	AppReleaseDigest          Digest  `json:"appReleaseDigest"`
	AppCapabilityID           string  `json:"appCapabilityId"`
	RuntimeCapabilityID       string  `json:"runtimeCapabilityId"`
	WorkloadID                string  `json:"workloadId"`
	ServiceReleaseDigest      Digest  `json:"serviceReleaseDigest"`
	MetadataSourceDigest      Digest  `json:"metadataSourceDigest"`
	OwnershipEpoch            uint64  `json:"ownershipEpoch"`
	CatalogSnapshotDigest     Digest  `json:"catalogSnapshotDigest"`
	RuntimeMetadataViewDigest Digest  `json:"runtimeMetadataViewDigest"`
	Lineage                   Lineage `json:"lineage"`
	SecurityEpoch             uint64  `json:"securityEpoch"`
}

type servingCompatibilityReceiptDocumentV1 struct {
	Schema                    string  `json:"schema"`
	OrgID                     string  `json:"orgId"`
	AppInstallationID         string  `json:"appInstallationId"`
	AppReleaseDigest          Digest  `json:"appReleaseDigest"`
	AppCapabilityID           string  `json:"appCapabilityId"`
	RuntimeCapabilityID       string  `json:"runtimeCapabilityId"`
	WorkloadID                string  `json:"workloadId"`
	ServiceReleaseDigest      Digest  `json:"serviceReleaseDigest"`
	OwnershipEpoch            uint64  `json:"ownershipEpoch"`
	CatalogSnapshotDigest     Digest  `json:"catalogSnapshotDigest"`
	RuntimeMetadataViewDigest Digest  `json:"runtimeMetadataViewDigest"`
	Lineage                   Lineage `json:"lineage"`
	SecurityEpoch             uint64  `json:"securityEpoch"`
}

// ServingCompatibilityReceipt is immutable and content-addressed. It is not a
// mutable registry or a second Catalog read path.
type ServingCompatibilityReceipt struct {
	digest Digest
	doc    servingCompatibilityReceiptDocument
}

func BuildServingCompatibilityReceipt(input ServingCompatibilityReceiptInput) (Artifact, error) {
	schema := ServingCompatibilityReceiptSchemaV1
	if input.MetadataSourceDigest != "" {
		schema = ServingCompatibilityReceiptSchemaV2
	}
	doc := servingCompatibilityReceiptDocument{
		Schema: schema,
		OrgID:  input.OrgID, AppInstallationID: input.AppInstallationID,
		AppReleaseDigest: input.AppReleaseDigest, AppCapabilityID: input.AppCapabilityID,
		RuntimeCapabilityID: input.RuntimeCapabilityID, WorkloadID: input.WorkloadID,
		ServiceReleaseDigest: input.ServiceReleaseDigest, MetadataSourceDigest: input.MetadataSourceDigest,
		OwnershipEpoch:            input.OwnershipEpoch,
		CatalogSnapshotDigest:     input.CatalogSnapshotDigest,
		RuntimeMetadataViewDigest: input.RuntimeMetadataViewDigest,
		Lineage:                   input.Lineage, SecurityEpoch: input.SecurityEpoch,
	}
	if err := validateServingCompatibilityReceiptDocument(doc, schema == ServingCompatibilityReceiptSchemaV2); err != nil {
		return Artifact{}, err
	}
	var canonical any = doc
	if schema == ServingCompatibilityReceiptSchemaV1 {
		canonical = receiptDocumentV1(doc)
	}
	data, err := marshalCanonical(canonical)
	if err != nil {
		return Artifact{}, fmt.Errorf("build serving compatibility receipt: %w", err)
	}
	return makeArtifact(ArtifactServingCompatibilityReceipt, data), nil
}

func OpenServingCompatibilityReceipt(artifact Artifact) (*ServingCompatibilityReceipt, error) {
	if artifact.kind != ArtifactServingCompatibilityReceipt {
		return nil, fmt.Errorf("%w: expected %s, got %s", ErrArtifactCorrupt, ArtifactServingCompatibilityReceipt, artifact.kind)
	}
	if err := verifyArtifactBytes(artifact); err != nil {
		return nil, err
	}
	var header struct {
		Schema string `json:"schema"`
	}
	if err := json.Unmarshal(artifact.bytes, &header); err != nil {
		return nil, fmt.Errorf("%w: decode serving compatibility receipt schema: %v", ErrArtifactCorrupt, err)
	}
	var doc servingCompatibilityReceiptDocument
	switch header.Schema {
	case ServingCompatibilityReceiptSchemaV1:
		var legacy servingCompatibilityReceiptDocumentV1
		if err := decodeStrict(artifact.bytes, &legacy); err != nil {
			return nil, fmt.Errorf("%w: decode serving compatibility receipt: %v", ErrArtifactCorrupt, err)
		}
		if err := requireCanonicalDocument(artifact.bytes, legacy); err != nil {
			return nil, err
		}
		doc = receiptDocumentFromV1(legacy)
	case ServingCompatibilityReceiptSchemaV2:
		if err := decodeStrict(artifact.bytes, &doc); err != nil {
			return nil, fmt.Errorf("%w: decode serving compatibility receipt: %v", ErrArtifactCorrupt, err)
		}
		if err := requireCanonicalDocument(artifact.bytes, doc); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("%w: serving compatibility receipt schema %q", ErrUnsupportedSchema, header.Schema)
	}
	if err := validateServingCompatibilityReceiptDocument(doc, header.Schema == ServingCompatibilityReceiptSchemaV2); err != nil {
		return nil, fmt.Errorf("%w: %v", ErrArtifactCorrupt, err)
	}
	return &ServingCompatibilityReceipt{digest: artifact.digest, doc: doc}, nil
}

func validateServingCompatibilityReceiptDocument(doc servingCompatibilityReceiptDocument, requireMetadataSource bool) error {
	for path, value := range map[string]string{
		"orgId": doc.OrgID, "appInstallationId": doc.AppInstallationID,
		"appCapabilityId": doc.AppCapabilityID, "runtimeCapabilityId": doc.RuntimeCapabilityID,
		"workloadId": doc.WorkloadID,
	} {
		if err := requireCanonicalIdentity(path, value); err != nil {
			return err
		}
	}
	if doc.AppCapabilityID == doc.RuntimeCapabilityID {
		return fmt.Errorf("App and Runtime capability identities must be distinct")
	}
	for path, digest := range map[string]Digest{
		"appReleaseDigest":          doc.AppReleaseDigest,
		"serviceReleaseDigest":      doc.ServiceReleaseDigest,
		"catalogSnapshotDigest":     doc.CatalogSnapshotDigest,
		"runtimeMetadataViewDigest": doc.RuntimeMetadataViewDigest,
	} {
		if !validDigest(digest) {
			return fmt.Errorf("%s is not a canonical sha256 digest", path)
		}
	}
	if requireMetadataSource && !validDigest(doc.MetadataSourceDigest) {
		return fmt.Errorf("metadataSourceDigest is not a canonical sha256 digest")
	}
	if !requireMetadataSource && doc.MetadataSourceDigest != "" {
		return fmt.Errorf("v1 metadataSourceDigest must be absent")
	}
	if doc.OwnershipEpoch == 0 || doc.SecurityEpoch == 0 {
		return fmt.Errorf("ownership and security epochs must be explicit and non-zero")
	}
	return validateLineage(doc.Lineage)
}

func receiptDocumentV1(doc servingCompatibilityReceiptDocument) servingCompatibilityReceiptDocumentV1 {
	return servingCompatibilityReceiptDocumentV1{
		Schema: doc.Schema, OrgID: doc.OrgID, AppInstallationID: doc.AppInstallationID,
		AppReleaseDigest: doc.AppReleaseDigest, AppCapabilityID: doc.AppCapabilityID,
		RuntimeCapabilityID: doc.RuntimeCapabilityID, WorkloadID: doc.WorkloadID,
		ServiceReleaseDigest: doc.ServiceReleaseDigest, OwnershipEpoch: doc.OwnershipEpoch,
		CatalogSnapshotDigest: doc.CatalogSnapshotDigest, RuntimeMetadataViewDigest: doc.RuntimeMetadataViewDigest,
		Lineage: doc.Lineage, SecurityEpoch: doc.SecurityEpoch,
	}
}

func receiptDocumentFromV1(doc servingCompatibilityReceiptDocumentV1) servingCompatibilityReceiptDocument {
	return servingCompatibilityReceiptDocument{
		Schema: doc.Schema, OrgID: doc.OrgID, AppInstallationID: doc.AppInstallationID,
		AppReleaseDigest: doc.AppReleaseDigest, AppCapabilityID: doc.AppCapabilityID,
		RuntimeCapabilityID: doc.RuntimeCapabilityID, WorkloadID: doc.WorkloadID,
		ServiceReleaseDigest: doc.ServiceReleaseDigest, OwnershipEpoch: doc.OwnershipEpoch,
		CatalogSnapshotDigest: doc.CatalogSnapshotDigest, RuntimeMetadataViewDigest: doc.RuntimeMetadataViewDigest,
		Lineage: doc.Lineage, SecurityEpoch: doc.SecurityEpoch,
	}
}

func (r *ServingCompatibilityReceipt) Digest() Digest              { return r.digest }
func (r *ServingCompatibilityReceipt) OrgID() string               { return r.doc.OrgID }
func (r *ServingCompatibilityReceipt) AppInstallationID() string   { return r.doc.AppInstallationID }
func (r *ServingCompatibilityReceipt) AppReleaseDigest() Digest    { return r.doc.AppReleaseDigest }
func (r *ServingCompatibilityReceipt) AppCapabilityID() string     { return r.doc.AppCapabilityID }
func (r *ServingCompatibilityReceipt) RuntimeCapabilityID() string { return r.doc.RuntimeCapabilityID }
func (r *ServingCompatibilityReceipt) WorkloadID() string          { return r.doc.WorkloadID }
func (r *ServingCompatibilityReceipt) ServiceReleaseDigest() Digest {
	return r.doc.ServiceReleaseDigest
}
func (r *ServingCompatibilityReceipt) MetadataSourceDigest() Digest {
	return r.doc.MetadataSourceDigest
}
func (r *ServingCompatibilityReceipt) OwnershipEpoch() uint64 { return r.doc.OwnershipEpoch }
func (r *ServingCompatibilityReceipt) CatalogSnapshotDigest() Digest {
	return r.doc.CatalogSnapshotDigest
}
func (r *ServingCompatibilityReceipt) RuntimeMetadataViewDigest() Digest {
	return r.doc.RuntimeMetadataViewDigest
}
func (r *ServingCompatibilityReceipt) Lineage() Lineage      { return r.doc.Lineage }
func (r *ServingCompatibilityReceipt) SecurityEpoch() uint64 { return r.doc.SecurityEpoch }
