package setupcmd

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"github.com/nexuscrm/automation"
	"github.com/nexuscrm/backend/pkg/serverruntime"
	database "github.com/nexuscrm/db"
)

const (
	connectorRemoteIdentityRecoveryCommand       = "recover-connector-remote-identity-batch"
	connectorRemoteIdentityRecoverySchemaVersion = 1
	connectorRemoteIdentityRecoveryMaxInputBytes = 4 << 20

	connectorRemoteIdentityRecoveryBlockManifestInvalid        = "MANIFEST_INVALID"
	connectorRemoteIdentityRecoveryBlockDryRunReceiptInvalid   = "DRY_RUN_RECEIPT_INVALID"
	connectorRemoteIdentityRecoveryBlockRuntimeUnavailable     = "RUNTIME_UNAVAILABLE"
	connectorRemoteIdentityRecoveryBlockRecoveryRolledBack     = "RECOVERY_ROLLED_BACK"
	connectorRemoteIdentityRecoveryBlockCommittedReadback      = "COMMITTED_READBACK_BLOCKED"
	connectorRemoteIdentityRecoveryBlockRuntimeContractInvalid = "RUNTIME_RESULT_INVALID"
)

type connectorRemoteIdentityRecoveryManifest struct {
	SchemaVersion     int                                           `json:"schema_version"`
	Operation         string                                        `json:"operation,omitempty"`
	Issue             json.RawMessage                               `json:"issue,omitempty"`
	Environment       string                                        `json:"environment,omitempty"`
	ApprovalRef       string                                        `json:"approval_ref,omitempty"`
	DeployedTuple     connectorRemoteIdentityRecoveryDeployedTuple  `json:"deployed_tuple"`
	RuntimeBinding    connectorRemoteIdentityRecoveryRuntimeBinding `json:"runtime_binding"`
	Selector          connectorRemoteIdentityRecoverySelector       `json:"selector"`
	Census            json.RawMessage                               `json:"census,omitempty"`
	ExcludedSetSHA256 string                                        `json:"excluded_set_sha256,omitempty"`
	Candidates        []connectorRemoteIdentityRecoveryCandidate    `json:"candidates"`
}

type connectorRemoteIdentityRecoveryDeployedTuple struct {
	DeploymentID           string `json:"deployment_id"`
	ReleaseTag             string `json:"release_tag"`
	DownstreamCommit       string `json:"downstream_commit"`
	UpstreamCommit         string `json:"upstream_commit"`
	OfficialAppPacksCommit string `json:"official_app_packs_commit"`
	FinalPackHash          string `json:"final_pack_hash"`
}

type connectorRemoteIdentityRecoveryRuntimeBinding struct {
	ReleaseJSONSHA256 string `json:"release_json_sha256"`
	RuntimeEnvSHA256  string `json:"runtime_env_sha256"`
	SetupBinarySHA256 string `json:"setup_binary_sha256"`
}

type connectorRemoteIdentityRecoveryRuntimeProof struct {
	RuntimeBindingSHA256 string
	ReleaseJSONSHA256    string
	RuntimeEnvSHA256     string
	SetupBinarySHA256    string
	DeployedTupleSHA256  string
	cutoverLock          *os.File
}

type connectorRemoteIdentityRecoverySelector struct {
	ConnectorOwnerKey  string `json:"connector_owner_key"`
	ConnectorNamespace string `json:"connector_namespace"`
	DestinationSlotID  string `json:"destination_slot_id"`
}

type connectorRemoteIdentityRecoveryCandidate struct {
	RemoteID               string `json:"remote_id"`
	ExpectedSourceObject   string `json:"expected_source_object"`
	ExpectedSourceRecordID string `json:"expected_source_record_id"`
}

type connectorRemoteIdentityRecoveryReceipt struct {
	SchemaVersion         int                                          `json:"schema_version"`
	Command               string                                       `json:"command"`
	Mode                  string                                       `json:"mode"`
	ManifestSHA256        string                                       `json:"manifest_sha256"`
	RuntimeBindingSHA256  string                                       `json:"runtime_binding_sha256"`
	ReleaseJSONSHA256     string                                       `json:"release_json_sha256"`
	RuntimeEnvSHA256      string                                       `json:"runtime_env_sha256"`
	SetupBinarySHA256     string                                       `json:"setup_binary_sha256"`
	DeployedTupleSHA256   string                                       `json:"deployed_tuple_sha256"`
	DryRunReceiptSHA256   string                                       `json:"dry_run_receipt_sha256,omitempty"`
	BackupMarker          string                                       `json:"backup_marker,omitempty"`
	Outcome               string                                       `json:"outcome"`
	Committed             bool                                         `json:"committed"`
	ReadbackVerified      bool                                         `json:"readback_verified"`
	EnrollmentAllowed     bool                                         `json:"enrollment_allowed"`
	ResultContractValid   bool                                         `json:"result_contract_valid"`
	RequestedCount        *int                                         `json:"requested_count"`
	RecoveredCount        *int                                         `json:"recovered_count"`
	FinalizedLinkCount    *int                                         `json:"finalized_link_count"`
	ConsumedEvidenceCount *int                                         `json:"consumed_evidence_count"`
	ResolverMatchCount    *int                                         `json:"resolver_match_count"`
	Items                 []connectorRemoteIdentityRecoveryReceiptItem `json:"items"`
	BlockReason           string                                       `json:"block_reason,omitempty"`
}

