package catalogtenant

import (
	"bytes"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"fmt"

	tenantauthorityv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/tenantlayoutbundleauthorityv1"
)

const outcomeDigestDomain = "nexuscrm.catalog-tenant-authority-outcome/v1"

func completeTerminalCommand(command terminalCommand) (terminalCommand, error) {
	if err := validateTerminalCommand(command, false); err != nil {
		return terminalCommand{}, err
	}
	digest, err := terminalOutcomeDigest(command)
	if err != nil {
		return terminalCommand{}, err
	}
	command.OutcomeDigest = digest
	return command, nil
}

func validateDurableTerminalCommand(command terminalCommand) error {
	if err := validateTerminalCommand(command, true); err != nil {
		return fmt.Errorf("%w: invalid terminal command: %v", ErrCorrupt, err)
	}
	computed, err := terminalOutcomeDigest(command)
	if err != nil {
		return fmt.Errorf("%w: cannot recompute terminal outcome: %v", ErrCorrupt, err)
	}
	if computed != command.OutcomeDigest {
		return fmt.Errorf(
			"%w: outcome digest is %q, stored %q",
			ErrCorrupt,
			computed,
			command.OutcomeDigest,
		)
	}
	return nil
}

func validateTerminalCommand(command terminalCommand, requireDigest bool) error {
	if err := tenantauthorityv1.ValidateCommandID(command.CommandID); err != nil {
		return err
	}
	if err := validateDigest(command.RequestDigest); err != nil {
		return fmt.Errorf("RequestDigest: %w", err)
	}
	if command.Sequence == 0 {
		return fmt.Errorf("Sequence must be positive")
	}
	for label, reference := range map[string]ValueReference{
		"ObservedPrior":     command.ObservedPrior,
		"ResultingValue":    command.ResultingValue,
		"ObservedNamespace": command.ObservedNamespace,
		"ObservedBinding":   command.ObservedBinding,
	} {
		if err := validateReference(reference); err != nil {
			return fmt.Errorf("%s: %w", label, err)
		}
	}
	if requireDigest {
		if err := validateDigest(command.OutcomeDigest); err != nil {
			return fmt.Errorf("OutcomeDigest: %w", err)
		}
	} else if command.OutcomeDigest != "" {
		return fmt.Errorf("OutcomeDigest must be empty before derivation")
	}

	switch command.Kind {
	case commandKindNamespace:
		if command.ObservedNamespace.Present || command.ObservedBinding.Present {
			return fmt.Errorf("namespace command must not carry dependency references")
		}
		switch command.Result {
		case ResultApplied, ResultExpectedPriorMismatch, ResultActiveBinding, ResultActiveSlot:
		default:
			return fmt.Errorf("invalid namespace command result %q", command.Result)
		}
	case commandKindBinding:
		if command.ObservedBinding.Present {
			return fmt.Errorf("binding command must not carry a binding dependency reference")
		}
		switch command.Result {
		case ResultApplied, ResultExpectedPriorMismatch,
			ResultNamespaceDependencyMismatch, ResultActiveSlot:
		default:
			return fmt.Errorf("invalid binding command result %q", command.Result)
		}
	case commandKindSlot:
		switch command.Result {
		case ResultApplied, ResultExpectedPriorMismatch,
			ResultNamespaceDependencyMismatch, ResultBindingDependencyMismatch:
		default:
			return fmt.Errorf("invalid slot command result %q", command.Result)
		}
	default:
		return fmt.Errorf("unknown command kind %q", command.Kind)
	}
	if command.Result != ResultApplied && command.ResultingValue != command.ObservedPrior {
		return fmt.Errorf("non-applied command must retain the observed prior")
	}
	return nil
}

func terminalOutcomeDigest(command terminalCommand) (string, error) {
	copy := command
	copy.OutcomeDigest = ""
	if err := validateTerminalCommand(copy, false); err != nil {
		return "", err
	}
	var canonical bytes.Buffer
	writeCanonicalString(&canonical, outcomeDigestDomain)
	writeCanonicalString(&canonical, string(command.Kind))
	writeCanonicalString(&canonical, string(command.CommandID))
	writeCanonicalString(&canonical, command.RequestDigest)
	writeCanonicalString(&canonical, string(command.Result))
	writeReference(&canonical, command.ObservedPrior)
	writeReference(&canonical, command.ResultingValue)
	writeReference(&canonical, command.ObservedNamespace)
	writeReference(&canonical, command.ObservedBinding)
	writeCanonicalUint(&canonical, command.Sequence)
	digest := sha256.Sum256(canonical.Bytes())
	return "sha256:" + hex.EncodeToString(digest[:]), nil
}

func writeReference(out *bytes.Buffer, reference ValueReference) {
	if !reference.Present {
		out.WriteByte(0)
		return
	}
	out.WriteByte(1)
	writeCanonicalString(out, reference.Digest)
}

func writeCanonicalString(out *bytes.Buffer, value string) {
	writeCanonicalUint(out, uint64(len(value)))
	out.WriteString(value)
}

func writeCanonicalUint(out *bytes.Buffer, value uint64) {
	var encoded [binary.MaxVarintLen64]byte
	count := binary.PutUvarint(encoded[:], value)
	out.Write(encoded[:count])
}

func namespaceOutcome(command terminalCommand) NamespaceCommandOutcome {
	return NamespaceCommandOutcome{
		CommandID:      command.CommandID,
		RequestDigest:  command.RequestDigest,
		Result:         command.Result,
		ObservedPrior:  command.ObservedPrior,
		ResultingValue: command.ResultingValue,
		Sequence:       command.Sequence,
		OutcomeDigest:  command.OutcomeDigest,
	}
}

func bindingOutcome(command terminalCommand) OrgBindingCommandOutcome {
	return OrgBindingCommandOutcome{
		CommandID:         command.CommandID,
		RequestDigest:     command.RequestDigest,
		Result:            command.Result,
		ObservedNamespace: command.ObservedNamespace,
		ObservedPrior:     command.ObservedPrior,
		ResultingValue:    command.ResultingValue,
		Sequence:          command.Sequence,
		OutcomeDigest:     command.OutcomeDigest,
	}
}

func slotOutcome(command terminalCommand) SlotCommandOutcome {
	return SlotCommandOutcome{
		CommandID:         command.CommandID,
		RequestDigest:     command.RequestDigest,
		Result:            command.Result,
		ObservedNamespace: command.ObservedNamespace,
		ObservedBinding:   command.ObservedBinding,
		ObservedPrior:     command.ObservedPrior,
		ResultingValue:    command.ResultingValue,
		Sequence:          command.Sequence,
		OutcomeDigest:     command.OutcomeDigest,
	}
}
