package applicationnavigationv1

import (
	"context"
	"encoding/json"
	"net/url"
	"reflect"
	"strings"
	"unicode"
	"unicode/utf8"

	common "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/commonv1"
)

func invalid() error {
	return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentOperation}
}
func internal() error { return &common.Failure{Category: common.FailureInternal} }
func validText(s string, max int, empty bool) bool {
	if len(s) > max || !utf8.ValidString(s) || strings.TrimSpace(s) != s || (!empty && s == "") {
		return false
	}
	for _, r := range s {
		if unicode.IsControl(r) || unicode.Is(unicode.Cf, r) {
			return false
		}
	}
	return true
}
func ValidIdentifier(s string) bool { return validText(s, MaxTextBytes, false) }

func ValidateItems(items []Item) error {
	if items == nil || len(items) > MaxItems {
		return invalid()
	}
	seen := make(map[string]bool, len(items))
	for _, item := range items {
		key := item.ID
		if !ValidIdentifier(item.ID) || seen[key] || !validText(item.Label, MaxTextBytes, false) || !validText(item.Icon, MaxTextBytes, true) {
			return invalid()
		}
		seen[key] = true
		arms := 0
		if r := item.Target.Resource; r != nil {
			arms++
			if !common.ValidObjectIdentifier(r.ResourceType) || !validText(r.ResourceID, MaxTextBytes, true) {
				return invalid()
			}
		}
		if p := item.Target.Page; p != nil {
			arms++
			if !ValidIdentifier(p.ID) {
				return invalid()
			}
		}
		if u := item.Target.URL; u != nil {
			arms++
			if !validText(*u, MaxURLBytes, false) || strings.Contains(*u, "\\") {
				return invalid()
			}
			parsed, err := url.Parse(*u)
			if err != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") || parsed.Hostname() == "" || parsed.User != nil {
				return invalid()
			}
		}
		if arms != 1 {
			return invalid()
		}
	}
	return nil
}
func ValidateReadRequest(ctx context.Context, r ReadRequest) error {
	if err := common.ValidateRequestContext(ctx); err != nil {
		return err
	}
	if !ValidIdentifier(r.ApplicationID) {
		return invalid()
	}
	return nil
}
func ValidateResolveRequest(ctx context.Context, r ResolveRequest) error {
	if err := common.ValidateRequestContext(ctx); err != nil {
		return err
	}
	return validateResolveRequest(r)
}
func validateResolveRequest(r ResolveRequest) error {
	if r.ApplicationIDs == nil || len(r.ApplicationIDs) > MaxApplications {
		return invalid()
	}
	seen := map[string]bool{}
	for _, id := range r.ApplicationIDs {
		key := id
		if !ValidIdentifier(id) || seen[key] {
			return invalid()
		}
		seen[key] = true
	}
	return nil
}
func ValidateSaveRequest(ctx context.Context, r SaveRequest) error {
	if err := ValidateReadRequest(ctx, ReadRequest{r.ApplicationID}); err != nil {
		return err
	}
	return validateSaveRequest(r)
}
func validateSaveRequest(r SaveRequest) error {
	if !ValidIdentifier(r.ApplicationID) || r.ExpectedRevision == 0 || !ValidIdentifier(r.IdempotencyKey) {
		return invalid()
	}
	if err := ValidateItems(r.Items); err != nil {
		return err
	}
	b, err := json.Marshal(r)
	if err != nil {
		return invalid()
	}
	if len(b) > MaxSemanticRequestBytes {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticRequest, Maximum: MaxSemanticRequestBytes}
	}
	return nil
}
func validateDocument(d Document) error {
	if !ValidIdentifier(d.ApplicationID) || d.Revision == 0 {
		return internal()
	}
	if ValidateItems(d.Items) != nil {
		return internal()
	}
	return nil
}
func responseSize(v any) error {
	b, e := json.Marshal(v)
	if e != nil {
		return internal()
	}
	if len(b) > MaxSemanticResponseBytes {
		return responseExhausted()
	}
	return nil
}
func ValidateReadResponse(r ReadRequest, v ReadResponse) error {
	if err := validateNavigationReceipts(v.Receipts); err != nil {
		return err
	}
	if err := validateResponseRawBudget([]Document{v.Document}, v.Receipts); err != nil {
		return err
	}
	if v.Document.ApplicationID != r.ApplicationID {
		return internal()
	}
	if err := validateDocument(v.Document); err != nil {
		return err
	}
	return responseSize(v)
}
func ValidateResolveResponse(r ResolveRequest, v ResolveResponse) error {
	if err := validateNavigationReceipts(v.Receipts); err != nil {
		return err
	}
	if err := validateResponseRawBudget(v.Documents, v.Receipts); err != nil {
		return err
	}
	if validateResolveRequest(r) != nil || v.Documents == nil || len(v.Documents) > len(r.ApplicationIDs) {
		return internal()
	}
	allowed := map[string]bool{}
	for _, id := range r.ApplicationIDs {
		allowed[id] = true
	}
	for _, doc := range v.Documents {
		if !allowed[doc.ApplicationID] {
			return internal()
		}
		delete(allowed, doc.ApplicationID)
		if err := validateDocument(doc); err != nil {
			return err
		}
	}
	return responseSize(v)
}
func ValidateSaveResponse(r SaveRequest, v SaveResponse) error {
	if validateSaveRequest(r) != nil {
		return internal()
	}
	if err := ValidateReadResponse(ReadRequest{r.ApplicationID}, ReadResponse{v.Document, v.Receipts}); err != nil {
		return err
	}
	if !reflect.DeepEqual(v.Document.Items, r.Items) {
		return internal()
	}
	if v.Changed {
		if r.ExpectedRevision == ^uint64(0) || v.Document.Revision != r.ExpectedRevision+1 {
			return internal()
		}
	} else if v.Document.Revision != r.ExpectedRevision {
		return internal()
	}
	return responseSize(v)
}
func ValidTopLevelFailure(c common.FailureCategory) bool {
	switch c {
	case common.FailureInvalidRequest, common.FailureUnauthenticated, common.FailureForbidden, common.FailureMissing, common.FailureConflict, common.FailureCanceled, common.FailureDeadlineExceeded, common.FailureResourceExhausted, common.FailureIncompatible, common.FailureStaleState, common.FailureUnavailable, common.FailureInternal:
		return true
	}
	return false
}

