package analyticsauthoringv1

import (
	"context"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"

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

var commandIdentifierPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:@-]*$`)

func ValidateRequest(ctx context.Context, request Request) error {
	if err := common.ValidateRequestContext(ctx); err != nil {
		return err
	}
	return validateRequestShape(request)
}

func validateRequestShape(request Request) error {
	if !validCommandIdentifier(request.CommandID) {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandID}
	}
	if !request.Operation.Valid() {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentOperation}
	}
	if request.MinCatalogRevision != nil && *request.MinCatalogRevision == 0 {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
	}
	if request.ResourceID != "" && !validResourceIdentifier(request.ResourceID) {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentRecordID}
	}
	if request.Operation.Creates() && request.ResourceID != "" || !request.Operation.Creates() && request.ResourceID == "" {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentRecordID}
	}
	if request.Operation.Deletes() {
		if request.Dashboard != nil || request.Report != nil || request.Folder != nil {
			return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
		}
		return nil
	}
	definitions := 0
	for _, present := range []bool{request.Dashboard != nil, request.Report != nil, request.Folder != nil} {
		if present {
			definitions++
		}
	}
	if definitions != 1 {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
	}
	if request.Dashboard != nil && request.Dashboard.Components != nil && len(*request.Dashboard.Components) > MaxDashboardComponents {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitOperationCount, Maximum: MaxDashboardComponents}
	}
	if !reportDefinitionWithinEntryLimit(request.Report) {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitOperationCount, Maximum: MaxReportDefinitionEntries}
	}
	// Enforce the raw authored-data budget before parsing component JSON or
	// marshaling the canonical definition. The semantic contract is callable
	// in-process, so transport frame limits are not its admission boundary.
	if !definitionWithinRawBudget(request.Dashboard, request.Report, request.Folder) {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticRequest, Maximum: uint64(MaxDefinitionBytes)}
	}
	switch request.Operation {
	case CreateDashboard, UpdateDashboard:
		if request.Dashboard == nil {
			return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
		}
		if err := validateDashboard(request.Operation, request.Dashboard); err != nil {
			return err
		}
	case CreateReport, UpdateReport:
		if request.Report == nil {
			return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
		}
		if err := validateReport(request.Operation, request.Report); err != nil {
			return err
		}
	case CreateFolder, UpdateFolder:
		if request.Folder == nil {
			return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
		}
		if err := validateFolder(request.Operation, request.Folder); err != nil {
			return err
		}
		// Direct self-parenting can be rejected without consulting hierarchy
		// state. The transaction-owning Runtime engine remains responsible for
		// detecting indirect descendant cycles under lock.
		if request.Operation == UpdateFolder && request.Folder.ParentFolderID != nil &&
			*request.Folder.ParentFolderID == request.ResourceID {
			return errInvalidDefinition
		}
	}
	definition, err := canonicalDefinition(request)
	if err != nil {
		return &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}
	}
	if len(definition) > MaxDefinitionBytes {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitSemanticRequest, Maximum: uint64(MaxDefinitionBytes)}
	}
	return nil
}

func reportDefinitionWithinEntryLimit(report *ReportDefinition) bool {
	if report == nil {
		return true
	}
	remaining := MaxReportDefinitionEntries
	return takeCollectionEntries(&remaining, report.Columns) &&
		takeCollectionEntries(&remaining, report.Filters) &&
		takeCollectionEntries(&remaining, report.Grouping)
}

func takeCollectionEntries[T any](remaining *int, values *[]T) bool {
	if values == nil {
		return true
	}
	if remaining == nil || len(*values) > *remaining {
		return false
	}
	*remaining -= len(*values)
	return true
}

func definitionWithinRawBudget(dashboard *DashboardDefinition, report *ReportDefinition, folder *FolderDefinition) bool {
	remaining := MaxDefinitionBytes
	take := func(value string) bool {
		if len(value) > remaining {
			return false
		}
		remaining -= len(value)
		return true
	}
	takeOptional := func(value *string) bool { return value == nil || take(*value) }
	if dashboard != nil {
		if !takeOptional(dashboard.Name) || !takeOptional(dashboard.StableName) || !takeOptional(dashboard.FolderID) ||
			!takeOptional(dashboard.Description) || !takeOptional(dashboard.LayoutKind) || !takeOptional(dashboard.Kind) {
			return false
		}
		if dashboard.Components != nil {
			for _, component := range *dashboard.Components {
				if !take(component.ReportID) || !take(component.Kind) || !take(component.Title) || !take(component.Configuration) {
					return false
				}
			}
		}
	}
	if report != nil {
		if !takeOptional(report.Name) || !takeOptional(report.StableName) || !takeOptional(report.FolderID) ||
			!takeOptional(report.Description) || !takeOptional(report.SourceObject) || !takeOptional(report.SourceRef) ||
			!takeOptional(report.DisplayMode) || !takeOptional(report.Scope) {
			return false
		}
		if report.Columns != nil {
			for _, value := range *report.Columns {
				if !take(value) {
					return false
				}
			}
		}
		if report.Filters != nil {
			for _, value := range *report.Filters {
				if !take(value.Column) || !take(value.Operator) || !take(value.Value) {
					return false
				}
			}
		}
		if report.Grouping != nil {
			for _, value := range *report.Grouping {
				if !take(value) {
					return false
				}
			}
		}
		if report.DateFilter != nil && (!take(report.DateFilter.Column) || !take(report.DateFilter.DurationValue) || !take(report.DateFilter.StartDate) || !take(report.DateFilter.EndDate)) {
			return false
		}
	}
	if folder != nil && (!takeOptional(folder.Name) || !takeOptional(folder.StableName) || !takeOptional(folder.Description) ||
		!takeOptional(folder.Kind) || !takeOptional(folder.ParentFolderID)) {
		return false
	}
	return true
}

func validateDashboard(operation Operation, definition *DashboardDefinition) error {
	if definition == nil {
		return errInvalidDefinition
	}
	if operation == CreateDashboard &&
		(!validRequiredText(definition.Name) || !validRequiredText(definition.StableName) ||
			!validRequiredText(definition.FolderID) || !validRequiredText(definition.LayoutKind)) {
		return errInvalidDefinition
	}
	if operation == UpdateDashboard && definition.Name == nil && definition.StableName == nil &&
		definition.FolderID == nil && definition.Description == nil && definition.LayoutKind == nil &&
		definition.Kind == nil && definition.Components == nil {
		return errInvalidDefinition
	}
	for _, value := range []*string{definition.Name, definition.StableName, definition.FolderID, definition.LayoutKind} {
		if value != nil && !validRequiredText(value) {
			return errInvalidDefinition
		}
	}
	if definition.Description != nil && !validSafeText(*definition.Description, true) {
		return errInvalidDefinition
	}
	if definition.Kind != nil && !validOptionalString(*definition.Kind) {
		return errInvalidDefinition
	}
	if definition.Components == nil {
		return nil
	}
	if len(*definition.Components) > MaxDashboardComponents {
		return &common.Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitOperationCount, Maximum: MaxDashboardComponents}
	}
	for _, component := range *definition.Components {
		if !validRequiredString(component.Kind) || !validOptionalString(component.ReportID) || !validSafeText(component.Title, false) {
			return errInvalidDefinition
		}
		if component.Configuration != "" {
			canonical, err := CanonicalDefinitionJSON([]byte(component.Configuration))
			if err != nil || string(canonical) != component.Configuration {
				return errInvalidDefinition
			}
		}
	}
	return nil
}

func validateReport(operation Operation, definition *ReportDefinition) error {
	if definition == nil {
		return errInvalidDefinition
	}
	if operation == CreateReport &&
		(!validRequiredText(definition.Name) || !validRequiredText(definition.StableName) ||
			!validRequiredText(definition.FolderID) || !validRequiredText(definition.SourceObject) ||
			definition.ClearDateFilter) {
		return errInvalidDefinition
	}
	if operation == UpdateReport && definition.Name == nil && definition.StableName == nil &&
		definition.FolderID == nil && definition.Description == nil && definition.SourceObject == nil &&
		definition.SourceRef == nil && definition.DisplayMode == nil && definition.Scope == nil &&
		definition.Columns == nil && definition.Filters == nil && definition.Grouping == nil && definition.DateFilter == nil &&
		!definition.ClearDateFilter {
		return errInvalidDefinition
	}
	for _, value := range []*string{definition.Name, definition.StableName, definition.SourceObject, definition.SourceRef, definition.DisplayMode, definition.Scope} {
		if value != nil && !validRequiredText(value) {
			return errInvalidDefinition
		}
	}
	if definition.FolderID != nil && !validResourceIdentifier(*definition.FolderID) {
		return errInvalidDefinition
	}
	if definition.Description != nil && !validSafeText(*definition.Description, true) {
		return errInvalidDefinition
	}
	if definition.Columns != nil {
		for _, value := range *definition.Columns {
			if !validRequiredString(value) {
				return errInvalidDefinition
			}
		}
	}
	if definition.Filters != nil {
		for _, value := range *definition.Filters {
			if !validRequiredString(value.Column) || !validRequiredString(value.Operator) || !validSafeText(value.Value, false) {
				return errInvalidDefinition
			}
		}
	}
	if definition.Grouping != nil {
		for _, value := range *definition.Grouping {
			if !validRequiredString(value) {
				return errInvalidDefinition
			}
		}
	}
	if definition.DateFilter != nil && definition.ClearDateFilter {
		return errInvalidDefinition
	}
	if value := definition.DateFilter; value != nil &&
		(!validRequiredString(value.Column) || !validOptionalString(value.DurationValue) ||
			!validOptionalString(value.StartDate) || !validOptionalString(value.EndDate)) {
		return errInvalidDefinition
	}
	return nil
}

func validateFolder(operation Operation, definition *FolderDefinition) error {
	if definition == nil {
		return errInvalidDefinition
	}
	if operation == CreateFolder &&
		(!validRequiredText(definition.Name) || !validRequiredText(definition.StableName) ||
			!validRequiredText(definition.Kind) || definition.Access == nil || !definition.Access.Valid()) {
		return errInvalidDefinition
	}
	if operation == UpdateFolder && definition.Name == nil && definition.StableName == nil &&
		definition.Description == nil && definition.Kind == nil && definition.Access == nil && definition.ParentFolderID == nil {
		return errInvalidDefinition
	}
	for _, value := range []*string{definition.Name, definition.StableName, definition.Kind} {
		if value != nil && !validRequiredText(value) {
			return errInvalidDefinition
		}
	}
	if definition.Description != nil && !validSafeText(*definition.Description, true) {
		return errInvalidDefinition
	}
	if definition.ParentFolderID != nil && *definition.ParentFolderID != "" && !validResourceIdentifier(*definition.ParentFolderID) {
		return errInvalidDefinition
	}
	if definition.Access != nil && !definition.Access.Valid() {
		return errInvalidDefinition
	}
	return nil
}

var errInvalidDefinition = &common.Failure{Category: common.FailureInvalidRequest, InvalidComponent: common.InvalidComponentCommandData}

func validRequiredText(value *string) bool { return value != nil && validRequiredString(*value) }
func validRequiredString(value string) bool {
	return value != "" && len(value) <= MaxResourceIdentifierBytes && strings.TrimSpace(value) == value && validSafeText(value, false)
}
func validOptionalString(value string) bool {
	return value == "" || len(value) <= MaxResourceIdentifierBytes && strings.TrimSpace(value) == value && validSafeText(value, false)
}
func validSafeText(value string, allowLineBreaks bool) bool {
	if !utf8.ValidString(value) {
		return false
	}
	for _, character := range value {
		if character == 0 || unicode.IsControl(character) && !(allowLineBreaks && (character == '\n' || character == '\r' || character == '\t')) {
			return false
		}
	}
	return true
}

func canonicalDefinition(request Request) ([]byte, error) {
	dashboard := request.Dashboard
	if dashboard != nil && dashboard.Components != nil {
		// A present component set means replace the graph. Go can represent that
		// empty business effect as either a nil or non-nil slice behind the
		// pointer, while protobuf cannot preserve that representation detail.
		// Normalize both to [] without collapsing an absent patch to present.
		normalized := *dashboard
		components := make([]DashboardComponent, len(*dashboard.Components))
		copy(components, *dashboard.Components)
		normalized.Components = &components
		dashboard = &normalized
	}
	report := request.Report
	if report != nil && (report.Columns != nil || report.Filters != nil || report.Grouping != nil) {
		// Report collection patches have the same semantic presence rule as a
		// dashboard component replacement: absent means no patch, while present
		// nil and present empty are one explicit empty business effect. Protobuf
		// preserves wrapper presence but not the Go slice representation behind
		// that wrapper, so digest identity must normalize it here.
		normalized := *report
		if report.Columns != nil {
			columns := append([]string{}, (*report.Columns)...)
			normalized.Columns = &columns
		}
		if report.Filters != nil {
			filters := append([]ReportFilter{}, (*report.Filters)...)
			normalized.Filters = &filters
		}
		if report.Grouping != nil {
			grouping := append([]string{}, (*report.Grouping)...)
			normalized.Grouping = &grouping
		}
		report = &normalized
	}
	return json.Marshal(struct {
		Dashboard *DashboardDefinition `json:"dashboard,omitempty"`
		Report    *ReportDefinition    `json:"report,omitempty"`
		Folder    *FolderDefinition    `json:"folder,omitempty"`
	}{Dashboard: dashboard, Report: report, Folder: request.Folder})
}

func ValidateResponse(request Request, response Response) error {
	// Response validation is also the request/response correlation boundary.
	// Callers must not be able to map a locally valid response against an
	// operation or request shape that the capability would never admit.
	if err := validateRequestShape(request); err != nil {
		return &common.Failure{Category: common.FailureInternal, InvalidComponent: common.InvalidComponentResponse}
	}
	if response.CommandID != request.CommandID || !validResourceIdentifier(response.ResourceID) ||
		(!request.Operation.Creates() && response.ResourceID != request.ResourceID) {
		return &common.Failure{Category: common.FailureInternal, InvalidComponent: common.InvalidComponentResponse}
	}
	if !common.ValidReceipts(response.Receipts, CapabilityName) ||
		(request.MinCatalogRevision != nil && response.Receipts.CatalogRevision < *request.MinCatalogRevision) {
		return &common.Failure{Category: common.FailureInternal, InvalidComponent: common.InvalidComponentResponse}
	}
	return nil
}

// CommandDigest returns the contract-wide sha256:<64 lowercase hex> spelling
// over the semantic business effect. Catalog revision is fenced independently
// by receipts, while the caller-selected CommandID is the lookup key rather
// than part of its value.
func CommandDigest(request Request) (string, error) {
	if err := validateRequestShape(request); err != nil {
		return "", err
	}
	definition, err := canonicalDefinition(request)
	if err != nil {
		return "", errInvalidDefinition
	}
	hash := sha256.New()
	for _, value := range [][]byte{[]byte(request.Operation), []byte(request.ResourceID), definition} {
		var size [8]byte
		binary.BigEndian.PutUint64(size[:], uint64(len(value)))
		_, _ = hash.Write(size[:])
		_, _ = hash.Write(value)
	}
	return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
}

func validCommandIdentifier(identifier string) bool {
	return len(identifier) <= MaxCommandIdentifierBytes && utf8.ValidString(identifier) && commandIdentifierPattern.MatchString(identifier)
}

func validResourceIdentifier(identifier string) bool {
	if identifier == "" || len(identifier) > MaxResourceIdentifierBytes || !utf8.ValidString(identifier) || strings.TrimSpace(identifier) != identifier {
		return false
	}
	for _, value := range identifier {
		if unicode.IsControl(value) {
			return false
		}
	}
	return true
}
