package catalogreconcile

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"regexp"
	"slices"
	"strings"
	"unicode"

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

const StoppedSourceConfigSchemaV1 = "nexuscrm.catalog-reconcile-stopped-source-config/v1"

var stoppedSourceGitCommit = regexp.MustCompile(`^[0-9a-f]{40}$`)

// StoppedSourceConfig is the explicit Test-only wrapper around the ordinary v4
// reconcile contract. SourceRelease is an input carrier and admission receipt;
// Reconcile remains the single lifecycle/serving-selection contract.
type StoppedSourceConfig struct {
	Schema        string                     `json:"schema"`
	SourceRelease StoppedSourceReleaseConfig `json:"sourceRelease"`
	Reconcile     Config                     `json:"reconcile"`
}

// StoppedSourceReleaseConfig binds one canonical AppPackage carrier to exact
// caller-owned Git coordinates. It contains no publisher trust, attestation,
// signature, provenance, or promotion authority.
type StoppedSourceReleaseConfig struct {
	PackagePath                  string `json:"packagePath"`
	AppID                        string `json:"appId"`
	ExpectedAppReleaseDigest     string `json:"expectedAppReleaseDigest"`
	ExpectedPackageContentDigest string `json:"expectedPackageContentDigest"`
	SourceRepository             string `json:"sourceRepository"`
	SourceCommit                 string `json:"sourceCommit"`
}

type stoppedSourceConfigEnvelope struct {
	Schema        string                     `json:"schema"`
	SourceRelease StoppedSourceReleaseConfig `json:"sourceRelease"`
	Reconcile     json.RawMessage            `json:"reconcile"`
}

