package filesv1

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

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

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

func ValidateCreateHeader(header CreateHeader) error {
	if !validOpaque(header.IdempotencyKey) || !common.ValidObjectIdentifier(header.ParentObjectID) ||
		!validOpaque(header.ParentRecordID) || !validOpaque(header.FileName) ||
		!validOpaque(header.ContentType) ||
		(header.AttachmentKind != "" && !validIdentity(header.AttachmentKind)) ||
		header.ContentLength > MaxFileBytes {
		return invalid(common.InvalidComponentCommandData)
	}
	return validateSize(header, MaxSemanticRequestBytes, common.LimitSemanticRequest)
}

func ValidateCreateChunk(chunk CreateChunk) error {
	if len(chunk.Data) > MaxChunkBytes {
		return &Failure{Category: common.FailureResourceExhausted, LimitKind: common.LimitWireFrame, Maximum: 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 ValidateCreateHeader(*chunk.Header)
	}
	return nil
}

// NewCreateStreamState admits one upload against the live RPC context. Observe
// and Complete re-check this same context so cancellation cannot finalize a
// partial file.
func NewCreateStreamState(ctx context.Context) (*CreateStreamState, error) {
	if err := validateContext(ctx); err != nil {
		return nil, err
	}
	return &CreateStreamState{ctx: ctx}, nil
}

func (state *CreateStreamState) Observe(chunk CreateChunk) error {
	if state == nil || state.ctx == nil || state.complete {
		return invalid(common.InvalidComponentCommandData)
	}
	if err := validateContext(state.ctx); err != nil {
		return err
	}
	if err := ValidateCreateChunk(chunk); err != nil {
		return err
	}
	if chunk.Header != nil {
		if state.headerSeen || state.received != 0 {
			return invalid(common.InvalidComponentCommandData)
		}
		state.headerSeen = true
		state.contentLength = chunk.Header.ContentLength
		return nil
	}
	if !state.headerSeen || 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 *CreateStreamState) 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.received != state.contentLength {
		return invalid(common.InvalidComponentCommandData)
	}
	state.complete = true
	return nil
}

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

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

func ValidateCreateResponse(response CreateResponse) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) ||
		!validOpaque(response.FileID) || !validOpaque(response.FileName) ||
		!validOpaque(response.ContentType) || response.Size > MaxFileBytes {
		return internal()
	}
	return validateSize(response, MaxSemanticResponseBytes, common.LimitSemanticResponse)
}

func ValidateOpenChunk(chunk OpenChunk) error {
	if len(chunk.Data) > MaxChunkBytes || chunk.TotalSize > MaxFileBytes ||
		!validOpaque(chunk.FileName) || !validOpaque(chunk.ContentType) {
		return internal()
	}
	if chunk.Receipts != nil && !common.ValidReceipts(*chunk.Receipts, CapabilityName) {
		return internal()
	}
	return nil
}

func (state *OpenStreamState) Observe(chunk OpenChunk) error {
	if state == nil || state.complete {
		return internal()
	}
	if err := ValidateOpenChunk(chunk); err != nil {
		return err
	}
	if !state.chunkSeen {
		if chunk.Receipts == nil {
			return internal()
		}
		state.chunkSeen = true
		state.fileName = chunk.FileName
		state.contentType = chunk.ContentType
		state.totalSize = chunk.TotalSize
	} else if chunk.Receipts != nil || chunk.FileName != state.fileName ||
		chunk.ContentType != state.contentType || chunk.TotalSize != state.totalSize ||
		len(chunk.Data) == 0 {
		return internal()
	}
	if state.received > state.totalSize || uint64(len(chunk.Data)) > state.totalSize-state.received {
		return internal()
	}
	state.received += uint64(len(chunk.Data))
	return nil
}

func (state *OpenStreamState) Complete() error {
	if state == nil || state.complete || !state.chunkSeen || state.received != state.totalSize {
		return internal()
	}
	state.complete = true
	return nil
}

func ValidateDeleteResponse(response DeleteResponse) error {
	if !common.ValidReceipts(response.Receipts, CapabilityName) {
		return internal()
	}
	return nil
}

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 validIdentity(value string) bool {
	return len(value) <= MaxIdentifierBytes && identity.MatchString(value)
}

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 &Failure{Category: common.FailureResourceExhausted, LimitKind: kind, Maximum: uint64(maximum)}
	}
	return nil
}

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