type connectorRemoteIdentityRecoveryReceiptItem struct {
	RemoteID       string `json:"remote_id"`
	SourceObject   string `json:"source_object"`
	SourceRecordID string `json:"source_record_id"`
}

type connectorRemoteIdentityRecoveryArgs struct {
	manifestPath        string
	manifestSHA256      string
	releaseJSONPath     string
	releaseJSONSHA256   string
	runtimeEnvPath      string
	runtimeEnvSHA256    string
	setupBinarySHA256   string
	apply               bool
	dryRunReceiptPath   string
	dryRunReceiptSHA256 string
	backupMarker        string
}

type connectorRemoteIdentityRecoveryBatchRunner interface {
	RecoverBatch(context.Context, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest) (automation.ConnectorInboundRemoteIdentityRecoveryBatchResult, error)
	DryRunBatch(context.Context, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest) (automation.ConnectorInboundRemoteIdentityRecoveryBatchResult, error)
}

type connectorRemoteIdentityRecoveryRunnerBuilder func(
	*database.TiDBConnection,
	serverruntime.ProductHooks,
) (connectorRemoteIdentityRecoveryBatchRunner, error)

type connectorRemoteIdentityRecoveryRuntimeVerifier func(
	context.Context,
	*database.TiDBConnection,
	serverruntime.ProductHooks,
	connectorRemoteIdentityRecoveryArgs,
	connectorRemoteIdentityRecoveryManifest,
) (connectorRemoteIdentityRecoveryRuntimeProof, error)

func buildConnectorRemoteIdentityRecoveryRunner(
	conn *database.TiDBConnection,
	hooks serverruntime.ProductHooks,
) (connectorRemoteIdentityRecoveryBatchRunner, error) {
	return serverruntime.BuildConnectorInboundRemoteIdentityRecoveryMaintenance(conn, hooks)
}

func runConnectorRemoteIdentityRecoveryCommand(
	ctx context.Context,
	conn *database.TiDBConnection,
	hooks serverruntime.ProductHooks,
	args []string,
	stdout io.Writer,
	stderr io.Writer,
	verifyRuntime connectorRemoteIdentityRecoveryRuntimeVerifier,
	build connectorRemoteIdentityRecoveryRunnerBuilder,
) int {
	parsed, err := parseConnectorRemoteIdentityRecoveryArgs(args, stderr)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 2
	}
	manifestBytes, err := readConnectorRemoteIdentityRecoveryFile(parsed.manifestPath)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 2
	}
	manifestSHA, err := verifyConnectorRemoteIdentityRecoverySHA256("manifest", manifestBytes, parsed.manifestSHA256)
	if err != nil {
		fmt.Fprintln(stderr, err)
		return 2
	}
	manifest, request, err := decodeConnectorRemoteIdentityRecoveryManifest(manifestBytes)
	if err != nil {
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, connectorRemoteIdentityRecoveryRuntimeProof{}, manifest, 0, connectorRemoteIdentityRecoveryBlockManifestInvalid)
		fmt.Fprintln(stderr, err)
		return 1
	}
	if err := verifyConnectorRemoteIdentityRecoveryManifestBindingArgs(parsed, manifest); err != nil {
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, connectorRemoteIdentityRecoveryRuntimeProof{}, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockManifestInvalid)
		fmt.Fprintln(stderr, err)
		return 1
	}
	if verifyRuntime == nil {
		err := fmt.Errorf("%s: live runtime verifier is not wired", connectorRemoteIdentityRecoveryCommand)
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, connectorRemoteIdentityRecoveryRuntimeProof{}, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockRuntimeUnavailable)
		fmt.Fprintln(stderr, err)
		return 1
	}
	runtimeProof, err := verifyRuntime(ctx, conn, hooks, parsed, manifest)
	if runtimeProof.cutoverLock != nil {
		defer releaseConnectorRemoteIdentityRecoveryCutoverLock(runtimeProof.cutoverLock)
	}
	if err != nil {
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, runtimeProof, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockRuntimeUnavailable)
		fmt.Fprintln(stderr, err)
		return 1
	}

	if parsed.apply {
		if err := verifyConnectorRemoteIdentityRecoveryDryRunReceipt(parsed, manifestSHA, runtimeProof, manifest); err != nil {
			writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, runtimeProof, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockDryRunReceiptInvalid)
			fmt.Fprintln(stderr, err)
			return 1
		}
	}
	if build == nil {
		err := fmt.Errorf("%s: maintenance runner builder is not wired", connectorRemoteIdentityRecoveryCommand)
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, runtimeProof, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockRuntimeUnavailable)
		fmt.Fprintln(stderr, err)
		return 1
	}
	runner, err := build(conn, hooks)
	if err != nil {
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, runtimeProof, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockRuntimeUnavailable)
		fmt.Fprintln(stderr, err)
		return 1
	}
	if runner == nil {
		err := fmt.Errorf("%s: maintenance runner is not wired", connectorRemoteIdentityRecoveryCommand)
		writeConnectorRemoteIdentityRecoveryBlockedReceipt(stdout, parsed, manifestSHA, runtimeProof, manifest, len(manifest.Candidates), connectorRemoteIdentityRecoveryBlockRuntimeUnavailable)
		fmt.Fprintln(stderr, err)
		return 1
	}

	var result automation.ConnectorInboundRemoteIdentityRecoveryBatchResult
	if parsed.apply {
		result, err = runner.RecoverBatch(ctx, request)
	} else {
		result, err = runner.DryRunBatch(ctx, request)
	}
	result, resultContractValid, err := validateConnectorRemoteIdentityRecoveryRuntimeResult(parsed, manifest, result, err)
	receipt := connectorRemoteIdentityRecoveryReceiptFromResult(parsed, manifestSHA, runtimeProof, manifest, result, resultContractValid)
	if encodeErr := writeConnectorRemoteIdentityRecoveryReceipt(stdout, receipt); encodeErr != nil {
		fmt.Fprintf(stderr, "%s: write receipt: %v\n", connectorRemoteIdentityRecoveryCommand, encodeErr)
		return 1
	}
	if err != nil {
		fmt.Fprintf(stderr, "%s: %v\n", connectorRemoteIdentityRecoveryCommand, err)
		return 1
	}
	return 0
}

