package catalogreconcile

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"

	"github.com/nexuscrm/metadata/catalogsnapshot"
	"github.com/pingcap-inc/nexuscrm-contract/pkg/appauthority"
	commonv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
	"github.com/pingcap-inc/nexuscrm-controlplane/strictjson"
)

// recordViewReleaseComponentKind names the App-neutral, immutable Catalog
// input. L1 maps any product/source representation to this one release fact;
// Catalog never recognizes a CRM presentation component or source checkout.
const recordViewReleaseComponentKind = "metadata.record-view.v1"

type recordViewReleaseDocument struct {
	Resolvers []recordViewReleaseResolver `json:"resolvers"`
	Fields    []recordViewReleaseField    `json:"fields"`
}

type recordViewReleaseResolver struct {
	ResolverID                      string                         `json:"resolverId"`
	Kind                            string                         `json:"kind"`
	SourceObjectAPIName             string                         `json:"sourceObjectApiName"`
	SourceFileReferenceFieldAPIName string                         `json:"sourceFileReferenceFieldApiName"`
	SourceDeletedFieldAPIName       string                         `json:"sourceDeletedFieldApiName,omitempty"`
	SourceRecordMatcher             *recordViewSourceRecordMatcher `json:"sourceRecordMatcher"`
}

// recordViewSourceRecordMatcher is the exact immutable AppRelease fact that
// Catalog lowers into the neutral RuntimeMetadataView. A product parser
// supplies the request-scoped byte proof for one current parent-field value;
// that proof offset is not a release fact. StartOffsetBytes, by contrast, is
// a static relation inside the source record identifier and must be owned by
// the AppRelease whenever a segment matcher is used.
type recordViewSourceRecordMatcher struct {
	Kind             string  `json:"kind"`
	StartOffsetBytes *uint16 `json:"startOffsetBytes,omitempty"`
	LengthBytes      uint16  `json:"lengthBytes,omitempty"`
}

type recordViewReleaseField struct {
	ParentAPIName string `json:"parentApiName"`
	FieldAPIName  string `json:"fieldApiName"`
	ResolverID    string `json:"resolverId"`
}

