package clusterrecovery

import (
	"errors"
	"fmt"
	"io"
)

func DecodeReceipt(r io.Reader) (*Receipt, error) {
	var receipt Receipt
	if err := decodeStrictBoundedJSON(r, MaxSeedBytes, &receipt); err != nil {
		return nil, fmt.Errorf("decode recovery receipt: %w", err)
	}
	if !isSHA256(receipt.ReceiptHash) {
		return nil, errors.New("receipt_hash must be a lowercase SHA-256 digest")
	}
	if err := validateReceiptShape(&receipt); err != nil {
		return nil, err
	}
	return &receipt, nil
}

func FinalizeReceipt(receipt *Receipt) error {
	if receipt == nil {
		return errors.New("receipt is required")
	}
	receipt.StartedAt = receipt.StartedAt.UTC()
	receipt.CompletedAt = receipt.CompletedAt.UTC()
	receipt.ReceiptHash = ""
	if err := validateReceiptShape(receipt); err != nil {
		return err
	}
	hash, err := checksumJSON(receipt)
	if err != nil {
		return err
	}
	receipt.ReceiptHash = hash
	return nil
}

func VerifyReceipt(receipt *Receipt, expectedHash string) error {
	if receipt == nil {
		return errors.New("receipt is required")
	}
	if err := validateReceiptShape(receipt); err != nil {
		return err
	}
	if !isSHA256(expectedHash) || receipt.ReceiptHash != expectedHash {
		return errors.New("receipt hash does not match expected hash")
	}
	copyReceipt := *receipt
	copyReceipt.ReceiptHash = ""
	actual, err := checksumJSON(copyReceipt)
	if err != nil {
		return err
	}
	if actual != expectedHash {
		return errors.New("receipt content hash verification failed")
	}
	return nil
}

func validateReceiptShape(receipt *Receipt) error {
	if receipt.SchemaVersion != SchemaVersion || receipt.Environment != StagingEnvironment {
		return errors.New("receipt schema or environment is invalid")
	}
	if !runIDPattern.MatchString(receipt.RunID) {
		return errors.New("receipt run_id is invalid")
	}
	if !isSHA256(receipt.ManifestHash) || !isSHA256(receipt.ApprovalRefHash) || !isSHA256(receipt.WorkloadIdentityHash) {
		return errors.New("receipt manifest, approval, or workload identity hash is invalid")
	}
	if receipt.MutationClass != MutationCreate && receipt.MutationClass != MutationUpdate {
		return errors.New("receipt mutation class is invalid")
	}
	if receipt.Phase != ApplyPhaseCanary && receipt.Phase != ApplyPhaseBatch {
		return errors.New("receipt phase is invalid")
	}
	if receipt.Offset < 0 || receipt.Limit <= 0 || len(receipt.Results) > receipt.Limit {
		return errors.New("receipt action bounds are invalid")
	}
	if receipt.StartedAt.IsZero() || receipt.CompletedAt.IsZero() || receipt.CompletedAt.Before(receipt.StartedAt) {
		return errors.New("receipt timestamps are invalid")
	}
	if receipt.Phase == ApplyPhaseBatch {
		if !isSHA256(receipt.PriorReceiptHash) || !isSHA256(receipt.VerificationRefHash) {
			return errors.New("batch receipt must reference verified prior-batch evidence")
		}
	}
	if receipt.RollbackBundleHash != "" && !isSHA256(receipt.RollbackBundleHash) {
		return errors.New("receipt rollback bundle hash is invalid")
	}
	if !receipt.Stopped && !isSHA256(receipt.RollbackBundleHash) {
		return errors.New("successful receipt must reference rollback evidence")
	}
	for i, result := range receipt.Results {
		if result.ActionIndex < receipt.Offset || result.ActionIndex >= receipt.Offset+receipt.Limit {
			return fmt.Errorf("receipt result %d is outside action bounds", i)
		}
		if result.Kind != ObjectKindMgmtCluster && result.Kind != ObjectKindResourcePool {
			return fmt.Errorf("receipt result %d kind is invalid", i)
		}
		if _, err := parsePositiveID(result.ClusterID); err != nil {
			return fmt.Errorf("receipt result %d cluster ID is invalid", i)
		}
		if result.Outcome != MutationOutcomeApplied && result.Outcome != MutationOutcomeNoop && result.Outcome != MutationOutcomeFailed {
			return fmt.Errorf("receipt result %d outcome is invalid", i)
		}
		if result.Outcome == MutationOutcomeFailed && result.ReasonCode == "" {
			return fmt.Errorf("failed receipt result %d lacks a reason code", i)
		}
		if !receipt.Stopped && result.ActionIndex != receipt.Offset+i {
			return errors.New("successful receipt results must be contiguous")
		}
		for _, checksum := range []string{result.BeforeChecksum, result.AfterChecksum} {
			if checksum != "" && !isSHA256(checksum) {
				return fmt.Errorf("receipt result %d checksum is invalid", i)
			}
		}
		switch result.Outcome {
		case MutationOutcomeApplied:
			if !isSHA256(result.AfterChecksum) ||
				(receipt.MutationClass == MutationCreate && result.BeforeChecksum != "") ||
				(receipt.MutationClass == MutationUpdate && (!isSHA256(result.BeforeChecksum) || result.BeforeChecksum == result.AfterChecksum)) {
				return fmt.Errorf("applied receipt result %d has invalid mutation checksums", i)
			}
		case MutationOutcomeNoop:
			if !isSHA256(result.BeforeChecksum) || result.BeforeChecksum != result.AfterChecksum {
				return fmt.Errorf("no-op receipt result %d has invalid checksums", i)
			}
		case MutationOutcomeFailed:
			if result.AfterChecksum != "" {
				return fmt.Errorf("failed receipt result %d must not have an after checksum", i)
			}
		}
	}
	if !receipt.Stopped && len(receipt.Results) != receipt.Limit {
		return errors.New("successful receipt must account for every selected action")
	}
	return nil
}

func successfulPriorReceipt(receipt *Receipt, manifest *Manifest) bool {
	if receipt == nil || manifest == nil || receipt.Stopped ||
		receipt.ManifestHash != manifest.ManifestHash || receipt.MutationClass != manifest.MutationClass ||
		receipt.RunID != manifest.RunID || len(receipt.Results) == 0 {
		return false
	}
	if receipt.Phase != ApplyPhaseCanary && receipt.Phase != ApplyPhaseBatch {
		return false
	}
	for _, result := range receipt.Results {
		if result.Outcome != MutationOutcomeApplied && result.Outcome != MutationOutcomeNoop {
			return false
		}
		if result.ActionIndex < 0 || result.ActionIndex >= len(manifest.Actions) {
			return false
		}
		action := manifest.Actions[result.ActionIndex]
		if result.ClusterID != action.ClusterID || result.Kind != action.Kind {
			return false
		}
	}
	return true
}
