// Package runtimework owns control-plane intent and deterministic evidence for
// activating the generic Runtime Worker deployment fence. Persistence remains
// the sole owner of the atomic database representation and compare-and-set.
package runtimework

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strconv"

	"github.com/nexuscrm/persistence"
	"github.com/pingcap-inc/nexuscrm-controlplane/strictjson"
)

const (
	ConfigSchemaV1 = "nexuscrm.runtime-work-ownership-activation/v1"
	OutputSchemaV1 = "nexuscrm.runtime-work-ownership-activation-result/v1"
)

// Config is the immutable operator-authored compare-and-set intent. Epochs are
// decimal strings so JSON tooling cannot round values above 2^53.
type Config struct {
	Schema                 string `json:"schema"`
	OperationID            string `json:"operationId"`
	WorkFamily             string `json:"workFamily"`
	DeploymentID           string `json:"deploymentId"`
	WorkerReleaseDigest    string `json:"workerReleaseDigest"`
	ExpectedOwnershipEpoch string `json:"expectedOwnershipEpoch"`
	expectedOwnershipEpoch uint64
}

type Output struct {
	Schema              string `json:"schema"`
	OperationID         string `json:"operationId"`
	WorkFamily          string `json:"workFamily"`
	DeploymentID        string `json:"deploymentId"`
	WorkerReleaseDigest string `json:"workerReleaseDigest"`
	OwnershipEpoch      string `json:"ownershipEpoch"`
}

type ActivateFunc func(context.Context, persistence.RuntimeWorkOwnershipActivation) (persistence.RuntimeWorkOwnership, error)

func ParseConfig(raw []byte) (Config, error) {
	if err := strictjson.RejectDuplicateKeys(raw, "Runtime work ownership activation config"); err != nil {
		return Config{}, fmt.Errorf("decode Runtime work ownership activation config: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var config Config
	if err := decoder.Decode(&config); err != nil {
		return Config{}, fmt.Errorf("decode Runtime work ownership activation config: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		return Config{}, errors.New("Runtime work ownership activation config contains trailing content")
	}
	if config.Schema != ConfigSchemaV1 {
		return Config{}, fmt.Errorf("unsupported Runtime work ownership activation config schema %q", config.Schema)
	}
	epoch, err := parseCanonicalEpoch(config.ExpectedOwnershipEpoch, true)
	if err != nil {
		return Config{}, fmt.Errorf("Runtime work ownership activation expected epoch: %w", err)
	}
	config.expectedOwnershipEpoch = epoch
	request, err := config.Activation()
	if err != nil {
		return Config{}, err
	}
	if !persistence.ValidRuntimeOperationID(request.OperationID) {
		return Config{}, errors.New("Runtime work ownership activation operation ID must be canonical and at most 255 bytes")
	}
	return config, nil
}

func (c Config) Activation() (persistence.RuntimeWorkOwnershipActivation, error) {
	epoch := c.expectedOwnershipEpoch
	if c.ExpectedOwnershipEpoch != strconv.FormatUint(epoch, 10) {
		var err error
		epoch, err = parseCanonicalEpoch(c.ExpectedOwnershipEpoch, true)
		if err != nil {
			return persistence.RuntimeWorkOwnershipActivation{}, fmt.Errorf("Runtime work ownership activation expected epoch: %w", err)
		}
	}
	if epoch == ^uint64(0) {
		return persistence.RuntimeWorkOwnershipActivation{}, errors.New("Runtime work ownership activation epoch is exhausted")
	}
	target := persistence.RuntimeWorkOwnership{
		WorkFamily: c.WorkFamily, DeploymentID: c.DeploymentID,
		WorkerReleaseDigest: c.WorkerReleaseDigest, OwnershipEpoch: epoch + 1,
	}
	if err := target.Validate(); err != nil {
		return persistence.RuntimeWorkOwnershipActivation{}, err
	}
	if !persistence.ValidRuntimeOperationID(c.OperationID) {
		return persistence.RuntimeWorkOwnershipActivation{}, errors.New("Runtime work ownership activation operation ID must be canonical and at most 255 bytes")
	}
	return persistence.RuntimeWorkOwnershipActivation{
		OperationID: c.OperationID, WorkFamily: c.WorkFamily, DeploymentID: c.DeploymentID,
		WorkerReleaseDigest: c.WorkerReleaseDigest, ExpectedOwnershipEpoch: epoch,
	}, nil
}

func Apply(ctx context.Context, activate ActivateFunc, config Config) (Output, error) {
	if ctx == nil {
		return Output{}, errors.New("Runtime work ownership activation context is required")
	}
	if activate == nil {
		return Output{}, errors.New("Runtime work ownership activation authority is required")
	}
	request, err := config.Activation()
	if err != nil {
		return Output{}, err
	}
	ownership, err := activate(ctx, request)
	if err != nil {
		return Output{}, err
	}
	wantEpoch := request.ExpectedOwnershipEpoch + 1
	if ownership.WorkFamily != request.WorkFamily || ownership.DeploymentID != request.DeploymentID ||
		ownership.WorkerReleaseDigest != request.WorkerReleaseDigest || ownership.OwnershipEpoch != wantEpoch {
		return Output{}, errors.New("Runtime work ownership activation returned a mismatched authority")
	}
	return Output{
		Schema: OutputSchemaV1, OperationID: request.OperationID, WorkFamily: ownership.WorkFamily,
		DeploymentID: ownership.DeploymentID, WorkerReleaseDigest: ownership.WorkerReleaseDigest,
		OwnershipEpoch: strconv.FormatUint(ownership.OwnershipEpoch, 10),
	}, nil
}

func MarshalOutput(output Output) ([]byte, string, error) {
	if output.Schema != OutputSchemaV1 {
		return nil, "", fmt.Errorf("unsupported Runtime work ownership activation output schema %q", output.Schema)
	}
	epoch, err := parseCanonicalEpoch(output.OwnershipEpoch, false)
	if err != nil {
		return nil, "", fmt.Errorf("Runtime work ownership activation output epoch: %w", err)
	}
	if !persistence.ValidRuntimeOperationID(output.OperationID) {
		return nil, "", errors.New("Runtime work ownership activation output operation ID is invalid")
	}
	if err := (persistence.RuntimeWorkOwnership{
		WorkFamily: output.WorkFamily, DeploymentID: output.DeploymentID,
		WorkerReleaseDigest: output.WorkerReleaseDigest, OwnershipEpoch: epoch,
	}).Validate(); err != nil {
		return nil, "", err
	}
	raw, err := json.Marshal(output)
	if err != nil {
		return nil, "", err
	}
	sum := sha256.Sum256(raw)
	return raw, "sha256:" + hex.EncodeToString(sum[:]), nil
}

func parseCanonicalEpoch(value string, allowZero bool) (uint64, error) {
	if value == "" {
		return 0, errors.New("canonical decimal string is required")
	}
	epoch, err := strconv.ParseUint(value, 10, 64)
	if err != nil || strconv.FormatUint(epoch, 10) != value || (!allowZero && epoch == 0) {
		return 0, errors.New("canonical decimal string is required")
	}
	return epoch, nil
}