func parseConnectorRemoteIdentityRecoveryArgs(args []string, stderr io.Writer) (connectorRemoteIdentityRecoveryArgs, error) {
	fs := flag.NewFlagSet(connectorRemoteIdentityRecoveryCommand, flag.ContinueOnError)
	fs.SetOutput(stderr)
	manifestPath := fs.String("manifest", "", "immutable recovery manifest JSON path")
	manifestSHA256 := fs.String("manifest-sha256", "", "expected lowercase SHA-256 of the exact manifest bytes")
	releaseJSONPath := fs.String("release-json", "", "inherited descriptor path for the active release.json")
	releaseJSONSHA256 := fs.String("release-json-sha256", "", "expected lowercase SHA-256 of the exact active release.json bytes")
	runtimeEnvPath := fs.String("runtime-env", "", "inherited descriptor path for the active runtime.env")
	runtimeEnvSHA256 := fs.String("runtime-env-sha256", "", "expected lowercase SHA-256 of the exact active runtime.env bytes")
	setupBinarySHA256 := fs.String("setup-binary-sha256", "", "expected lowercase SHA-256 of the executing setup binary")
	apply := fs.Bool("apply", false, "commit the batch after validating the matching dry-run receipt and backup marker")
	dryRunReceiptPath := fs.String("dry-run-receipt", "", "matching dry-run receipt JSON path (required with --apply)")
	dryRunReceiptSHA256 := fs.String("dry-run-receipt-sha256", "", "expected lowercase SHA-256 of exact dry-run receipt bytes (required with --apply)")
	backupMarker := fs.String("backup-marker", "", "L2-verified backup marker (required with --apply)")
	if err := fs.Parse(args); err != nil {
		return connectorRemoteIdentityRecoveryArgs{}, err
	}
	if fs.NArg() != 0 {
		return connectorRemoteIdentityRecoveryArgs{}, fmt.Errorf("%s: unexpected arguments: %s", connectorRemoteIdentityRecoveryCommand, strings.Join(fs.Args(), " "))
	}
	parsed := connectorRemoteIdentityRecoveryArgs{
		manifestPath:        strings.TrimSpace(*manifestPath),
		manifestSHA256:      strings.TrimSpace(*manifestSHA256),
		releaseJSONPath:     strings.TrimSpace(*releaseJSONPath),
		releaseJSONSHA256:   strings.TrimSpace(*releaseJSONSHA256),
		runtimeEnvPath:      strings.TrimSpace(*runtimeEnvPath),
		runtimeEnvSHA256:    strings.TrimSpace(*runtimeEnvSHA256),
		setupBinarySHA256:   strings.TrimSpace(*setupBinarySHA256),
		apply:               *apply,
		dryRunReceiptPath:   strings.TrimSpace(*dryRunReceiptPath),
		dryRunReceiptSHA256: strings.TrimSpace(*dryRunReceiptSHA256),
		backupMarker:        strings.TrimSpace(*backupMarker),
	}
	if parsed.manifestPath == "" || parsed.manifestSHA256 == "" || parsed.releaseJSONPath == "" || parsed.releaseJSONSHA256 == "" ||
		parsed.runtimeEnvPath == "" || parsed.runtimeEnvSHA256 == "" || parsed.setupBinarySHA256 == "" {
		return connectorRemoteIdentityRecoveryArgs{}, fmt.Errorf("%s: manifest and live runtime authority flags are required", connectorRemoteIdentityRecoveryCommand)
	}
	for label, value := range map[string]string{
		"manifest": parsed.manifestSHA256, "release.json": parsed.releaseJSONSHA256,
		"runtime.env": parsed.runtimeEnvSHA256, "setup binary": parsed.setupBinarySHA256,
	} {
		if err := validateConnectorRemoteIdentityRecoverySHA256(label, value); err != nil {
			return connectorRemoteIdentityRecoveryArgs{}, err
		}
	}
	if parsed.apply {
		if parsed.dryRunReceiptPath == "" || parsed.dryRunReceiptSHA256 == "" || parsed.backupMarker == "" {
			return connectorRemoteIdentityRecoveryArgs{}, fmt.Errorf("%s: --apply requires --dry-run-receipt, --dry-run-receipt-sha256, and --backup-marker", connectorRemoteIdentityRecoveryCommand)
		}
		if err := validateConnectorRemoteIdentityRecoverySHA256("dry-run receipt", parsed.dryRunReceiptSHA256); err != nil {
			return connectorRemoteIdentityRecoveryArgs{}, err
		}
	} else if parsed.dryRunReceiptPath != "" || parsed.dryRunReceiptSHA256 != "" || parsed.backupMarker != "" {
		return connectorRemoteIdentityRecoveryArgs{}, fmt.Errorf("%s: apply guards are only accepted with --apply", connectorRemoteIdentityRecoveryCommand)
	}
	return parsed, nil
}

