package auditlogv1

import (
	"context"
	"encoding/json"
	"regexp"
	"strings"
	"time"
	"unicode/utf8"

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

var identifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)

func ValidateRequest(ctx context.Context, request Request) error {
	if err := contextFailure(ctx); err != nil {
		return err
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(InvalidComponentDeadline)
	}
	if !validRequestShape(request) {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateResponse(response Response) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	if len(response.Rows) > MaxRows {
		return exhausted(LimitRecordCount, MaxRows)
	}
	seen := make(map[string]struct{}, len(response.Rows))
	var previousTime time.Time
	previousID := ""
	for index, row := range response.Rows {
		changedAt, ok := parseDateTime(row.ChangedAt)
		if !ok || !validOpaque(row.ID, MaxRecordIdentifierBytes) ||
			!validIdentifier(row.ObjectID, MaxObjectIdentifierBytes) ||
			!validOpaque(row.RecordID, MaxRecordIdentifierBytes) ||
			!validIdentifier(row.FieldID, MaxObjectIdentifierBytes) ||
			!validOpaque(row.ChangedByID, MaxRecordIdentifierBytes) ||
			!validOptional(row.OldValue) || !validOptional(row.NewValue) ||
			!validTexts(row.Action, row.ChangesJSON, row.Entrypoint, row.EntrypointDetail,
				row.RequestID, row.TraceID, row.SessionID, row.ClientIP, row.ActualUserID,
				row.Outcome, row.ErrorCode) {
			return internal()
		}
		if _, duplicate := seen[row.ID]; duplicate {
			return internal()
		}
		seen[row.ID] = struct{}{}
		if index > 0 && (changedAt.After(previousTime) ||
			(changedAt.Equal(previousTime) && row.ID >= previousID)) {
			return internal()
		}
		previousTime, previousID = changedAt, row.ID
	}
	encoded, err := json.Marshal(response)
	if err != nil {
		return internal()
	}
	if len(encoded) > MaxSemanticResponseBytes {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return nil
}

// ValidateResponseForRequest proves that a privileged audit page is closed
// over the exact record authority and page bound requested by the caller.
func ValidateResponseForRequest(request Request, response Response) error {
	if err := ValidateResponse(response); err != nil {
		return err
	}
	if !validRequestShape(request) ||
		(request.MinCatalogRevision != nil &&
			response.Receipts.CatalogRevision < *request.MinCatalogRevision) ||
		len(response.Rows) > int(request.Limit) {
		return internal()
	}
	for _, row := range response.Rows {
		if row.ObjectID != request.ObjectID || row.RecordID != request.RecordID {
			return internal()
		}
	}
	return nil
}

func validRequestShape(request Request) bool {
	return validIdentifier(request.ObjectID, MaxObjectIdentifierBytes) &&
		validOpaque(request.RecordID, MaxRecordIdentifierBytes) &&
		request.Limit > 0 && request.Limit <= MaxRows && request.Offset <= MaxOffset &&
		(request.MinCatalogRevision == nil || *request.MinCatalogRevision > 0)
}

func validIdentifier(value string, maximum int) bool {
	return value != "" && len(value) <= maximum && identifierPattern.MatchString(value)
}

func validOpaque(value string, maximum int) bool {
	return strings.TrimSpace(value) != "" && len(value) <= maximum && utf8.ValidString(value)
}

func validOptional(value *string) bool { return value == nil || utf8.ValidString(*value) }

func validTexts(values ...string) bool {
	for _, value := range values {
		if !utf8.ValidString(value) {
			return false
		}
	}
	return true
}

func parseDateTime(value string) (time.Time, bool) {
	parsed, err := time.Parse(time.RFC3339Nano, value)
	return parsed, err == nil && parsed.Location() == time.UTC && strings.HasSuffix(value, "Z")
}

func contextFailure(ctx context.Context) error {
	if ctx == nil {
		return invalid(InvalidComponentDeadline)
	}
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: FailureDeadlineExceeded}
		}
		return &Failure{Category: FailureCanceled}
	}
	return nil
}

func invalid(component InvalidComponent) *Failure {
	return &Failure{Category: FailureInvalidRequest, InvalidComponent: component}
}

func exhausted(kind LimitKind, maximum uint64) *Failure {
	return &Failure{Category: FailureResourceExhausted, LimitKind: kind, Maximum: maximum}
}

func incompatible() *Failure {
	return &Failure{Category: FailureIncompatible, RequiredCapability: CapabilityName,
		SupportedMajor: ProtocolMajor, SupportedMinorMin: ProtocolMinor, SupportedMinorMax: ProtocolMinor}
}

func internal() *Failure { return &Failure{Category: FailureInternal} }
