package bulkingestv1

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

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

func ValidateCreateRequest(ctx context.Context, request CreateRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if len(request.ObjectID) > common.MaxObjectIdentifierBytes {
		return exhausted(common.LimitObjectID, common.MaxObjectIdentifierBytes)
	}
	if !common.ValidObjectIdentifier(request.ObjectID) {
		return invalid(common.InvalidComponentObjectID)
	}
	if !request.Operation.Valid() {
		return invalid(common.InvalidComponentOperation)
	}
	if !validClientVersion(request.ClientVersion) {
		return invalid(common.InvalidComponentCommandData)
	}
	if request.Operation == OperationUpsert {
		if !common.ValidFieldIdentifier(request.ExternalIDFieldName) {
			return invalid(common.InvalidComponentFieldValue)
		}
	} else if request.ExternalIDFieldName != "" {
		return invalid(common.InvalidComponentFieldValue)
	}
	return validateSize(request, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateReadRequest(ctx context.Context, request ReadRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if !validOpaque(request.JobID) {
		return invalid(common.InvalidComponentRecordID)
	}
	return nil
}

func ValidateCloseRequest(ctx context.Context, request CloseRequest) error {
	if err := validateContext(ctx); err != nil {
		return err
	}
	if !validOpaque(request.JobID) ||
		(request.State != StateUploadComplete && request.State != StateAborted) {
		return invalid(common.InvalidComponentOperation)
	}
	return nil
}

func ValidateUploadHeader(header UploadHeader) error {
	if !validOpaque(header.JobID) {
		return invalid(common.InvalidComponentRecordID)
	}
	if !header.ContentLengthKnown && header.ContentLength != 0 {
		return invalid(common.InvalidComponentCommandData)
	}
	if header.ContentLengthKnown && header.ContentLength > MaxUploadBytes {
		return exhausted(common.LimitUploadBytes, MaxUploadBytes)
	}
	return validateSize(header, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateUploadChunk(chunk UploadChunk) error {
	if len(chunk.Data) > MaxChunkBytes {
		return exhausted(common.LimitWireFrame, MaxChunkBytes)
	}
	if (chunk.Header == nil && len(chunk.Data) == 0) ||
		(chunk.Header != nil && len(chunk.Data) != 0) {
		return invalid(common.InvalidComponentCommandData)
	}
	if chunk.Header != nil {
		return ValidateUploadHeader(*chunk.Header)
	}
	return nil
}

func NewUploadStreamState(ctx context.Context) (*UploadStreamState, error) {
	if err := validateContext(ctx); err != nil {
		return nil, err
	}
	return &UploadStreamState{ctx: ctx}, nil
}

func (state *UploadStreamState) Observe(chunk UploadChunk) error {
	if state == nil || state.ctx == nil || state.complete {
		return invalid(common.InvalidComponentCommandData)
	}
	if err := validateContext(state.ctx); err != nil {
		return err
	}
	if err := ValidateUploadChunk(chunk); err != nil {
		return err
	}
	if chunk.Header != nil {
		if state.headerSeen || state.received != 0 {
			return invalid(common.InvalidComponentCommandData)
		}
		state.headerSeen = true
		state.lengthKnown = chunk.Header.ContentLengthKnown
		state.contentLength = chunk.Header.ContentLength
		return nil
	}
	if !state.headerSeen {
		return invalid(common.InvalidComponentCommandData)
	}
	if state.received > MaxUploadBytes || uint64(len(chunk.Data)) > MaxUploadBytes-state.received {
		return exhausted(common.LimitUploadBytes, MaxUploadBytes)
	}
	if state.lengthKnown && (state.received > state.contentLength ||
		uint64(len(chunk.Data)) > state.contentLength-state.received) {
		return invalid(common.InvalidComponentCommandData)
	}
	state.received += uint64(len(chunk.Data))
	return nil
}

func (state *UploadStreamState) Complete() error {
	if state == nil || state.ctx == nil || state.complete {
		return invalid(common.InvalidComponentCommandData)
	}
	if err := validateContext(state.ctx); err != nil {
		return err
	}
	if !state.headerSeen || (state.lengthKnown && state.received != state.contentLength) {
		return invalid(common.InvalidComponentCommandData)
	}
	state.complete = true
	return nil
}

func ValidateCreateResponse(request CreateRequest, response CreateResponse) error {
	if response.Job.ObjectID != request.ObjectID || response.Job.Operation != request.Operation ||
		response.Job.ExternalIDFieldName != request.ExternalIDFieldName ||
		response.Job.ClientVersion != request.ClientVersion || response.Job.State != StateOpen {
		return internal()
	}
	return validateJobResponse(response.Job, response.Receipts, response)
}

func ValidateReadResponse(request ReadRequest, response ReadResponse) error {
	if response.Job.ID != request.JobID {
		return internal()
	}
	return validateJobResponse(response.Job, response.Receipts, response)
}

func ValidateCloseResponse(request CloseRequest, response CloseResponse) error {
	if response.Job.ID != request.JobID || response.Job.State != request.State {
		return internal()
	}
	return validateJobResponse(response.Job, response.Receipts, response)
}

func ValidateUploadResponse(request UploadHeader, response UploadResponse) error {
	if response.JobID != request.JobID || !common.ValidReceipts(response.Receipts, CapabilityName) {
		return internal()
	}
	return nil
}

func validateJobResponse(job Job, receipts Receipts, response any) error {
	if !common.ValidReceipts(receipts, CapabilityName) || !validOpaque(job.ID) ||
		!common.ValidObjectIdentifier(job.ObjectID) || !job.Operation.Valid() ||
		!job.State.Valid() || !validOpaque(job.CreatedByID) ||
		!validTimestamp(job.CreatedAt) || !validTimestamp(job.ModifiedAt) ||
		!validClientVersion(job.ClientVersion) ||
		!validJobFailure(job.State, job.FailureCode) ||
		(job.Operation == OperationUpsert) != (job.ExternalIDFieldName != "") ||
		(job.ExternalIDFieldName != "" && !common.ValidFieldIdentifier(job.ExternalIDFieldName)) {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func validClientVersion(value float64) bool {
	return value >= 0 && !math.IsInf(value, 0) && !math.IsNaN(value)
}

func validJobFailure(state State, code FailureCode) bool {
	if state == StateFailed {
		return code.Valid()
	}
	return code == ""
}

func validateContext(ctx context.Context) error {
	if ctx == nil {
		return invalid(common.InvalidComponentDeadline)
	}
	if err := ctx.Err(); err != nil {
		if err == context.DeadlineExceeded {
			return &Failure{Category: common.FailureDeadlineExceeded}
		}
		return &Failure{Category: common.FailureCanceled}
	}
	if _, ok := ctx.Deadline(); !ok {
		return invalid(common.InvalidComponentDeadline)
	}
	return nil
}

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

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

func validateSize(value any, maximum int, kind common.LimitKind) error {
	encoded, err := json.Marshal(value)
	if err != nil {
		return internal()
	}
	if len(encoded) > maximum {
		return exhausted(kind, uint64(maximum))
	}
	return nil
}

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

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

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