func decodeConnectorRemoteIdentityRecoveryManifest(
	data []byte,
) (connectorRemoteIdentityRecoveryManifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest, error) {
	var manifest connectorRemoteIdentityRecoveryManifest
	if err := decodeConnectorRemoteIdentityRecoveryJSON(data, &manifest); err != nil {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: decode manifest: %w", connectorRemoteIdentityRecoveryCommand, err)
	}
	if manifest.SchemaVersion != connectorRemoteIdentityRecoverySchemaVersion {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest schema_version must be %d", connectorRemoteIdentityRecoveryCommand, connectorRemoteIdentityRecoverySchemaVersion)
	}
	manifest.DeployedTuple.DeploymentID = strings.TrimSpace(manifest.DeployedTuple.DeploymentID)
	manifest.DeployedTuple.ReleaseTag = strings.TrimSpace(manifest.DeployedTuple.ReleaseTag)
	manifest.DeployedTuple.DownstreamCommit = strings.TrimSpace(manifest.DeployedTuple.DownstreamCommit)
	manifest.DeployedTuple.UpstreamCommit = strings.TrimSpace(manifest.DeployedTuple.UpstreamCommit)
	manifest.DeployedTuple.OfficialAppPacksCommit = strings.TrimSpace(manifest.DeployedTuple.OfficialAppPacksCommit)
	manifest.DeployedTuple.FinalPackHash = strings.TrimSpace(manifest.DeployedTuple.FinalPackHash)
	if manifest.DeployedTuple.DeploymentID == "" || manifest.DeployedTuple.ReleaseTag == "" ||
		!isConnectorRemoteIdentityRecoveryAuthorityToken(manifest.DeployedTuple.DeploymentID) ||
		!isConnectorRemoteIdentityRecoveryAuthorityToken(manifest.DeployedTuple.ReleaseTag) {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest deployed_tuple deployment_id and release_tag are required ASCII authority tokens", connectorRemoteIdentityRecoveryCommand)
	}
	for label, value := range map[string]string{
		"downstream_commit":         manifest.DeployedTuple.DownstreamCommit,
		"upstream_commit":           manifest.DeployedTuple.UpstreamCommit,
		"official_app_packs_commit": manifest.DeployedTuple.OfficialAppPacksCommit,
	} {
		if !isConnectorRemoteIdentityRecoveryGitSHA(value) {
			return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest deployed_tuple %s must be exactly 40 lowercase hexadecimal characters", connectorRemoteIdentityRecoveryCommand, label)
		}
	}
	if err := validateConnectorRemoteIdentityRecoverySHA256("manifest deployed_tuple final_pack_hash", manifest.DeployedTuple.FinalPackHash); err != nil {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, err
	}
	manifest.RuntimeBinding.ReleaseJSONSHA256 = strings.TrimSpace(manifest.RuntimeBinding.ReleaseJSONSHA256)
	manifest.RuntimeBinding.RuntimeEnvSHA256 = strings.TrimSpace(manifest.RuntimeBinding.RuntimeEnvSHA256)
	manifest.RuntimeBinding.SetupBinarySHA256 = strings.TrimSpace(manifest.RuntimeBinding.SetupBinarySHA256)
	for label, value := range map[string]string{
		"manifest runtime_binding release_json_sha256": manifest.RuntimeBinding.ReleaseJSONSHA256,
		"manifest runtime_binding runtime_env_sha256":  manifest.RuntimeBinding.RuntimeEnvSHA256,
		"manifest runtime_binding setup_binary_sha256": manifest.RuntimeBinding.SetupBinarySHA256,
	} {
		if err := validateConnectorRemoteIdentityRecoverySHA256(label, value); err != nil {
			return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, err
		}
	}
	selector := connectorRemoteIdentityRecoverySelector{
		ConnectorOwnerKey:  strings.TrimSpace(manifest.Selector.ConnectorOwnerKey),
		ConnectorNamespace: strings.TrimSpace(manifest.Selector.ConnectorNamespace),
		DestinationSlotID:  strings.TrimSpace(manifest.Selector.DestinationSlotID),
	}
	if selector.ConnectorOwnerKey == "" || selector.ConnectorNamespace == "" || selector.DestinationSlotID == "" {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest selector connector_owner_key, connector_namespace, and destination_slot_id are required", connectorRemoteIdentityRecoveryCommand)
	}
	if len(manifest.Candidates) == 0 || len(manifest.Candidates) > automation.ConnectorInboundRemoteIdentityRecoveryBatchMaxItems {
		return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest candidate count must be between 1 and %d", connectorRemoteIdentityRecoveryCommand, automation.ConnectorInboundRemoteIdentityRecoveryBatchMaxItems)
	}

	request := automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{
		Selector: automation.ConnectorInboundRemoteIdentityRecoveryBatchSelector{
			ConnectorOwnerKey:  selector.ConnectorOwnerKey,
			ConnectorNamespace: selector.ConnectorNamespace,
			DestinationSlotID:  selector.DestinationSlotID,
		},
		Items: make([]automation.ConnectorInboundRemoteIdentityRecoveryBatchItem, 0, len(manifest.Candidates)),
	}
	seenRemoteIDs := make(map[string]struct{}, len(manifest.Candidates))
	for i := range manifest.Candidates {
		candidate := manifest.Candidates[i]
		candidate.RemoteID = strings.TrimSpace(candidate.RemoteID)
		candidate.ExpectedSourceObject = strings.TrimSpace(candidate.ExpectedSourceObject)
		candidate.ExpectedSourceRecordID = strings.TrimSpace(candidate.ExpectedSourceRecordID)
		if candidate.RemoteID == "" || candidate.ExpectedSourceObject == "" || candidate.ExpectedSourceRecordID == "" {
			return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest candidate %d requires remote_id and expected source pair", connectorRemoteIdentityRecoveryCommand, i)
		}
		if _, duplicate := seenRemoteIDs[candidate.RemoteID]; duplicate {
			return manifest, automation.ConnectorInboundRemoteIdentityRecoveryBatchRequest{}, fmt.Errorf("%s: manifest has duplicate remote_id %q", connectorRemoteIdentityRecoveryCommand, candidate.RemoteID)
		}
		seenRemoteIDs[candidate.RemoteID] = struct{}{}
		manifest.Candidates[i] = candidate
		request.Items = append(request.Items, automation.ConnectorInboundRemoteIdentityRecoveryBatchItem{
			RemoteID: candidate.RemoteID, ExpectedSourceObject: candidate.ExpectedSourceObject, ExpectedSourceRecordID: candidate.ExpectedSourceRecordID,
		})
	}
	manifest.Selector = selector
	return manifest, request, nil
}