// Navigation revisions describe current documents. Catalog/schema/engine
// receipts would incorrectly bind an edit to an immutable publication.
func validateNavigationReceipts(r common.Receipts) error {
	if !common.ValidReceipts(r, CapabilityName) {
		return &common.Failure{Category: common.FailureIncompatible, RequiredCapability: CapabilityName,
			SupportedMajor: common.ProtocolMajor, SupportedMinorMin: common.ProtocolMinor, SupportedMinorMax: common.ProtocolMinor}
	}
	if r.CatalogRevision != 0 || r.SchemaGeneration != 0 || r.EngineGeneration != 0 {
		return internal()
	}
	return nil
}

func responseExhausted() error {
	return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticResponse, Maximum: MaxSemanticResponseBytes}
}

// Reject aggregate oversized data before Unicode/URL validation or JSON
// serialization. Every loop is count-bounded and string accounting reads only
// lengths; subtraction prevents overflow from adversarial field sizes.
func validateResponseRawBudget(documents []Document, receipts common.Receipts) error {
	if len(documents) > MaxApplications {
		return internal()
	}
	remaining := MaxSemanticResponseBytes
	take := func(values ...string) bool {
		for _, value := range values {
			if len(value) > remaining {
				return false
			}
			remaining -= len(value)
		}
		return true
	}
	if !take(receipts.ProtocolVersion, receipts.CapabilityVersion) {
		return responseExhausted()
	}
	for _, document := range documents {
		if len(document.Items) > MaxItems {
			return internal()
		}
		if !take(document.ApplicationID) {
			return responseExhausted()
		}
		for _, item := range document.Items {
			if !take(item.ID, item.Label, item.Icon) {
				return responseExhausted()
			}
			if resource := item.Target.Resource; resource != nil && !take(resource.ResourceType, resource.ResourceID) {
				return responseExhausted()
			}
			if page := item.Target.Page; page != nil && !take(page.ID) {
				return responseExhausted()
			}
			if item.Target.URL != nil && !take(*item.Target.URL) {
				return responseExhausted()
			}
		}
	}
	return nil
}
