package catalogreconcile

import (
	"bytes"
	"context"
	"crypto/ed25519"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"slices"
	"strconv"
	"strings"
	"time"

	"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 (
	PublisherTrustTransitionConfigSchemaV1 = "nexuscrm.catalog-publisher-trust-transition/v1"
	PublisherTrustTransitionOutputSchemaV1 = "nexuscrm.catalog-publisher-trust-transition-result/v1"
)

type PublisherTrustTransitionKey struct {
	KeyID           string `json:"keyId"`
	Algorithm       string `json:"algorithm"`
	PublicKeyBase64 string `json:"publicKeyBase64"`
	ValidFrom       string `json:"validFrom"`
	ValidUntil      string `json:"validUntil"`
}

// PublisherTrustTransitionConfig is a separate operator-authored compare-and-set operation. The
// expected trust is part of the fence: a release package cannot authorize its
// own key by supplying only a target key.
type PublisherTrustTransitionConfig struct {
	Schema                 string                        `json:"schema"`
	OperationID            string                        `json:"operationId"`
	PublisherID            string                        `json:"publisherId"`
	ExpectedOwnershipEpoch string                        `json:"expectedOwnershipEpoch"`
	ExpectedActiveKeys     []PublisherTrustTransitionKey `json:"expectedActiveKeys"`
	TargetActiveKeys       []PublisherTrustTransitionKey `json:"targetActiveKeys"`
	expectedEpoch          uint64
	expectedTrust          catalogapp.PublisherTrust
	targetTrust            catalogapp.PublisherTrust
}

type PublisherTrustTransitionOutput struct {
	Schema              string `json:"schema"`
	OperationID         string `json:"operationId"`
	PublisherID         string `json:"publisherId"`
	OwnershipEpoch      string `json:"ownershipEpoch"`
	PreviousTrustDigest string `json:"previousTrustDigest"`
	ActiveTrustDigest   string `json:"activeTrustDigest"`
}

type ReadPublisherFunc func(context.Context, string) (catalogapp.RegisteredPublisher, error)
type TransitionPublisherFunc func(context.Context, catalogapp.PublisherTrustTransitionRequest) (catalogapp.PublisherTrustTransitionResult, error)