func verifyConnectorRemoteIdentityRecoveryManifestBindingArgs(
	args connectorRemoteIdentityRecoveryArgs,
	manifest connectorRemoteIdentityRecoveryManifest,
) error {
	if args.releaseJSONSHA256 != manifest.RuntimeBinding.ReleaseJSONSHA256 ||
		args.runtimeEnvSHA256 != manifest.RuntimeBinding.RuntimeEnvSHA256 ||
		args.setupBinarySHA256 != manifest.RuntimeBinding.SetupBinarySHA256 {
		return fmt.Errorf("%s: manifest runtime_binding does not match the command authority fingerprints", connectorRemoteIdentityRecoveryCommand)
	}
	return nil
}

func verifyConnectorRemoteIdentityRecoveryDryRunReceipt(
	args connectorRemoteIdentityRecoveryArgs,
	manifestSHA string,
	runtimeProof connectorRemoteIdentityRecoveryRuntimeProof,
	manifest connectorRemoteIdentityRecoveryManifest,
) error {
	data, err := readConnectorRemoteIdentityRecoveryFile(args.dryRunReceiptPath)
	if err != nil {
		return fmt.Errorf("%s: read dry-run receipt: %w", connectorRemoteIdentityRecoveryCommand, err)
	}
	if _, err := verifyConnectorRemoteIdentityRecoverySHA256("dry-run receipt", data, args.dryRunReceiptSHA256); err != nil {
		return err
	}
	var receipt connectorRemoteIdentityRecoveryReceipt
	if err := decodeConnectorRemoteIdentityRecoveryJSON(data, &receipt); err != nil {
		return fmt.Errorf("%s: decode dry-run receipt: %w", connectorRemoteIdentityRecoveryCommand, err)
	}
	if receipt.SchemaVersion != connectorRemoteIdentityRecoverySchemaVersion ||
		receipt.Command != connectorRemoteIdentityRecoveryCommand ||
		receipt.Mode != "dry_run" ||
		receipt.ManifestSHA256 != manifestSHA ||
		receipt.RuntimeBindingSHA256 != runtimeProof.RuntimeBindingSHA256 ||
		receipt.ReleaseJSONSHA256 != runtimeProof.ReleaseJSONSHA256 ||
		receipt.RuntimeEnvSHA256 != runtimeProof.RuntimeEnvSHA256 ||
		receipt.SetupBinarySHA256 != runtimeProof.SetupBinarySHA256 ||
		receipt.DeployedTupleSHA256 != runtimeProof.DeployedTupleSHA256 ||
		receipt.Outcome != automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeDryRunVerified ||
		receipt.Committed || receipt.ReadbackVerified || receipt.EnrollmentAllowed || !receipt.ResultContractValid ||
		receipt.DryRunReceiptSHA256 != "" || receipt.BackupMarker != "" || receipt.BlockReason != "" ||
		!connectorRemoteIdentityRecoveryCountEquals(receipt.RequestedCount, len(manifest.Candidates)) ||
		!connectorRemoteIdentityRecoveryCountEquals(receipt.RecoveredCount, len(manifest.Candidates)) ||
		!connectorRemoteIdentityRecoveryCountEquals(receipt.FinalizedLinkCount, len(manifest.Candidates)) ||
		!connectorRemoteIdentityRecoveryCountEquals(receipt.ConsumedEvidenceCount, len(manifest.Candidates)) ||
		!connectorRemoteIdentityRecoveryCountEquals(receipt.ResolverMatchCount, 0) ||
		len(receipt.Items) != len(manifest.Candidates) {
		return fmt.Errorf("%s: dry-run receipt does not prove a full rollback-verified batch for this manifest", connectorRemoteIdentityRecoveryCommand)
	}
	expected := make(map[string]connectorRemoteIdentityRecoveryCandidate, len(manifest.Candidates))
	for _, candidate := range manifest.Candidates {
		expected[candidate.RemoteID] = candidate
	}
	for _, item := range receipt.Items {
		candidate, ok := expected[item.RemoteID]
		if !ok || item.SourceObject != candidate.ExpectedSourceObject || item.SourceRecordID != candidate.ExpectedSourceRecordID {
			return fmt.Errorf("%s: dry-run receipt item %q does not match this manifest's exact candidate set", connectorRemoteIdentityRecoveryCommand, item.RemoteID)
		}
		delete(expected, item.RemoteID)
	}
	if len(expected) != 0 {
		return fmt.Errorf("%s: dry-run receipt omitted %d manifest candidate(s)", connectorRemoteIdentityRecoveryCommand, len(expected))
	}
	return nil
}

