package planmanage

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/workflow/plan"
)

const (
	defaultPageLimit       = 50
	maxPageLimit           = 100
	maxIdempotencyKeyBytes = 128
	MaxDescriptionBytes    = 64 << 10
	MaxLayoutBytes         = 256 << 10
)

var planKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,63}$`)

func normalizeActor(actor Actor) (Actor, error) {
	actor.Type = strings.TrimSpace(actor.Type)
	actor.ID = strings.TrimSpace(actor.ID)
	actor.MemberID = strings.TrimSpace(actor.MemberID)
	if actor.Type != ActorTypeHuman && actor.Type != ActorTypeAgent {
		return Actor{}, invalidRequest("actor type must be human or agent")
	}
	if actor.ID == "" {
		return Actor{}, invalidRequest("actor id is required")
	}
	if actor.MemberID == "" {
		return Actor{}, invalidRequest("actor member id is required")
	}
	return actor, nil
}

func normalizePlanKey(value string) (string, error) {
	value = strings.TrimSpace(value)
	if !planKeyPattern.MatchString(value) {
		return "", invalidRequest("plan key must match " + planKeyPattern.String())
	}
	return value, nil
}

func normalizeDisplayName(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", invalidRequest("display name is required")
	}
	if len([]byte(value)) > 255 {
		return "", invalidRequest("display name must not exceed 255 bytes")
	}
	return value, nil
}

func normalizeDescription(value string) (string, error) {
	value = strings.TrimSpace(value)
	if len(value) > MaxDescriptionBytes {
		return "", invalidRequest(fmt.Sprintf("description must not exceed %d bytes", MaxDescriptionBytes))
	}
	return value, nil
}

func normalizeSource(value string) (string, error) {
	if len(value) > plan.MaxSourceBytes {
		return "", invalidRequest(fmt.Sprintf("Plan source must not exceed %d bytes", plan.MaxSourceBytes))
	}
	return value, nil
}

func sourceBudgetDiagnostics(value string) []plan.Diagnostic {
	if len(value) <= plan.MaxSourceBytes {
		return nil
	}
	return []plan.Diagnostic{{
		Severity: plan.SeverityError,
		Code:     "resource.source_too_large",
		Path:     "$",
		Message:  fmt.Sprintf("PlanVersion source must not exceed %d bytes", plan.MaxSourceBytes),
	}}
}

func normalizeIdempotencyKey(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value == "" {
		return "", invalidRequest("idempotency key is required")
	}
	if len([]byte(value)) > maxIdempotencyKeyBytes {
		return "", invalidRequest(fmt.Sprintf("idempotency key must not exceed %d bytes", maxIdempotencyKeyBytes))
	}
	for _, char := range value {
		if char < 0x21 || char > 0x7e {
			return "", invalidRequest("idempotency key must contain only visible ASCII characters")
		}
	}
	return value, nil
}

func normalizeLayout(raw json.RawMessage) (json.RawMessage, error) {
	if len(raw) > MaxLayoutBytes {
		return nil, invalidRequest(fmt.Sprintf("layout must not exceed %d bytes", MaxLayoutBytes))
	}
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		return json.RawMessage("{}"), nil
	}
	var object map[string]any
	decoder := json.NewDecoder(bytes.NewReader(trimmed))
	decoder.UseNumber()
	if err := decoder.Decode(&object); err != nil || object == nil {
		return nil, invalidRequest("layout must be a JSON object")
	}
	var trailing any
	if err := decoder.Decode(&trailing); err != io.EOF {
		return nil, invalidRequest("layout must contain exactly one JSON object")
	}
	normalized, err := json.Marshal(object)
	if err != nil {
		return nil, invalidRequest("layout is not valid JSON")
	}
	if len(normalized) > MaxLayoutBytes {
		return nil, invalidRequest(fmt.Sprintf("layout must not exceed %d bytes after normalization", MaxLayoutBytes))
	}
	return normalized, nil
}

func normalizeList(input ListInput) (ListInput, error) {
	input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
	input.Cursor = strings.TrimSpace(input.Cursor)
	input.State = strings.ToLower(strings.TrimSpace(input.State))
	input.Search = strings.TrimSpace(input.Search)
	if input.WorkspaceID == "" {
		return ListInput{}, invalidRequest("workspace id is required")
	}
	switch input.State {
	case "", "active", "archived", "all":
		// valid
	default:
		return ListInput{}, invalidRequest("state must be one of active, archived, or all")
	}
	if input.Limit == 0 {
		input.Limit = defaultPageLimit
	}
	if input.Limit < 1 || input.Limit > maxPageLimit {
		return ListInput{}, invalidRequest(fmt.Sprintf("limit must be between 1 and %d", maxPageLimit))
	}
	return input, nil
}