func ParsePublisherTrustTransitionConfig(raw []byte) (PublisherTrustTransitionConfig, error) {
	if err := strictjson.RejectDuplicateKeys(raw, "Catalog publisher trust transition config"); err != nil {
		return PublisherTrustTransitionConfig{}, fmt.Errorf("decode Catalog publisher trust transition config: %w", err)
	}
	decoder := json.NewDecoder(bytes.NewReader(raw))
	decoder.DisallowUnknownFields()
	var config PublisherTrustTransitionConfig
	if err := decoder.Decode(&config); err != nil {
		return PublisherTrustTransitionConfig{}, fmt.Errorf("decode Catalog publisher trust transition config: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		return PublisherTrustTransitionConfig{}, errors.New("Catalog publisher trust transition config contains trailing content")
	}
	if config.Schema != PublisherTrustTransitionConfigSchemaV1 {
		return PublisherTrustTransitionConfig{}, fmt.Errorf("unsupported Catalog publisher trust transition config schema %q", config.Schema)
	}
	if strings.TrimSpace(config.OperationID) == "" || len(config.OperationID) > 255 || strings.TrimSpace(config.OperationID) != config.OperationID {
		return PublisherTrustTransitionConfig{}, errors.New("Catalog publisher trust transition operation ID is invalid")
	}
	if err := appauthority.ValidatePublisherID(config.PublisherID); err != nil {
		return PublisherTrustTransitionConfig{}, err
	}
	epoch, err := parseEpoch(config.ExpectedOwnershipEpoch)
	if err != nil {
		return PublisherTrustTransitionConfig{}, err
	}
	if epoch == ^uint64(0) {
		return PublisherTrustTransitionConfig{}, errors.New("Catalog publisher trust transition expected epoch is exhausted")
	}
	expected, err := trustFromKeys(config.PublisherID, config.ExpectedActiveKeys)
	if err != nil {
		return PublisherTrustTransitionConfig{}, fmt.Errorf("expected publisher trust: %w", err)
	}
	target, err := trustFromKeys(config.PublisherID, config.TargetActiveKeys)
	if err != nil {
		return PublisherTrustTransitionConfig{}, fmt.Errorf("target publisher trust: %w", err)
	}
	if trustMatches(expected, target) {
		return PublisherTrustTransitionConfig{}, errors.New("Catalog publisher trust transition target equals expected trust")
	}
	config.expectedEpoch = epoch
	config.expectedTrust = expected
	config.targetTrust = target
	return config, nil
}

func ApplyPublisherTrustTransition(ctx context.Context, read ReadPublisherFunc, transition TransitionPublisherFunc, config PublisherTrustTransitionConfig) (PublisherTrustTransitionOutput, error) {
	if ctx == nil || read == nil || transition == nil {
		return PublisherTrustTransitionOutput{}, errors.New("Catalog publisher trust transition authority is required")
	}
	previousDigest, err := trustDigest(config.expectedTrust)
	if err != nil {
		return PublisherTrustTransitionOutput{}, err
	}
	targetDigest, err := trustDigest(config.targetTrust)
	if err != nil {
		return PublisherTrustTransitionOutput{}, err
	}
	result, err := transition(ctx, catalogapp.PublisherTrustTransitionRequest{
		OperationID: config.OperationID, ExpectedOwnershipEpoch: config.expectedEpoch,
		ExpectedTrust: config.expectedTrust, TargetTrust: config.targetTrust,
	})
	if err != nil {
		return PublisherTrustTransitionOutput{}, fmt.Errorf("rotate Catalog publisher trust: %w", err)
	}
	if result.OperationID != config.OperationID || result.PublisherID != config.PublisherID ||
		result.ExpectedOwnershipEpoch != config.expectedEpoch ||
		result.OwnershipEpoch != config.expectedEpoch+1 ||
		result.PreviousTrustDigest != previousDigest || result.ActiveTrustDigest != targetDigest {
		return PublisherTrustTransitionOutput{}, errors.New("Catalog publisher trust transition returned mismatched durable evidence")
	}
	confirmed, err := read(ctx, config.PublisherID)
	if err != nil {
		return PublisherTrustTransitionOutput{}, fmt.Errorf("confirm Catalog publisher trust transition: %w", err)
	}
	if confirmed.OwnershipEpoch != result.OwnershipEpoch || !registeredMatches(confirmed, config.targetTrust) {
		return PublisherTrustTransitionOutput{}, errors.New("Catalog publisher trust transition returned mismatched authority")
	}
	return transitionOutput(config, result.OwnershipEpoch, previousDigest, targetDigest), nil
}

func transitionOutput(config PublisherTrustTransitionConfig, epoch uint64, previousDigest, targetDigest string) PublisherTrustTransitionOutput {
	return PublisherTrustTransitionOutput{Schema: PublisherTrustTransitionOutputSchemaV1, OperationID: config.OperationID, PublisherID: config.PublisherID,
		OwnershipEpoch: strconv.FormatUint(epoch, 10), PreviousTrustDigest: previousDigest, ActiveTrustDigest: targetDigest}
}

func MarshalPublisherTrustTransitionOutput(output PublisherTrustTransitionOutput) ([]byte, string, error) {
	if output.Schema != PublisherTrustTransitionOutputSchemaV1 || output.OperationID == "" || output.PublisherID == "" {
		return nil, "", errors.New("Catalog publisher trust transition output is incomplete")
	}
	if _, err := parseEpoch(output.OwnershipEpoch); err != nil {
		return nil, "", err
	}
	for _, digest := range []string{output.PreviousTrustDigest, output.ActiveTrustDigest} {
		if len(digest) != 71 || !strings.HasPrefix(digest, "sha256:") {
			return nil, "", errors.New("Catalog publisher trust transition output digest is invalid")
		}
		if _, err := hex.DecodeString(strings.TrimPrefix(digest, "sha256:")); err != nil {
			return nil, "", errors.New("Catalog publisher trust transition output digest is invalid")
		}
	}
	raw, err := json.Marshal(output)
	if err != nil {
		return nil, "", err
	}
	sum := sha256.Sum256(raw)
	return raw, "sha256:" + hex.EncodeToString(sum[:]), nil
}

func trustFromKeys(publisherID string, input []PublisherTrustTransitionKey) (catalogapp.PublisherTrust, error) {
	if len(input) == 0 {
		return catalogapp.PublisherTrust{}, errors.New("at least one active key is required")
	}
	keys := make([]catalogapp.PublisherKey, 0, len(input))
	for index, item := range input {
		if strings.TrimSpace(item.KeyID) == "" || strings.TrimSpace(item.KeyID) != item.KeyID || len(item.KeyID) > 128 || item.Algorithm != releaseartifact.SignatureAlgorithm {
			return catalogapp.PublisherTrust{}, fmt.Errorf("key[%d] identity is invalid", index)
		}
		publicKey, err := base64.StdEncoding.DecodeString(item.PublicKeyBase64)
		if err != nil || base64.StdEncoding.EncodeToString(publicKey) != item.PublicKeyBase64 || len(publicKey) != ed25519.PublicKeySize {
			return catalogapp.PublisherTrust{}, fmt.Errorf("key[%d] public key is not canonical base64", index)
		}
		validFrom, err := parseTime(item.ValidFrom)
		if err != nil {
			return catalogapp.PublisherTrust{}, fmt.Errorf("key[%d] validFrom: %w", index, err)
		}
		validUntil, err := parseTime(item.ValidUntil)
		if err != nil || !validUntil.After(validFrom) {
			return catalogapp.PublisherTrust{}, fmt.Errorf("key[%d] validity window is invalid", index)
		}
		keys = append(keys, catalogapp.PublisherKey{KeyID: item.KeyID, Algorithm: item.Algorithm,
			PublicKey: publicKey, ValidFrom: validFrom, ValidUntil: validUntil})
	}
	slices.SortFunc(keys, func(left, right catalogapp.PublisherKey) int { return strings.Compare(left.KeyID, right.KeyID) })
	for index := 1; index < len(keys); index++ {
		if keys[index-1].KeyID == keys[index].KeyID {
			return catalogapp.PublisherTrust{}, fmt.Errorf("duplicate key ID %q", keys[index].KeyID)
		}
	}
	return catalogapp.PublisherTrust{PublisherID: publisherID, Active: true, Keys: keys}, nil
}

func registeredMatches(existing catalogapp.RegisteredPublisher, trust catalogapp.PublisherTrust) bool {
	if existing.PublisherID != trust.PublisherID || !existing.Active {
		return false
	}
	active := make([]catalogapp.PublisherKey, 0, len(existing.Keys))
	for _, key := range existing.Keys {
		if key.Active {
			active = append(active, key.PublisherKey)
		}
	}
	slices.SortFunc(active, func(left, right catalogapp.PublisherKey) int { return strings.Compare(left.KeyID, right.KeyID) })
	return keysMatch(active, trust.Keys)
}

func trustMatches(left, right catalogapp.PublisherTrust) bool {
	return left.PublisherID == right.PublisherID && left.Active == right.Active && keysMatch(left.Keys, right.Keys)
}

func keysMatch(left, right []catalogapp.PublisherKey) bool {
	if len(left) != len(right) {
		return false
	}
	for index := range left {
		if left[index].KeyID != right[index].KeyID || left[index].Algorithm != right[index].Algorithm ||
			!bytes.Equal(left[index].PublicKey, right[index].PublicKey) || !left[index].ValidFrom.Equal(right[index].ValidFrom) ||
			!left[index].ValidUntil.Equal(right[index].ValidUntil) {
			return false
		}
	}
	return true
}

func trustDigest(trust catalogapp.PublisherTrust) (string, error) {
	return catalogapp.PublisherTrustDigest(trust)
}

func parseEpoch(value string) (uint64, error) {
	epoch, err := strconv.ParseUint(value, 10, 64)
	if err != nil || strconv.FormatUint(epoch, 10) != value {
		return 0, errors.New("Catalog publisher trust transition epoch must be a canonical decimal string")
	}
	return epoch, nil
}

func parseTime(value string) (time.Time, error) {
	parsed, err := time.Parse(time.RFC3339Nano, value)
	if err != nil || parsed.Location() != time.UTC || formatTime(parsed) != value {
		return time.Time{}, errors.New("canonical UTC RFC3339 timestamp is required")
	}
	return parsed.UTC().Truncate(time.Microsecond), nil
}

func formatTime(value time.Time) string {
	return value.UTC().Truncate(time.Microsecond).Format(time.RFC3339Nano)
}