func validateConnectorRemoteIdentityRecoveryRuntimeResult(
	args connectorRemoteIdentityRecoveryArgs,
	manifest connectorRemoteIdentityRecoveryManifest,
	result automation.ConnectorInboundRemoteIdentityRecoveryBatchResult,
	runErr error,
) (automation.ConnectorInboundRemoteIdentityRecoveryBatchResult, bool, error) {
	wantCount := len(manifest.Candidates)
	expected := make(map[string]connectorRemoteIdentityRecoveryCandidate, wantCount)
	for _, candidate := range manifest.Candidates {
		expected[candidate.RemoteID] = candidate
	}
	itemsExact := len(result.Items) == wantCount
	if itemsExact {
		for _, item := range result.Items {
			candidate, ok := expected[item.RemoteID]
			if !ok || item.SourceObject != candidate.ExpectedSourceObject || item.SourceRecordID != candidate.ExpectedSourceRecordID ||
				strings.TrimSpace(item.LinkKey) == "" || strings.TrimSpace(item.EvidenceID) == "" {
				itemsExact = false
				break
			}
			delete(expected, item.RemoteID)
		}
		itemsExact = itemsExact && len(expected) == 0
	}

	valid := false
	if runErr == nil && !args.apply {
		valid = result.Outcome == automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeDryRunVerified &&
			!result.Committed && !result.ReadbackVerified && !result.EnrollmentAllowed &&
			result.RequestedCount == wantCount && result.RecoveredCount == wantCount && itemsExact &&
			strings.TrimSpace(result.BlockReason) == ""
	}
	if runErr == nil && args.apply {
		valid = result.Outcome == automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeCommittedVerified &&
			result.Committed && result.ReadbackVerified && result.EnrollmentAllowed &&
			result.RequestedCount == wantCount && result.RecoveredCount == wantCount && itemsExact &&
			strings.TrimSpace(result.BlockReason) == ""
	}
	if runErr != nil && result.Committed {
		valid = result.Outcome == automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeCommittedReadbackBlocked &&
			!result.ReadbackVerified && !result.EnrollmentAllowed &&
			result.RequestedCount == wantCount && result.RecoveredCount == wantCount && itemsExact &&
			strings.TrimSpace(result.BlockReason) != ""
	}
	if runErr != nil && !result.Committed {
		valid = result.Outcome == automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeRolledBackBlocked &&
			!result.ReadbackVerified && !result.EnrollmentAllowed && result.RecoveredCount == 0 && len(result.Items) == 0 &&
			(result.RequestedCount == 0 || result.RequestedCount == wantCount) && strings.TrimSpace(result.BlockReason) != ""
	}
	if valid {
		if runErr != nil {
			if result.Committed {
				result.BlockReason = connectorRemoteIdentityRecoveryBlockCommittedReadback
			} else {
				result.BlockReason = connectorRemoteIdentityRecoveryBlockRecoveryRolledBack
			}
		}
		return result, true, runErr
	}

	contractErr := fmt.Errorf("%s: maintenance runner returned a result that violates the receipt contract", connectorRemoteIdentityRecoveryCommand)
	if runErr != nil {
		contractErr = fmt.Errorf("%w: %v", contractErr, runErr)
	}
	if result.Committed {
		result.Outcome = automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeCommittedReadbackBlocked
		result.ReadbackVerified = false
		result.EnrollmentAllowed = false
	} else {
		result = automation.ConnectorInboundRemoteIdentityRecoveryBatchResult{
			Outcome:        automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeRolledBackBlocked,
			RequestedCount: wantCount,
		}
	}
	result.BlockReason = connectorRemoteIdentityRecoveryBlockRuntimeContractInvalid
	return result, false, contractErr
}

