package catalogsnapshot

import (
	"bytes"
	"errors"
	"fmt"
	"strings"
)

const RuntimeWorkBindingSchemaV1 = "nexuscrm.runtime-work-metadata-binding/v1"
const RuntimeWorkBindingSetSchemaV1 = "nexuscrm.runtime-work-metadata-binding-set/v1"

// RuntimeWorkBinding is the app-blind identity of the immutable metadata view
// that caused durable Runtime work to be created. Work-family packages own
// their payloads and effects; Catalog remains the sole owner of this binding.
//
// Persisting only a revision is insufficient: the two content digests prove
// the exact compiler input and least-privilege projection that a worker must
// reopen before interpreting the payload.
type RuntimeWorkBinding struct {
	OrgID                     string `json:"orgId"`
	AppInstallationID         string `json:"appInstallationId"`
	RuntimeCapabilityID       string `json:"runtimeCapabilityId"`
	CatalogRevision           uint64 `json:"catalogRevision"`
	SchemaGeneration          uint64 `json:"schemaGeneration"`
	EngineGeneration          uint64 `json:"engineGeneration"`
	CatalogSnapshotDigest     Digest `json:"catalogSnapshotDigest"`
	RuntimeMetadataViewDigest Digest `json:"runtimeMetadataViewDigest"`
}

type runtimeWorkBindingDocument struct {
	Schema  string             `json:"schema"`
	Binding RuntimeWorkBinding `json:"binding"`
}

// RuntimeWorkBindingSet carries one durable row's primary execution view and,
// when record mutation can produce timed Flow work, the Flow capability view
// from the same immutable Catalog lineage. Durable transports keep this
// envelope opaque; the worker verifies both artifacts before installing
// either. A single-binding v1 document remains the N-1 representation.
type RuntimeWorkBindingSet struct {
	Primary  RuntimeWorkBinding  `json:"primary"`
	FlowWork *RuntimeWorkBinding `json:"flowWork,omitempty"`
}

type runtimeWorkBindingSetDocument struct {
	Schema  string                `json:"schema"`
	Binding RuntimeWorkBindingSet `json:"binding"`
}

// RuntimeWorkBindingFromView derives the durable identity from the already
// verified request-bound view. It does not select a current revision.
func RuntimeWorkBindingFromView(view *RuntimeMetadataView) (RuntimeWorkBinding, error) {
	if view == nil {
		return RuntimeWorkBinding{}, errors.New("runtime work binding: metadata view is required")
	}
	lineage := view.Lineage()
	binding := RuntimeWorkBinding{
		OrgID: view.OrgID(), AppInstallationID: view.AppInstallationID(), RuntimeCapabilityID: view.CapabilityID(),
		CatalogRevision: uint64(lineage.CatalogRevision), SchemaGeneration: uint64(lineage.SchemaGeneration),
		EngineGeneration: uint64(lineage.EngineGeneration), CatalogSnapshotDigest: view.SourceSnapshotDigest(),
		RuntimeMetadataViewDigest: view.Digest(),
	}
	if err := binding.Validate(); err != nil {
		return RuntimeWorkBinding{}, err
	}
	return binding, nil
}

func (b RuntimeWorkBinding) Validate() error {
	for name, value := range map[string]string{
		"org": b.OrgID, "App installation": b.AppInstallationID, "Runtime capability": b.RuntimeCapabilityID,
	} {
		if value == "" || strings.TrimSpace(value) != value || len(value) > 255 {
			return fmt.Errorf("runtime work binding: %s identity is invalid", name)
		}
	}
	if b.CatalogRevision == 0 || b.SchemaGeneration == 0 || b.EngineGeneration == 0 ||
		!validDigest(b.CatalogSnapshotDigest) || !validDigest(b.RuntimeMetadataViewDigest) {
		return errors.New("runtime work binding: immutable definition binding is incomplete")
	}
	return nil
}

// EncodeRuntimeWorkBinding returns deterministic bytes suitable for an opaque
// durable-work envelope. The schema is versioned independently from queue
// payloads so unsupported future binding semantics fail closed.
func EncodeRuntimeWorkBinding(binding RuntimeWorkBinding) (string, error) {
	if err := binding.Validate(); err != nil {
		return "", err
	}
	encoded, err := marshalCanonical(runtimeWorkBindingDocument{Schema: RuntimeWorkBindingSchemaV1, Binding: binding})
	if err != nil {
		return "", fmt.Errorf("runtime work binding: encode: %w", err)
	}
	return string(encoded), nil
}