// readRecordViewReleaseDocument consumes only the component bytes already
// verified by Catalog's AppRelease authority. Zero matching components means
// the App owns no attachment-source decoration. More than one would create
// competing owners for the same release fact and fails closed.
func readRecordViewReleaseDocument(release *appauthority.VerifiedPackage) (recordViewReleaseDocument, error) {
	if release == nil {
		return recordViewReleaseDocument{}, fmt.Errorf("record-view metadata requires the verified AppRelease")
	}
	verified, err := appauthority.VerifyPackage(release.Package)
	if err != nil {
		return recordViewReleaseDocument{}, fmt.Errorf("verify record-view AppRelease package: %w", err)
	}
	if verified.AppReleaseDigest != release.AppReleaseDigest {
		return recordViewReleaseDocument{}, fmt.Errorf("record-view AppRelease identity disagrees with its verified package")
	}

	match := -1
	for i, component := range verified.Bundle.Components {
		if component.ComponentKind != recordViewReleaseComponentKind {
			continue
		}
		if match >= 0 {
			return recordViewReleaseDocument{}, fmt.Errorf("record-view AppRelease contains multiple %q components", recordViewReleaseComponentKind)
		}
		if component.ComponentFormatVersion != 1 {
			return recordViewReleaseDocument{}, fmt.Errorf("record-view component %q has unsupported format version %d", component.ComponentID, component.ComponentFormatVersion)
		}
		match = i
	}
	if match < 0 {
		return recordViewReleaseDocument{Resolvers: []recordViewReleaseResolver{}, Fields: []recordViewReleaseField{}}, nil
	}

	behavior := verified.ComponentBlobs[match].CanonicalBehavior
	if err := strictjson.RejectDuplicateKeys(behavior, "record-view AppRelease component"); err != nil {
		return recordViewReleaseDocument{}, fmt.Errorf("decode record-view AppRelease component: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(behavior))
	decoder.DisallowUnknownFields()
	var document recordViewReleaseDocument
	if err := decoder.Decode(&document); err != nil {
		return recordViewReleaseDocument{}, fmt.Errorf("decode record-view AppRelease component: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			err = fmt.Errorf("trailing JSON value")
		}
		return recordViewReleaseDocument{}, fmt.Errorf("decode record-view AppRelease component: %w", err)
	}
	if document.Resolvers == nil || document.Fields == nil {
		return recordViewReleaseDocument{}, fmt.Errorf("record-view AppRelease component must declare resolver and field arrays")
	}
	if err := validateRecordViewReleaseDocument(document); err != nil {
		return recordViewReleaseDocument{}, err
	}
	return document, nil
}

// validateRecordViewReleaseDocument rejects a component whose closed resolver
// vocabulary cannot become a safe Runtime input. It deliberately validates
// only the AppRelease-owned matcher shape here; CatalogSnapshot remains the
// sole owner of object/field closure and canonicalization.
func validateRecordViewReleaseDocument(document recordViewReleaseDocument) error {
	for index, resolver := range document.Resolvers {
		path := fmt.Sprintf("record-view resolver %d", index)
		if resolver.Kind != string(catalogsnapshot.RecordAttachmentSourceByRecordV2) {
			return fmt.Errorf("%s has unsupported kind %q", path, resolver.Kind)
		}
		matcher := resolver.SourceRecordMatcher
		if matcher == nil {
			return fmt.Errorf("%s requires a sourceRecordMatcher", path)
		}
		switch catalogsnapshot.RecordAttachmentSourceRecordMatcherKind(matcher.Kind) {
		case catalogsnapshot.RecordAttachmentSourceMatchRecordIDV1:
			if matcher.StartOffsetBytes != nil || matcher.LengthBytes != 0 {
				return fmt.Errorf("%s exact matcher must not declare a segment", path)
			}
		case catalogsnapshot.RecordAttachmentSourceMatchRecordIDSegmentV1:
			// Existing AppRelease artifacts can carry the original length-only
			// matcher. Keep them reconstructible for release lifecycle work; the
			// RuntimeMetadataView deliberately grants them no serving authority.
			if matcher.StartOffsetBytes != nil || matcher.LengthBytes == 0 || matcher.LengthBytes > commonv1.MaxRecordIdentifierBytes {
				return fmt.Errorf("%s historical segment matcher length must be between 1 and %d bytes without a start offset", path, commonv1.MaxRecordIdentifierBytes)
			}
		case catalogsnapshot.RecordAttachmentSourceMatchRecordIDSegmentV2:
			if matcher.StartOffsetBytes == nil {
				return fmt.Errorf("%s segment matcher requires a start offset", path)
			}
			if matcher.LengthBytes == 0 || int(*matcher.StartOffsetBytes)+int(matcher.LengthBytes) > commonv1.MaxRecordIdentifierBytes {
				return fmt.Errorf("%s segment matcher must remain within the %d-byte record identifier contract", path, commonv1.MaxRecordIdentifierBytes)
			}
		default:
			return fmt.Errorf("%s has unsupported sourceRecordMatcher kind %q", path, matcher.Kind)
		}
	}
	return nil
}

// validateRecordViewServingDocument separates historical release readability
// from a new serving activation. A v1 segment has no static start offset, so
// Runtime cannot derive its source-record predicate without product-specific
// behavior. Catalog may still read and compile the old release for lifecycle
// reconstruction, but Reconcile must not activate a record-view capability
// that would silently omit its declared attachment-source behavior.
func validateRecordViewServingDocument(document recordViewReleaseDocument) error {
	for index, resolver := range document.Resolvers {
		matcher := resolver.SourceRecordMatcher
		if matcher != nil && catalogsnapshot.RecordAttachmentSourceRecordMatcherKind(matcher.Kind) == catalogsnapshot.RecordAttachmentSourceMatchRecordIDSegmentV1 {
			return fmt.Errorf("record-view resolver %d uses historical %q without the required start offset", index, matcher.Kind)
		}
	}
	return nil
}