func connectorRemoteIdentityRecoveryReceiptFromResult(
	args connectorRemoteIdentityRecoveryArgs,
	manifestSHA string,
	runtimeProof connectorRemoteIdentityRecoveryRuntimeProof,
	manifest connectorRemoteIdentityRecoveryManifest,
	result automation.ConnectorInboundRemoteIdentityRecoveryBatchResult,
	resultContractValid bool,
) connectorRemoteIdentityRecoveryReceipt {
	mode := "dry_run"
	if args.apply {
		mode = "apply"
	}
	receipt := connectorRemoteIdentityRecoveryReceipt{
		SchemaVersion:        connectorRemoteIdentityRecoverySchemaVersion,
		Command:              connectorRemoteIdentityRecoveryCommand,
		Mode:                 mode,
		ManifestSHA256:       manifestSHA,
		RuntimeBindingSHA256: runtimeProof.RuntimeBindingSHA256,
		ReleaseJSONSHA256:    runtimeProof.ReleaseJSONSHA256,
		RuntimeEnvSHA256:     runtimeProof.RuntimeEnvSHA256,
		SetupBinarySHA256:    runtimeProof.SetupBinarySHA256,
		DeployedTupleSHA256:  runtimeProof.DeployedTupleSHA256,
		Outcome:              result.Outcome,
		Committed:            result.Committed,
		ReadbackVerified:     result.ReadbackVerified,
		EnrollmentAllowed:    result.EnrollmentAllowed,
		ResultContractValid:  resultContractValid,
		BlockReason:          result.BlockReason,
		Items:                []connectorRemoteIdentityRecoveryReceiptItem{},
	}
	if args.apply {
		receipt.DryRunReceiptSHA256 = args.dryRunReceiptSHA256
		receipt.BackupMarker = args.backupMarker
	}

	wantCount := len(manifest.Candidates)
	if resultContractValid {
		receipt.RequestedCount = connectorRemoteIdentityRecoveryIntPointer(result.RequestedCount)
		receipt.RecoveredCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
		receipt.FinalizedLinkCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
		receipt.ConsumedEvidenceCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
		resolverMatches := 0
		if result.ReadbackVerified {
			resolverMatches = result.RecoveredCount
		}
		receipt.ResolverMatchCount = connectorRemoteIdentityRecoveryIntPointer(resolverMatches)
		for _, item := range result.Items {
			receipt.Items = append(receipt.Items, connectorRemoteIdentityRecoveryReceiptItem{
				RemoteID: item.RemoteID, SourceObject: item.SourceObject, SourceRecordID: item.SourceRecordID,
			})
		}
		return receipt
	}

	receipt.RequestedCount = connectorRemoteIdentityRecoveryIntPointer(wantCount)
	if result.Committed {
		if result.RecoveredCount >= 0 && result.RecoveredCount <= wantCount {
			receipt.RecoveredCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
			receipt.FinalizedLinkCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
			receipt.ConsumedEvidenceCount = connectorRemoteIdentityRecoveryIntPointer(result.RecoveredCount)
		}
		receipt.Items = connectorRemoteIdentityRecoveryValidatedReceiptSubset(manifest, result.Items)
		return receipt
	}
	zero := 0
	receipt.RecoveredCount = &zero
	receipt.FinalizedLinkCount = connectorRemoteIdentityRecoveryIntPointer(0)
	receipt.ConsumedEvidenceCount = connectorRemoteIdentityRecoveryIntPointer(0)
	receipt.ResolverMatchCount = connectorRemoteIdentityRecoveryIntPointer(0)
	return receipt
}

