package approvalir

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"reflect"
	"strings"
	"unicode/utf8"
)

const (
	DefinitionEnvelopeSchemaV1 = "nexuscrm.approval-definition/v1"
	MaxDefinitionSourceBytes   = 4 * 1024 * 1024
	MaxDefinitionEnvelopeBytes = 1024 * 1024
)

// DefinitionEnvelope is the inert, product-neutral handoff from an L1-owned
// authoring decoder to Catalog publication. Catalog remains the authority that
// revision-stamps and serves the definition; this envelope is only compiler
// input and cannot activate itself.
type DefinitionEnvelope struct {
	Schema       string     `json:"schema"`
	SourceDigest string     `json:"sourceDigest"`
	Definition   Definition `json:"definition"`
}

// EncodeDefinitionEnvelope lowers one already-decoded definition into the
// canonical neutral sidecar stored by the L1 metadata lifecycle.
func EncodeDefinitionEnvelope(source []byte, definition *Definition, validator FormulaValidator) ([]byte, error) {
	if len(source) == 0 || len(source) > MaxDefinitionSourceBytes {
		return nil, fmt.Errorf("approval definition source size %d is outside 1..%d", len(source), MaxDefinitionSourceBytes)
	}
	if definition == nil {
		return nil, fmt.Errorf("approval definition is nil")
	}
	if err := validateDefinitionUTF8(definition); err != nil {
		return nil, err
	}
	if err := definition.ValidateWithFormulaValidator(validator); err != nil {
		return nil, fmt.Errorf("approval definition: %w", err)
	}
	sum := sha256.Sum256(source)
	envelope := DefinitionEnvelope{
		Schema:       DefinitionEnvelopeSchemaV1,
		SourceDigest: "sha256:" + hex.EncodeToString(sum[:]),
		Definition:   canonicalDefinition(definition),
	}
	encoded, err := json.Marshal(envelope)
	if err != nil {
		return nil, fmt.Errorf("encode approval definition envelope: %w", err)
	}
	if len(encoded) > MaxDefinitionEnvelopeBytes {
		return nil, fmt.Errorf("approval definition envelope size %d exceeds %d", len(encoded), MaxDefinitionEnvelopeBytes)
	}
	return encoded, nil
}

// validateDefinitionUTF8 must run before any JSON encoding. encoding/json
// replaces malformed UTF-8 in Go strings, which would otherwise change the
// published definition before its canonical bytes and digest are established.
func validateDefinitionUTF8(definition *Definition) error {
	return validateUTF8Strings(reflect.ValueOf(definition), "definition")
}

func validateUTF8Strings(value reflect.Value, path string) error {
	if !value.IsValid() {
		return nil
	}
	switch value.Kind() {
	case reflect.Interface, reflect.Pointer:
		if value.IsNil() {
			return nil
		}
		return validateUTF8Strings(value.Elem(), path)
	case reflect.String:
		if !utf8.ValidString(value.String()) {
			return fmt.Errorf("approval %s contains invalid UTF-8", path)
		}
	case reflect.Struct:
		typeOfValue := value.Type()
		for index := 0; index < value.NumField(); index++ {
			if err := validateUTF8Strings(value.Field(index), path+"."+typeOfValue.Field(index).Name); err != nil {
				return err
			}
		}
	case reflect.Slice, reflect.Array:
		for index := 0; index < value.Len(); index++ {
			if err := validateUTF8Strings(value.Index(index), fmt.Sprintf("%s[%d]", path, index)); err != nil {
				return err
			}
		}
	}
	return nil
}

// DecodeDefinitionEnvelope strictly opens one canonical neutral sidecar. It
// rejects unknown fields, stale schemas, non-canonical bytes, and definitions
// that the platform formula authority cannot execute.
func DecodeDefinitionEnvelope(data []byte, validator FormulaValidator) (*DefinitionEnvelope, error) {
	if len(data) == 0 || len(data) > MaxDefinitionEnvelopeBytes {
		return nil, fmt.Errorf("approval definition envelope size %d is outside 1..%d", len(data), MaxDefinitionEnvelopeBytes)
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()
	var envelope DefinitionEnvelope
	if err := decoder.Decode(&envelope); err != nil {
		return nil, fmt.Errorf("decode approval definition envelope: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return nil, fmt.Errorf("decode approval definition envelope: trailing JSON value")
		}
		return nil, fmt.Errorf("decode approval definition envelope: trailing content: %w", err)
	}
	if envelope.Schema != DefinitionEnvelopeSchemaV1 {
		return nil, fmt.Errorf("unsupported approval definition envelope schema %q", envelope.Schema)
	}
	if !validSourceDigest(envelope.SourceDigest) {
		return nil, fmt.Errorf("invalid approval definition source digest %q", envelope.SourceDigest)
	}
	if err := envelope.Definition.ValidateWithFormulaValidator(validator); err != nil {
		return nil, fmt.Errorf("approval definition: %w", err)
	}
	envelope.Definition = canonicalDefinition(&envelope.Definition)
	canonical, err := json.Marshal(envelope)
	if err != nil {
		return nil, fmt.Errorf("re-encode approval definition envelope: %w", err)
	}
	if !bytes.Equal(data, canonical) {
		return nil, fmt.Errorf("approval definition envelope is not canonical")
	}
	return &envelope, nil
}

func validSourceDigest(value string) bool {
	if len(value) != len("sha256:")+sha256.Size*2 || value != strings.ToLower(value) || !strings.HasPrefix(value, "sha256:") {
		return false
	}
	_, err := hex.DecodeString(strings.TrimPrefix(value, "sha256:"))
	return err == nil
}

func canonicalDefinition(source *Definition) Definition {
	result := *source
	result.Policy.AllowedSubmitters = append([]AllowedSubmitter{}, source.Policy.AllowedSubmitters...)
	result.Steps = append([]Step{}, source.Steps...)
	for index := range result.Steps {
		step := &result.Steps[index]
		step.Approvers = append([]Approver{}, step.Approvers...)
		step.ApprovalActions = append([]Action{}, step.ApprovalActions...)
		step.RejectionActions = append([]Action{}, step.RejectionActions...)
		step.Criteria = canonicalCriteria(step.Criteria)
	}
	result.EntryCriteria = canonicalCriteria(source.EntryCriteria)
	result.InitialSubmissionActions = append([]Action{}, source.InitialSubmissionActions...)
	result.StepApprovalActions = append([]Action{}, source.StepApprovalActions...)
	result.FinalApprovalActions = append([]Action{}, source.FinalApprovalActions...)
	result.FinalRejectionActions = append([]Action{}, source.FinalRejectionActions...)
	result.RecallActions = append([]Action{}, source.RecallActions...)
	result.ReassignActions = append([]Action{}, source.ReassignActions...)
	return result
}

func canonicalCriteria(source *Criteria) *Criteria {
	if source == nil {
		return nil
	}
	result := *source
	result.Items = append([]CriteriaItem{}, source.Items...)
	return &result
}
