package catalogreconcile

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"

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

// MarshalOutput emits the closed canonical handoff and its content digest.
// Output uses a field-only struct, so encoding/json order is deterministic.
func MarshalOutput(output Output) ([]byte, string, error) {
	if output.Schema != OutputSchemaV1 && output.Schema != OutputSchemaV2 && output.Schema != OutputSchemaV3 {
		return nil, "", fmt.Errorf("unsupported Catalog reconcile output schema %q", output.Schema)
	}
	if output.Schema == OutputSchemaV2 || output.Schema == OutputSchemaV3 {
		if strings.TrimSpace(output.MetadataSourceDigest) == "" {
			return nil, "", fmt.Errorf("Catalog reconcile output metadataSourceDigest is required")
		}
	} else if output.MetadataSourceDigest != "" {
		return nil, "", fmt.Errorf("Catalog reconcile v1 output cannot declare metadataSourceDigest")
	}
	required := map[string]string{
		"operationId": output.OperationID, "orgId": output.OrgID,
		"appInstallationId": output.AppInstallationID, "appReleaseDigest": output.AppReleaseDigest,
		"workloadId":            output.WorkloadID,
		"serviceReleaseDigest":  output.ServiceReleaseDigest,
		"catalogSnapshotDigest": output.CatalogSnapshotDigest,
	}
	if output.Schema == OutputSchemaV3 {
		if output.AppCapabilityID != "" || output.RuntimeCapabilityID != "" || output.HandlerBindingID != "" ||
			output.RuntimeMetadataViewDigest != "" || output.CompatibilityReceiptDigest != "" || output.OwnershipEpoch != 0 {
			return nil, "", fmt.Errorf("Catalog reconcile v3 output cannot declare singular capability facts")
		}
		if len(output.Capabilities) == 0 {
			return nil, "", fmt.Errorf("Catalog reconcile v3 output requires at least one capability fact")
		}
		seenRuntime := make(map[string]struct{}, len(output.Capabilities))
		for i, capability := range output.Capabilities {
			if i > 0 && output.Capabilities[i-1].AppCapabilityID >= capability.AppCapabilityID {
				return nil, "", fmt.Errorf("Catalog reconcile v3 capability facts must be unique and canonically ordered")
			}
			if _, duplicate := seenRuntime[capability.RuntimeCapabilityID]; duplicate {
				return nil, "", fmt.Errorf("Catalog reconcile v3 capability facts must be unique and canonically ordered")
			}
			seenRuntime[capability.RuntimeCapabilityID] = struct{}{}
			if _, err := projectionProfile(capability.RuntimeCapabilityID); err != nil {
				return nil, "", err
			}
			for name, value := range map[string]string{
				"appCapabilityId":            capability.AppCapabilityID,
				"runtimeCapabilityId":        capability.RuntimeCapabilityID,
				"handlerBindingId":           capability.HandlerBindingID,
				"runtimeMetadataViewDigest":  capability.RuntimeMetadataViewDigest,
				"compatibilityReceiptDigest": capability.CompatibilityReceiptDigest,
			} {
				if strings.TrimSpace(value) == "" {
					return nil, "", fmt.Errorf("Catalog reconcile output capabilities[%d].%s is required", i, name)
				}
			}
			if capability.OwnershipEpoch == 0 {
				return nil, "", fmt.Errorf("Catalog reconcile output capability ownership epoch must be non-zero")
			}
		}
	} else {
		if output.LayoutAuthoringViewRef != nil {
			return nil, "", fmt.Errorf("Catalog reconcile v1/v2 output cannot declare a Layout authoring view ref")
		}
		required["appCapabilityId"] = output.AppCapabilityID
		required["runtimeCapabilityId"] = output.RuntimeCapabilityID
		required["handlerBindingId"] = output.HandlerBindingID
		required["runtimeMetadataViewDigest"] = output.RuntimeMetadataViewDigest
		required["compatibilityReceiptDigest"] = output.CompatibilityReceiptDigest
		if len(output.Capabilities) != 0 {
			return nil, "", fmt.Errorf("Catalog reconcile v1/v2 output cannot declare capabilities")
		}
	}
	if output.LayoutAuthoringViewRef != nil {
		if err := layoutauthoringv1.ValidateCatalogViewRef(*output.LayoutAuthoringViewRef); err != nil {
			return nil, "", fmt.Errorf("Catalog reconcile output Layout authoring view ref: %w", err)
		}
		if output.LayoutAuthoringViewRef.CatalogRevision != output.CatalogRevision {
			return nil, "", fmt.Errorf("Catalog reconcile output Layout authoring view revision disagrees with the publication")
		}
	}
	for name, value := range required {
		if strings.TrimSpace(value) == "" {
			return nil, "", fmt.Errorf("Catalog reconcile output %s is required", name)
		}
	}
	if (output.Schema != OutputSchemaV3 && output.OwnershipEpoch == 0) || output.CatalogRevision == 0 || output.SchemaGeneration == 0 ||
		output.EngineGeneration == 0 || output.SecurityEpoch == 0 {
		return nil, "", fmt.Errorf("Catalog reconcile output epochs and generations must be non-zero")
	}
	frontendFacts := []string{output.FrontendBindingID, output.FrontendReleaseDigest, output.FrontendReceiptDigest}
	frontendCount := 0
	for _, value := range frontendFacts {
		if strings.TrimSpace(value) != "" {
			frontendCount++
		}
	}
	if frontendCount != 0 && frontendCount != len(frontendFacts) {
		return nil, "", fmt.Errorf("Catalog reconcile output frontend facts must be complete or absent")
	}
	raw, err := json.Marshal(output)
	if err != nil {
		return nil, "", err
	}
	sum := sha256.Sum256(raw)
	return raw, "sha256:" + hex.EncodeToString(sum[:]), nil
}