func connectorRemoteIdentityRecoveryValidatedReceiptSubset(
	manifest connectorRemoteIdentityRecoveryManifest,
	items []automation.ConnectorInboundRemoteIdentityRecoveryBatchItemResult,
) []connectorRemoteIdentityRecoveryReceiptItem {
	expected := make(map[string]connectorRemoteIdentityRecoveryCandidate, len(manifest.Candidates))
	for _, candidate := range manifest.Candidates {
		expected[candidate.RemoteID] = candidate
	}
	type observed struct {
		item  connectorRemoteIdentityRecoveryReceiptItem
		valid bool
		count int
	}
	observedByRemoteID := make(map[string]observed, len(items))
	for _, item := range items {
		candidate, ok := expected[item.RemoteID]
		entry := observedByRemoteID[item.RemoteID]
		entry.count++
		if ok && item.SourceObject == candidate.ExpectedSourceObject && item.SourceRecordID == candidate.ExpectedSourceRecordID {
			entry.valid = true
			entry.item = connectorRemoteIdentityRecoveryReceiptItem{
				RemoteID: item.RemoteID, SourceObject: item.SourceObject, SourceRecordID: item.SourceRecordID,
			}
		} else {
			entry.valid = false
		}
		observedByRemoteID[item.RemoteID] = entry
	}
	remoteIDs := make([]string, 0, len(observedByRemoteID))
	for remoteID, entry := range observedByRemoteID {
		if entry.valid && entry.count == 1 {
			remoteIDs = append(remoteIDs, remoteID)
		}
	}
	sort.Strings(remoteIDs)
	result := make([]connectorRemoteIdentityRecoveryReceiptItem, 0, len(remoteIDs))
	for _, remoteID := range remoteIDs {
		result = append(result, observedByRemoteID[remoteID].item)
	}
	return result
}

func connectorRemoteIdentityRecoveryIntPointer(value int) *int {
	return &value
}

func connectorRemoteIdentityRecoveryCountEquals(value *int, expected int) bool {
	return value != nil && *value == expected
}

func writeConnectorRemoteIdentityRecoveryBlockedReceipt(
	stdout io.Writer,
	args connectorRemoteIdentityRecoveryArgs,
	manifestSHA string,
	runtimeProof connectorRemoteIdentityRecoveryRuntimeProof,
	manifest connectorRemoteIdentityRecoveryManifest,
	requested int,
	blockReason string,
) {
	result := automation.ConnectorInboundRemoteIdentityRecoveryBatchResult{
		Outcome:        automation.ConnectorInboundRemoteIdentityRecoveryBatchOutcomeRolledBackBlocked,
		RequestedCount: requested,
		BlockReason:    blockReason,
	}
	_ = writeConnectorRemoteIdentityRecoveryReceipt(stdout, connectorRemoteIdentityRecoveryReceiptFromResult(args, manifestSHA, runtimeProof, manifest, result, true))
}

func writeConnectorRemoteIdentityRecoveryReceipt(stdout io.Writer, receipt connectorRemoteIdentityRecoveryReceipt) error {
	encoder := json.NewEncoder(stdout)
	encoder.SetEscapeHTML(false)
	return encoder.Encode(receipt)
}

func decodeConnectorRemoteIdentityRecoveryJSON(data []byte, target any) error {
	if err := rejectDuplicateJSONKeys(data); err != nil {
		return err
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(target); err != nil {
		return err
	}
	var extra any
	if err := decoder.Decode(&extra); err != io.EOF {
		if err == nil {
			return fmt.Errorf("multiple JSON values are not allowed")
		}
		return err
	}
	return nil
}

func readConnectorRemoteIdentityRecoveryFile(path string) ([]byte, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("%s: read %s: %w", connectorRemoteIdentityRecoveryCommand, path, err)
	}
	if len(data) == 0 || len(data) > connectorRemoteIdentityRecoveryMaxInputBytes {
		return nil, fmt.Errorf("%s: %s must contain between 1 and %d bytes", connectorRemoteIdentityRecoveryCommand, path, connectorRemoteIdentityRecoveryMaxInputBytes)
	}
	return data, nil
}

func verifyConnectorRemoteIdentityRecoverySHA256(label string, data []byte, expected string) (string, error) {
	expected = strings.TrimSpace(expected)
	if err := validateConnectorRemoteIdentityRecoverySHA256(label, expected); err != nil {
		return "", err
	}
	sum := sha256.Sum256(data)
	actual := hex.EncodeToString(sum[:])
	if actual != expected {
		return "", fmt.Errorf("%s: %s SHA-256 mismatch: got %s", connectorRemoteIdentityRecoveryCommand, label, actual)
	}
	return actual, nil
}

func validateConnectorRemoteIdentityRecoverySHA256(label, value string) error {
	decoded, err := hex.DecodeString(value)
	if err != nil || len(decoded) != sha256.Size || strings.ToLower(value) != value {
		return fmt.Errorf("%s: %s SHA-256 must be exactly 64 lowercase hexadecimal characters", connectorRemoteIdentityRecoveryCommand, label)
	}
	return nil
}

func isConnectorRemoteIdentityRecoveryGitSHA(value string) bool {
	if len(value) != 40 || strings.ToLower(value) != value {
		return false
	}
	decoded, err := hex.DecodeString(value)
	return err == nil && len(decoded) == 20
}

func isConnectorRemoteIdentityRecoveryAuthorityToken(value string) bool {
	if len(value) == 0 || len(value) > 256 {
		return false
	}
	for i := 0; i < len(value); i++ {
		ch := value[i]
		if (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || strings.ContainsRune("-._:+/@", rune(ch)) {
			continue
		}
		return false
	}
	return true
}