// ParseStoppedSourceConfig accepts only the stopped-source wrapper and an
// ordinary v4 reconcile object with releaseImport absent. The default config
// parser deliberately does not recognize this schema.
func ParseStoppedSourceConfig(raw []byte) (StoppedSourceConfig, error) {
	if err := strictjson.RejectDuplicateKeys(raw, "Catalog stopped-source reconcile config"); err != nil {
		return StoppedSourceConfig{}, fmt.Errorf("decode Catalog stopped-source reconcile config: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var envelope stoppedSourceConfigEnvelope
	if err := decoder.Decode(&envelope); err != nil {
		return StoppedSourceConfig{}, fmt.Errorf("decode Catalog stopped-source reconcile config: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		return StoppedSourceConfig{}, fmt.Errorf("Catalog stopped-source reconcile config contains trailing content")
	}
	if envelope.Schema != StoppedSourceConfigSchemaV1 {
		return StoppedSourceConfig{}, fmt.Errorf(
			"unsupported Catalog stopped-source reconcile config schema %q",
			envelope.Schema,
		)
	}
	if len(envelope.Reconcile) == 0 || bytes.Equal(envelope.Reconcile, []byte("null")) {
		return StoppedSourceConfig{}, fmt.Errorf("Catalog stopped-source reconcile config reconcile is required")
	}
	var reconcileFields map[string]json.RawMessage
	if err := json.Unmarshal(envelope.Reconcile, &reconcileFields); err != nil {
		return StoppedSourceConfig{}, fmt.Errorf("decode nested Catalog reconcile config: %w", err)
	}
	if _, declared := reconcileFields["releaseImport"]; declared {
		return StoppedSourceConfig{}, fmt.Errorf("Catalog stopped-source reconcile nested config cannot declare releaseImport")
	}
	reconcile, err := ParseConfig(envelope.Reconcile)
	if err != nil {
		return StoppedSourceConfig{}, fmt.Errorf("decode nested Catalog reconcile config: %w", err)
	}
	if reconcile.Schema != ConfigSchemaV4 {
		return StoppedSourceConfig{}, fmt.Errorf("Catalog stopped-source reconcile requires nested config schema v4")
	}
	if err := validateStoppedSourceReleaseConfig(envelope.SourceRelease); err != nil {
		return StoppedSourceConfig{}, err
	}
	if envelope.SourceRelease.AppID != reconcile.AppID ||
		envelope.SourceRelease.ExpectedAppReleaseDigest != reconcile.ExpectedAppReleaseDigest {
		return StoppedSourceConfig{}, fmt.Errorf(
			"Catalog stopped-source release identity must exactly match the nested reconcile identity",
		)
	}
	return StoppedSourceConfig{
		Schema: envelope.Schema, SourceRelease: envelope.SourceRelease, Reconcile: reconcile,
	}, nil
}

func validateStoppedSourceReleaseConfig(config StoppedSourceReleaseConfig) error {
	if strings.TrimSpace(config.PackagePath) == "" || config.PackagePath != strings.TrimSpace(config.PackagePath) {
		return fmt.Errorf("Catalog stopped-source packagePath must be canonical and non-empty")
	}
	if err := appauthority.ValidateAppID(config.AppID); err != nil {
		return fmt.Errorf("Catalog stopped-source AppID: %w", err)
	}
	if err := appauthority.ValidateDigest(config.ExpectedAppReleaseDigest); err != nil {
		return fmt.Errorf("Catalog stopped-source AppRelease digest: %w", err)
	}
	if err := appauthority.ValidateDigest(config.ExpectedPackageContentDigest); err != nil {
		return fmt.Errorf("Catalog stopped-source package content digest: %w", err)
	}
	if strings.TrimSpace(config.SourceRepository) == "" ||
		config.SourceRepository != strings.TrimSpace(config.SourceRepository) ||
		len(config.SourceRepository) > 1024 ||
		strings.IndexFunc(config.SourceRepository, func(value rune) bool {
			return unicode.IsControl(value) || unicode.IsSpace(value)
		}) >= 0 {
		return fmt.Errorf("Catalog stopped-source sourceRepository is not canonical")
	}
	if !stoppedSourceGitCommit.MatchString(config.SourceCommit) {
		return fmt.Errorf("Catalog stopped-source sourceCommit must be a lowercase 40-character Git object ID")
	}
	return nil
}

// ReleaseInput freezes the parsed source identity around the package bytes
// read by the command. The path itself is intentionally not persisted.
func (c StoppedSourceConfig) ReleaseInput(packageBytes []byte) (catalogapp.StoppedSourceReleaseInput, error) {
	if c.Schema != StoppedSourceConfigSchemaV1 || c.Reconcile.Schema != ConfigSchemaV4 ||
		c.Reconcile.ReleaseImport != nil {
		return catalogapp.StoppedSourceReleaseInput{}, fmt.Errorf("Catalog stopped-source config is not a closed v1/v4 contract")
	}
	if err := validateStoppedSourceReleaseConfig(c.SourceRelease); err != nil {
		return catalogapp.StoppedSourceReleaseInput{}, err
	}
	if c.SourceRelease.AppID != c.Reconcile.AppID ||
		c.SourceRelease.ExpectedAppReleaseDigest != c.Reconcile.ExpectedAppReleaseDigest {
		return catalogapp.StoppedSourceReleaseInput{}, fmt.Errorf(
			"Catalog stopped-source release identity must exactly match the nested reconcile identity",
		)
	}
	return catalogapp.StoppedSourceReleaseInput{
		AppID:                        c.SourceRelease.AppID,
		ExpectedAppReleaseDigest:     c.SourceRelease.ExpectedAppReleaseDigest,
		ExpectedPackageContentDigest: c.SourceRelease.ExpectedPackageContentDigest,
		SourceRepository:             c.SourceRelease.SourceRepository,
		SourceCommit:                 c.SourceRelease.SourceCommit,
		PackageBytes:                 bytes.Clone(packageBytes),
		Compatibility:                cloneHostCompatibility(c.Reconcile.Compatibility()),
	}, nil
}

// StoppedSourceReleaseResolver is the minimum Catalog owner surface needed to
// resolve the exact branch package. Import is allowed only after ReadRelease
// returns ErrNotFound.
type StoppedSourceReleaseResolver interface {
	ReadRelease(context.Context, string, string) (catalogapp.RegisteredRelease, error)
	ReadStoppedSourceRelease(context.Context, catalogapp.StoppedSourceReleaseReference) (catalogapp.RegisteredRelease, error)
	ImportStoppedSourceRelease(context.Context, catalogapp.StoppedSourceReleaseInput) (catalogapp.RegisteredRelease, catalogapp.StoppedSourceReleaseReference, error)
}

// StoppedSourceReleaseResolution preserves whether the exact package was
// already admitted by a signed publisher or by the stopped-source path. The
// admission kind selects only the operation entrypoint; all later reconcile
// behavior is shared.
type StoppedSourceReleaseResolution struct {
	Release   catalogapp.RegisteredRelease
	Reference catalogapp.StoppedSourceReleaseReference
}

// ResolveStoppedSourceRelease verifies the supplied canonical package before
// reading Catalog. An existing signed release is reused only when its rebuilt
// canonical carrier is byte-identical. An existing stopped-source release must
// match the exact source receipt. No error other than ErrNotFound can trigger
// import.
func ResolveStoppedSourceRelease(
	ctx context.Context,
	authority StoppedSourceReleaseResolver,
	input catalogapp.StoppedSourceReleaseInput,
) (StoppedSourceReleaseResolution, error) {
	if authority == nil {
		return StoppedSourceReleaseResolution{}, fmt.Errorf("Catalog stopped-source release authority is required")
	}
	input.PackageBytes = bytes.Clone(input.PackageBytes)
	input.Compatibility = cloneHostCompatibility(input.Compatibility)
	candidate, reference, err := catalogapp.VerifyStoppedSourceReleaseCandidate(input)
	if err != nil {
		return StoppedSourceReleaseResolution{}, fmt.Errorf("verify stopped-source AppRelease candidate: %w", err)
	}

	release, err := authority.ReadRelease(ctx, input.AppID, input.ExpectedAppReleaseDigest)
	if err == nil {
		switch release.AdmissionKind {
		case catalogapp.ReleaseAdmissionSignedPublisher:
			if err := validateExactStoppedSourceCandidate(release, candidate, input.PackageBytes, input.Compatibility); err != nil {
				return StoppedSourceReleaseResolution{}, err
			}
			return StoppedSourceReleaseResolution{Release: release, Reference: reference}, nil
		case catalogapp.ReleaseAdmissionStoppedSource:
			release, err = authority.ReadStoppedSourceRelease(ctx, reference)
			if err != nil {
				return StoppedSourceReleaseResolution{}, fmt.Errorf("read exact stopped-source AppRelease: %w", err)
			}
			if err := validateExactStoppedSourceCandidate(release, candidate, input.PackageBytes, input.Compatibility); err != nil {
				return StoppedSourceReleaseResolution{}, err
			}
			return StoppedSourceReleaseResolution{Release: release, Reference: reference}, nil
		default:
			return StoppedSourceReleaseResolution{}, fmt.Errorf(
				"%w: AppRelease has unsupported admission kind %q",
				catalogapp.ErrCorrupt,
				release.AdmissionKind,
			)
		}
	}
	if !errors.Is(err, catalogapp.ErrNotFound) {
		return StoppedSourceReleaseResolution{}, fmt.Errorf("read registered AppRelease: %w", err)
	}
	if err := ctx.Err(); err != nil {
		return StoppedSourceReleaseResolution{}, err
	}
	release, importedReference, err := authority.ImportStoppedSourceRelease(ctx, input)
	if err != nil {
		return StoppedSourceReleaseResolution{}, fmt.Errorf("import missing stopped-source AppRelease: %w", err)
	}
	if importedReference != reference {
		return StoppedSourceReleaseResolution{}, fmt.Errorf(
			"%w: imported stopped-source receipt does not match the verified source candidate",
			catalogapp.ErrConflict,
		)
	}
	if err := validateExactStoppedSourceCandidate(release, candidate, input.PackageBytes, input.Compatibility); err != nil {
		return StoppedSourceReleaseResolution{}, fmt.Errorf("imported stopped-source AppRelease is invalid: %w", err)
	}
	if release.AdmissionKind != catalogapp.ReleaseAdmissionStoppedSource {
		return StoppedSourceReleaseResolution{}, fmt.Errorf(
			"%w: imported AppRelease is not a stopped-source admission",
			catalogapp.ErrConflict,
		)
	}
	return StoppedSourceReleaseResolution{Release: release, Reference: reference}, nil
}

func validateExactStoppedSourceCandidate(
	release catalogapp.RegisteredRelease,
	candidate catalogapp.VerifiedStoppedSourceReleaseCandidate,
	packageBytes []byte,
	compatibility releaseartifact.HostCompatibility,
) error {
	if release.AppID != candidate.AppID || release.PublisherID != candidate.PublisherID ||
		release.SemanticVersion != candidate.SemanticVersion ||
		release.AppReleaseDigest != candidate.AppReleaseDigest ||
		release.MetadataBundleDigest != candidate.MetadataBundleDigest {
		return fmt.Errorf("%w: Catalog AppRelease does not match the exact stopped-source candidate", catalogapp.ErrConflict)
	}
	if err := releaseartifact.VerifyCompatibility(release.Package, compatibility); err != nil {
		return fmt.Errorf("registered AppRelease is incompatible with this host: %w", err)
	}
	_, storedBytes, err := releaseartifact.BuildPackage(release.Package.Package)
	if err != nil {
		return fmt.Errorf("%w: rebuild registered AppPackage: %v", catalogapp.ErrCorrupt, err)
	}
	if !bytes.Equal(storedBytes, packageBytes) {
		return fmt.Errorf("%w: registered AppPackage bytes do not match the exact source carrier", catalogapp.ErrConflict)
	}
	return nil
}

// StoppedSourceReconcileAuthority extends the existing reconcile owner only
// with the exact source read and operation entrypoint. It does not expose a
// second serving lifecycle.
type StoppedSourceReconcileAuthority interface {
	Authority
	ReadStoppedSourceRelease(context.Context, catalogapp.StoppedSourceReleaseReference) (catalogapp.RegisteredRelease, error)
	BeginStoppedSourceOperation(context.Context, catalogapp.BeginOperationRequest, catalogapp.StoppedSourceReleaseReference) (catalogapp.Operation, error)
}

type stoppedSourceOperationAdapter struct {
	StoppedSourceReconcileAuthority
	resolution StoppedSourceReleaseResolution
}

func (a stoppedSourceOperationAdapter) ReadRelease(
	ctx context.Context,
	appID, digest string,
) (catalogapp.RegisteredRelease, error) {
	if appID != a.resolution.Release.AppID || digest != a.resolution.Release.AppReleaseDigest {
		return catalogapp.RegisteredRelease{}, catalogapp.ErrConflict
	}
	var (
		release catalogapp.RegisteredRelease
		err     error
	)
	switch a.resolution.Release.AdmissionKind {
	case catalogapp.ReleaseAdmissionSignedPublisher:
		release, err = a.StoppedSourceReconcileAuthority.ReadRelease(ctx, appID, digest)
	case catalogapp.ReleaseAdmissionStoppedSource:
		release, err = a.StoppedSourceReconcileAuthority.ReadStoppedSourceRelease(ctx, a.resolution.Reference)
	default:
		return catalogapp.RegisteredRelease{}, fmt.Errorf(
			"%w: AppRelease has unsupported admission kind %q",
			catalogapp.ErrCorrupt,
			a.resolution.Release.AdmissionKind,
		)
	}
	if err != nil {
		return catalogapp.RegisteredRelease{}, err
	}
	if !sameResolvedRelease(release, a.resolution.Release) {
		return catalogapp.RegisteredRelease{}, fmt.Errorf(
			"%w: AppRelease changed after stopped-source resolution",
			catalogapp.ErrConflict,
		)
	}
	return release, nil
}

func (a stoppedSourceOperationAdapter) BeginOperation(
	ctx context.Context,
	request catalogapp.BeginOperationRequest,
) (catalogapp.Operation, error) {
	switch a.resolution.Release.AdmissionKind {
	case catalogapp.ReleaseAdmissionSignedPublisher:
		return a.StoppedSourceReconcileAuthority.BeginOperation(ctx, request)
	case catalogapp.ReleaseAdmissionStoppedSource:
		return a.StoppedSourceReconcileAuthority.BeginStoppedSourceOperation(
			ctx,
			request,
			a.resolution.Reference,
		)
	default:
		return catalogapp.Operation{}, fmt.Errorf(
			"%w: AppRelease has unsupported admission kind %q",
			catalogapp.ErrCorrupt,
			a.resolution.Release.AdmissionKind,
		)
	}
}

func sameResolvedRelease(left, right catalogapp.RegisteredRelease) bool {
	if left.AppID != right.AppID || left.PublisherID != right.PublisherID ||
		left.SemanticVersion != right.SemanticVersion ||
		left.AppReleaseDigest != right.AppReleaseDigest ||
		left.MetadataBundleDigest != right.MetadataBundleDigest ||
		left.AdmissionKind != right.AdmissionKind {
		return false
	}
	_, leftBytes, leftErr := releaseartifact.BuildPackage(left.Package.Package)
	_, rightBytes, rightErr := releaseartifact.BuildPackage(right.Package.Package)
	return leftErr == nil && rightErr == nil && bytes.Equal(leftBytes, rightBytes)
}

// ReconcileStoppedSource selects only the release-read and operation-entry
// adapter, then delegates the complete serving lifecycle to Reconcile.
func ReconcileStoppedSource(
	ctx context.Context,
	authority StoppedSourceReconcileAuthority,
	writer catalogapp.RuntimeProjectionWriter,
	store catalogsnapshot.Store,
	withinTransaction InTransaction,
	reader MetadataReader,
	request Request,
	resolution StoppedSourceReleaseResolution,
) (Output, error) {
	if authority == nil {
		return Output{}, fmt.Errorf("Catalog stopped-source reconcile authority is required")
	}
	if resolution.Release.AdmissionKind != catalogapp.ReleaseAdmissionSignedPublisher &&
		resolution.Release.AdmissionKind != catalogapp.ReleaseAdmissionStoppedSource {
		return Output{}, fmt.Errorf(
			"%w: AppRelease has unsupported admission kind %q",
			catalogapp.ErrCorrupt,
			resolution.Release.AdmissionKind,
		)
	}
	adapter := stoppedSourceOperationAdapter{
		StoppedSourceReconcileAuthority: authority,
		resolution:                      resolution,
	}
	return Reconcile(ctx, adapter, writer, store, withinTransaction, reader, request)
}

func cloneHostCompatibility(input releaseartifact.HostCompatibility) releaseartifact.HostCompatibility {
	return releaseartifact.HostCompatibility{
		Protocols:          slices.Clone(input.Protocols),
		EngineCapabilities: slices.Clone(input.EngineCapabilities),
		ComponentKinds:     slices.Clone(input.ComponentKinds),
	}
}