func OpenRuntimeWorkBinding(encoded string) (RuntimeWorkBinding, error) {
	if encoded == "" || len(encoded) > 4096 {
		return RuntimeWorkBinding{}, errors.New("runtime work binding: encoded binding is absent or oversized")
	}
	var doc runtimeWorkBindingDocument
	if err := decodeStrict([]byte(encoded), &doc); err != nil {
		return RuntimeWorkBinding{}, fmt.Errorf("runtime work binding: decode: %w", err)
	}
	if doc.Schema != RuntimeWorkBindingSchemaV1 {
		return RuntimeWorkBinding{}, fmt.Errorf("runtime work binding: unsupported schema %q", doc.Schema)
	}
	canonical, err := marshalCanonical(doc)
	if err != nil || !bytes.Equal(canonical, []byte(encoded)) {
		return RuntimeWorkBinding{}, errors.New("runtime work binding: bytes are not canonical")
	}
	if err := doc.Binding.Validate(); err != nil {
		return RuntimeWorkBinding{}, err
	}
	return doc.Binding, nil
}

func EncodeRuntimeWorkBindingSet(binding RuntimeWorkBindingSet) (string, error) {
	if err := binding.Validate(); err != nil {
		return "", err
	}
	if binding.FlowWork == nil {
		return EncodeRuntimeWorkBinding(binding.Primary)
	}
	encoded, err := marshalCanonical(runtimeWorkBindingSetDocument{
		Schema: RuntimeWorkBindingSetSchemaV1, Binding: binding,
	})
	if err != nil {
		return "", fmt.Errorf("runtime work binding set: encode: %w", err)
	}
	return string(encoded), nil
}

// OpenRuntimeWorkBindingSet accepts the current set document and the N-1
// single-binding document. It never selects or upgrades metadata; compatibility
// here is only the opaque durable envelope grammar.
func OpenRuntimeWorkBindingSet(encoded string) (RuntimeWorkBindingSet, error) {
	if primary, err := OpenRuntimeWorkBinding(encoded); err == nil {
		return RuntimeWorkBindingSet{Primary: primary}, nil
	}
	if encoded == "" || len(encoded) > 8192 {
		return RuntimeWorkBindingSet{}, errors.New("runtime work binding set: encoded binding is absent or oversized")
	}
	var doc runtimeWorkBindingSetDocument
	if err := decodeStrict([]byte(encoded), &doc); err != nil {
		return RuntimeWorkBindingSet{}, fmt.Errorf("runtime work binding set: decode: %w", err)
	}
	if doc.Schema != RuntimeWorkBindingSetSchemaV1 {
		return RuntimeWorkBindingSet{}, fmt.Errorf("runtime work binding set: unsupported schema %q", doc.Schema)
	}
	canonical, err := marshalCanonical(doc)
	if err != nil || !bytes.Equal(canonical, []byte(encoded)) {
		return RuntimeWorkBindingSet{}, errors.New("runtime work binding set: bytes are not canonical")
	}
	if err := doc.Binding.Validate(); err != nil {
		return RuntimeWorkBindingSet{}, err
	}
	return doc.Binding, nil
}

func (b RuntimeWorkBindingSet) Validate() error {
	if err := b.Primary.Validate(); err != nil {
		return err
	}
	if b.FlowWork == nil {
		return nil
	}
	if err := b.FlowWork.Validate(); err != nil {
		return err
	}
	if b.FlowWork.OrgID != b.Primary.OrgID ||
		b.FlowWork.AppInstallationID != b.Primary.AppInstallationID ||
		b.FlowWork.CatalogRevision != b.Primary.CatalogRevision ||
		b.FlowWork.SchemaGeneration != b.Primary.SchemaGeneration ||
		b.FlowWork.EngineGeneration != b.Primary.EngineGeneration ||
		b.FlowWork.CatalogSnapshotDigest != b.Primary.CatalogSnapshotDigest {
		return errors.New("runtime work binding set: primary and Flow work bindings do not share one immutable Catalog lineage")
	}
	return nil
}

// VerifyView proves that one reopened view is exactly the immutable projection
// named by the durable work. The expected profile belongs to the consuming
// engine, not to the payload or to an L1 caller.
func (b RuntimeWorkBinding) VerifyView(view *RuntimeMetadataView, expectedProfile ProjectionProfile) error {
	if err := b.Validate(); err != nil {
		return err
	}
	if view == nil || expectedProfile == "" || view.Profile() != expectedProfile ||
		view.Digest() != b.RuntimeMetadataViewDigest || view.SourceSnapshotDigest() != b.CatalogSnapshotDigest ||
		view.OrgID() != b.OrgID || view.AppInstallationID() != b.AppInstallationID || view.CapabilityID() != b.RuntimeCapabilityID {
		return errors.New("runtime work binding: immutable view does not match durable work identity")
	}
	lineage := view.Lineage()
	if uint64(lineage.CatalogRevision) != b.CatalogRevision || uint64(lineage.SchemaGeneration) != b.SchemaGeneration ||
		uint64(lineage.EngineGeneration) != b.EngineGeneration {
		return errors.New("runtime work binding: immutable view lineage does not match durable work identity")
	}
	return nil
}
