package recyclebinv1

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 SemanticResponseSize(response Response) (int, error) {
	encoded, err := json.Marshal(response)
	return len(encoded), err
}

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 !request.Scope.Valid() {
		return invalid(InvalidComponentOperation)
	}
	return nil
}

func ValidateResponse(response Response) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return incompatible()
	}
	if len(response.Items) > MaxItems {
		return exhausted(LimitRecordCount, MaxItems)
	}
	if len(response.FailedObjects) > MaxFailedObjects {
		return exhausted(LimitRecordCount, MaxFailedObjects)
	}
	seenRecords := make(map[string]struct{}, len(response.Items))
	var previousDeleted time.Time
	for index, item := range response.Items {
		deletedAt, deletedOK := parseDateTime(item.DeletedDate)
		if !validRecordID(item.RecordID) || !validIdentifier(item.ObjectID, MaxObjectIdentifierBytes) ||
			!validText(item.RecordName) || !validText(item.DeletedBy) ||
			!deletedOK || !validOptionalDateTime(item.CreatedDate) || !validOptionalDateTime(item.LastModifiedDate) {
			return internal()
		}
		if _, duplicate := seenRecords[item.RecordID]; duplicate {
			return internal()
		}
		seenRecords[item.RecordID] = struct{}{}
		if index > 0 {
			if deletedAt.After(previousDeleted) {
				return internal()
			}
		}
		previousDeleted = deletedAt
	}
	seenObjects := make(map[string]struct{}, len(response.FailedObjects))
	for _, failure := range response.FailedObjects {
		if !validIdentifier(failure.ObjectID, MaxObjectIdentifierBytes) || !failure.Code.Valid() ||
			!validText(failure.Message) {
			return internal()
		}
		if _, duplicate := seenObjects[failure.ObjectID]; duplicate {
			return internal()
		}
		seenObjects[failure.ObjectID] = struct{}{}
	}
	size, err := SemanticResponseSize(response)
	if err != nil {
		return internal()
	}
	if size > MaxSemanticResponseBytes {
		return exhausted(LimitSemanticResponse, MaxSemanticResponseBytes)
	}
	return nil
}

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

func validRecordID(value string) bool {
	return strings.TrimSpace(value) != "" && len(value) <= MaxRecordIdentifierBytes && utf8.ValidString(value)
}

func validText(value string) bool { return utf8.ValidString(value) }

func validOptionalDateTime(value string) bool { return value == "" || validDateTime(value) }

func validDateTime(value string) bool {
	_, ok := parseDateTime(value)
	return ok
}

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 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} }
