package todoist

import (
	"context"
	"encoding/json"
	"fmt"
	"math/big"
	"net/url"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension/runner"
)

// Handlers returns the Action dispatch table. The keys must exactly match the
// Definition catalog; bootstrap verifies the coverage.
func Handlers() map[string]runner.RESTHandler {
	return map[string]runner.RESTHandler{
		ActionGetCurrentUser: handleGetCurrentUser,
		ActionListProjects:   handleListProjects,
		ActionGetProject:     handleGetProject,
		ActionCreateProject:  handleCreateProject,
		ActionUpdateProject:  handleUpdateProject,
		ActionListSections:   handleListSections,
		ActionGetSection:     handleGetSection,
		ActionCreateSection:  handleCreateSection,
		ActionUpdateSection:  handleUpdateSection,
		ActionListTasks:      handleListTasks,
		ActionGetTask:        handleGetTask,
		ActionCreateTask:     handleCreateTask,
		ActionUpdateTask:     handleUpdateTask,
		ActionCloseTask:      handleCloseTask,
		ActionListComments:   handleListComments,
		ActionGetComment:     handleGetComment,
		ActionCreateComment:  handleCreateComment,
		ActionUpdateComment:  handleUpdateComment,
		ActionListLabels:     handleListLabels,
	}
}

// cursorPage is the Todoist v1 pagination envelope.
type cursorPage struct {
	Results    []json.RawMessage `json:"results"`
	NextCursor *string           `json:"next_cursor"`
}

// pageEnvelope re-wraps one page under the action's results key.
func pageEnvelope(key string, page cursorPage) (json.RawMessage, error) {
	if page.Results == nil {
		page.Results = []json.RawMessage{}
	}
	encoded, err := json.Marshal(map[string]any{
		key:          page.Results,
		"nextCursor": page.NextCursor,
	})
	if err != nil {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, "provider page was not encodable", err)
	}
	return encoded, nil
}

// body builds a compact JSON object from key/value pairs, skipping nil values
// and empty raw JSON (absent nullable fields).
func body(pairs ...any) map[string]any {
	object := make(map[string]any, len(pairs)/2)
	for index := 0; index+1 < len(pairs); index += 2 {
		key, ok := pairs[index].(string)
		if !ok {
			continue
		}
		value := pairs[index+1]
		if raw, isRaw := value.(json.RawMessage); isRaw {
			if len(raw) == 0 {
				continue
			}
			object[key] = raw
			continue
		}
		if value == nil {
			continue
		}
		object[key] = value
	}
	return object
}

func decodeInput(input json.RawMessage, target any) error {
	if len(input) == 0 {
		return nil
	}
	if err := json.Unmarshal(input, target); err != nil {
		return runner.Errorf(runner.ErrorValidation, "action input was not decodable", err)
	}
	return nil
}

func handleGetCurrentUser(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	_ json.RawMessage,
) (json.RawMessage, error) {
	user, err := client.Call(ctx, credentials, runner.RESTRequest{Path: "/user"})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"user": user})
}

type listProjectsInput struct {
	FolderID    *integerLike `json:"folderId"`
	WorkspaceID *integerLike `json:"workspaceId"`
	Cursor      string       `json:"cursor"`
	Limit       int          `json:"limit"`
}

func handleListProjects(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listProjectsInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryIntegerLike(query, "folder_id", parsed.FolderID)
	setQueryIntegerLike(query, "workspace_id", parsed.WorkspaceID)
	setQueryString(query, "cursor", parsed.Cursor)
	setQueryInt(query, "limit", parsed.Limit)
	var page cursorPage
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path: "/projects", Query: query,
	}, &page); err != nil {
		return nil, err
	}
	return pageEnvelope("projects", page)
}

type projectLookupInput struct {
	ProjectID string `json:"projectId"`
}

func handleGetProject(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed projectLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.ProjectID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "projectId is required", nil)
	}
	project, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path: "/projects/" + url.PathEscape(parsed.ProjectID),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"project": project})
}

// createProjectInput decodes parentId as raw JSON so the three states stay
// distinct, matching the upstream ternary: absent is omitted, explicit null is
// forwarded as parent_id:null, and a value is forwarded verbatim.
type createProjectInput struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	ParentID    json.RawMessage `json:"parentId"`
	Color       any             `json:"color"`
	IsFavorite  *bool           `json:"isFavorite"`
	ViewStyle   string          `json:"viewStyle"`
	WorkspaceID *integerLike    `json:"workspaceId"`
}

func handleCreateProject(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed createProjectInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.Name == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "name is required", nil)
	}
	var workspaceID any
	if parsed.WorkspaceID != nil {
		workspaceID = int64(*parsed.WorkspaceID)
	}
	project, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/projects",
		Body: body(
			"name", parsed.Name,
			"description", compactString(parsed.Description),
			"parent_id", parsed.ParentID,
			"color", parsed.Color,
			"is_favorite", derefBool(parsed.IsFavorite),
			"view_style", compactString(parsed.ViewStyle),
			"workspace_id", workspaceID,
		),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"project": project})
}

// updateProjectInput decodes nullable fields as raw JSON so an explicit null
// (clear the value) is forwarded verbatim while an absent field is omitted,
// matching the upstream Todoist update contract.
type updateProjectInput struct {
	ProjectID   string          `json:"projectId"`
	Name        json.RawMessage `json:"name"`
	Description json.RawMessage `json:"description"`
	Color       json.RawMessage `json:"color"`
	IsFavorite  json.RawMessage `json:"isFavorite"`
	ViewStyle   json.RawMessage `json:"viewStyle"`
	ChildOrder  json.RawMessage `json:"childOrder"`
	IsCollapsed json.RawMessage `json:"isCollapsed"`
	FolderID    json.RawMessage `json:"folderId"`
}

func handleUpdateProject(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed updateProjectInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.ProjectID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "projectId is required", nil)
	}
	project, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/projects/" + url.PathEscape(parsed.ProjectID),
		Body: body(
			"name", parsed.Name,
			"description", parsed.Description,
			"color", parsed.Color,
			"is_favorite", parsed.IsFavorite,
			"view_style", parsed.ViewStyle,
			"child_order", parsed.ChildOrder,
			"is_collapsed", parsed.IsCollapsed,
			"folder_id", parsed.FolderID,
		),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"project": project})
}

type listSectionsInput struct {
	ProjectID string `json:"projectId"`
	Cursor    string `json:"cursor"`
	Limit     int    `json:"limit"`
}

func handleListSections(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listSectionsInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryString(query, "project_id", parsed.ProjectID)
	setQueryString(query, "cursor", parsed.Cursor)
	setQueryInt(query, "limit", parsed.Limit)
	var page cursorPage
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path: "/sections", Query: query,
	}, &page); err != nil {
		return nil, err
	}
	return pageEnvelope("sections", page)
}

type sectionLookupInput struct {
	SectionID string `json:"sectionId"`
}

func handleGetSection(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed sectionLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.SectionID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "sectionId is required", nil)
	}
	section, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path: "/sections/" + url.PathEscape(parsed.SectionID),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"section": section})
}

type createSectionInput struct {
	ProjectID string `json:"projectId"`
	Name      string `json:"name"`
	Order     *int64 `json:"order"`
}

func handleCreateSection(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed createSectionInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.ProjectID == "" || parsed.Name == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "projectId and name are required", nil)
	}
	section, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/sections",
		Body: body(
			"name", parsed.Name,
			"project_id", parsed.ProjectID,
			"order", derefInt64(parsed.Order),
		),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"section": section})
}

type updateSectionInput struct {
	SectionID    string          `json:"sectionId"`
	Name         json.RawMessage `json:"name"`
	SectionOrder json.RawMessage `json:"sectionOrder"`
	IsCollapsed  json.RawMessage `json:"isCollapsed"`
}

func handleUpdateSection(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed updateSectionInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.SectionID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "sectionId is required", nil)
	}
	section, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/sections/" + url.PathEscape(parsed.SectionID),
		Body: body(
			"name", parsed.Name,
			"section_order", parsed.SectionOrder,
			"is_collapsed", parsed.IsCollapsed,
		),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"section": section})
}

type listTasksInput struct {
	ProjectID string   `json:"projectId"`
	SectionID string   `json:"sectionId"`
	ParentID  string   `json:"parentId"`
	Label     string   `json:"label"`
	IDs       []string `json:"ids"`
	GoalID    string   `json:"goalId"`
	Cursor    string   `json:"cursor"`
	Limit     int      `json:"limit"`
}

func handleListTasks(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listTasksInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryString(query, "project_id", parsed.ProjectID)
	setQueryString(query, "section_id", parsed.SectionID)
	setQueryString(query, "parent_id", parsed.ParentID)
	setQueryString(query, "label", parsed.Label)
	if len(parsed.IDs) > 0 {
		query.Set("ids", strings.Join(parsed.IDs, ","))
	}
	setQueryString(query, "goal_id", parsed.GoalID)
	setQueryString(query, "cursor", parsed.Cursor)
	setQueryInt(query, "limit", parsed.Limit)
	var page cursorPage
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path: "/tasks", Query: query,
	}, &page); err != nil {
		return nil, err
	}
	return pageEnvelope("tasks", page)
}

type taskLookupInput struct {
	TaskID string `json:"taskId"`
}

func handleGetTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed taskLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.TaskID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "taskId is required", nil)
	}
	task, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path: "/tasks/" + url.PathEscape(parsed.TaskID),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"task": task})
}

type taskBodyInput struct {
	Content      string          `json:"content"`
	Description  json.RawMessage `json:"description"`
	ProjectID    string          `json:"projectId"`
	SectionID    string          `json:"sectionId"`
	ParentID     string          `json:"parentId"`
	Order        *int64          `json:"order"`
	Labels       []string        `json:"labels"`
	Priority     *int64          `json:"priority"`
	AssigneeID   json.RawMessage `json:"assigneeId"`
	DueString    string          `json:"dueString"`
	DueDate      string          `json:"dueDate"`
	DueDatetime  string          `json:"dueDatetime"`
	DueLang      string          `json:"dueLang"`
	Duration     *int64          `json:"duration"`
	DurationUnit string          `json:"durationUnit"`
	DeadlineDate string          `json:"deadlineDate"`
	ChildOrder   *int64          `json:"childOrder"`
	IsCollapsed  *bool           `json:"isCollapsed"`
	DayOrder     *int64          `json:"dayOrder"`
}

func (parsed *taskBodyInput) requestBody(requireContent bool) map[string]any {
	if requireContent && parsed.Content == "" {
		return nil
	}
	content := any(parsed.Content)
	if !requireContent && parsed.Content == "" {
		// Baseline omits content on update when absent or empty
		// (optionalString), so a description-only update cannot clear the
		// title by accident.
		content = nil
	}
	var labels any
	if len(parsed.Labels) > 0 {
		labels = parsed.Labels
	}
	return body(
		"content", content,
		"description", parsed.Description,
		"project_id", compactString(parsed.ProjectID),
		"section_id", compactString(parsed.SectionID),
		"parent_id", compactString(parsed.ParentID),
		"order", derefInt64(parsed.Order),
		"labels", labels,
		"priority", derefInt64(parsed.Priority),
		"assignee_id", parsed.AssigneeID,
		"due_string", compactString(parsed.DueString),
		"due_date", compactString(parsed.DueDate),
		"due_datetime", compactString(parsed.DueDatetime),
		"due_lang", compactString(parsed.DueLang),
		"duration", derefInt64(parsed.Duration),
		"duration_unit", compactString(parsed.DurationUnit),
		"deadline_date", compactString(parsed.DeadlineDate),
		"child_order", derefInt64(parsed.ChildOrder),
		"is_collapsed", derefBool(parsed.IsCollapsed),
		"day_order", derefInt64(parsed.DayOrder),
	)
}

func handleCreateTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed taskBodyInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	requestBody := parsed.requestBody(true)
	if requestBody == nil {
		return nil, runner.Errorf(runner.ErrorValidation, "content is required", nil)
	}
	task, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/tasks",
		Body:   requestBody,
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"task": task})
}

type updateTaskInput struct {
	taskBodyInput
	TaskID string `json:"taskId"`
}

func handleUpdateTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed updateTaskInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.TaskID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "taskId is required", nil)
	}
	task, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/tasks/" + url.PathEscape(parsed.TaskID),
		Body:   parsed.requestBody(false),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"task": task})
}

func handleCloseTask(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed taskLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.TaskID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "taskId is required", nil)
	}
	if _, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/tasks/" + url.PathEscape(parsed.TaskID) + "/close",
	}); err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"success": true})
}

type listCommentsInput struct {
	TaskID    string `json:"taskId"`
	ProjectID string `json:"projectId"`
	Cursor    string `json:"cursor"`
	Limit     int    `json:"limit"`
}

// commentsPage is the comment pagination envelope with raw passthrough items.
type commentsPage struct {
	Results    []json.RawMessage `json:"results"`
	NextCursor *string           `json:"next_cursor"`
}

func handleListComments(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listCommentsInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryString(query, "task_id", parsed.TaskID)
	setQueryString(query, "project_id", parsed.ProjectID)
	setQueryString(query, "cursor", parsed.Cursor)
	setQueryInt(query, "limit", parsed.Limit)
	var page commentsPage
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path: "/comments", Query: query,
	}, &page); err != nil {
		return nil, err
	}
	normalized := make([]json.RawMessage, 0, len(page.Results))
	for _, comment := range page.Results {
		normalized = append(normalized, normalizeComment(comment))
	}
	return json.Marshal(map[string]any{
		"comments":   normalized,
		"nextCursor": page.NextCursor,
	})
}

type commentLookupInput struct {
	CommentID string `json:"commentId"`
}

func handleGetComment(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed commentLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.CommentID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "commentId is required", nil)
	}
	comment, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path: "/comments/" + url.PathEscape(parsed.CommentID),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"comment": normalizeComment(comment)})
}

type createCommentInput struct {
	Content      string             `json:"content"`
	TaskID       string             `json:"taskId"`
	ProjectID    string             `json:"projectId"`
	Attachment   *commentAttachment `json:"attachment"`
	UIDsToNotify []int64            `json:"uidsToNotify"`
}

type commentAttachment struct {
	FileURL      string `json:"fileUrl"`
	FileName     string `json:"fileName"`
	FileType     string `json:"fileType"`
	ResourceType string `json:"resourceType"`
}

func handleCreateComment(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed createCommentInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.Content == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "content is required", nil)
	}
	var attachment any
	if parsed.Attachment != nil {
		attachment = body(
			"file_url", compactString(parsed.Attachment.FileURL),
			"file_name", compactString(parsed.Attachment.FileName),
			"file_type", compactString(parsed.Attachment.FileType),
			"resource_type", compactString(parsed.Attachment.ResourceType),
		)
	}
	var notify []any
	for _, id := range parsed.UIDsToNotify {
		notify = append(notify, id)
	}
	var uidsToNotify any
	if len(notify) > 0 {
		uidsToNotify = notify
	}
	comment, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/comments",
		Body: body(
			"content", parsed.Content,
			"task_id", compactString(parsed.TaskID),
			"project_id", compactString(parsed.ProjectID),
			"attachment", attachment,
			"uids_to_notify", uidsToNotify,
		),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"comment": normalizeComment(comment)})
}

type updateCommentInput struct {
	CommentID string `json:"commentId"`
	Content   string `json:"content"`
}

func handleUpdateComment(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed updateCommentInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.CommentID == "" || parsed.Content == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "commentId and content are required", nil)
	}
	comment, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method: "POST",
		Path:   "/comments/" + url.PathEscape(parsed.CommentID),
		Body:   body("content", parsed.Content),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{"comment": normalizeComment(comment)})
}

type listLabelsInput struct {
	Cursor string `json:"cursor"`
	Limit  int    `json:"limit"`
}

func handleListLabels(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listLabelsInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryString(query, "cursor", parsed.Cursor)
	setQueryInt(query, "limit", parsed.Limit)
	var page cursorPage
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path: "/labels", Query: query,
	}, &page); err != nil {
		return nil, err
	}
	return pageEnvelope("labels", page)
}

// normalizeComment mirrors the upstream Todoist comment normalization: a
// valid attachment object passes through; otherwise the file_attachment
// object backs it; with no valid object on either side the attachment key is
// removed, exactly like the upstream optionalRecord ?? optionalRecord chain
// feeding compactObject.
func normalizeComment(comment json.RawMessage) json.RawMessage {
	if len(comment) == 0 || !json.Valid(comment) {
		return comment
	}
	var decoded map[string]json.RawMessage
	if err := json.Unmarshal(comment, &decoded); err != nil {
		return comment
	}
	if attachment, hasAttachment := decoded["attachment"]; hasAttachment && isJSONObject(attachment) {
		return comment
	}
	// Baseline: attachment = optionalRecord(attachment) ?? optionalRecord(file_attachment)
	// and compactObject drops an undefined result, so a null/scalar attachment
	// with no valid object fallback REMOVES the key instead of passing it
	// through.
	if fileAttachment, hasFile := decoded["file_attachment"]; hasFile && isJSONObject(fileAttachment) {
		decoded["attachment"] = fileAttachment
	} else {
		delete(decoded, "attachment")
	}
	encoded, err := json.Marshal(decoded)
	if err != nil {
		return comment
	}
	return encoded
}

// isJSONObject reports whether raw JSON is a non-null object, mirroring the
// upstream optionalRecord guard that falls back to file_attachment.
func isJSONObject(raw json.RawMessage) bool {
	if len(raw) == 0 {
		return false
	}
	trimmed := raw
	for len(trimmed) > 0 && (trimmed[0] == ' ' || trimmed[0] == '\t' || trimmed[0] == '\n' || trimmed[0] == '\r') {
		trimmed = trimmed[1:]
	}
	return len(trimmed) > 0 && trimmed[0] == '{'
}

// integerLike decodes the schema's integer-or-digit-string contract exactly:
// any JSON number whose numeric value is a non-negative integer within int64
// (including exponent and trailing-zero lexicals like 1e3 or 1.0), or a JSON
// string of 1-18 digits after proper unescaping. The Definition schema and
// the handler accept and reject the same values.
type integerLike int64

func (value *integerLike) UnmarshalJSON(raw []byte) error {
	decoder := json.NewDecoder(strings.NewReader(string(raw)))
	decoder.UseNumber()
	var decoded any
	if err := decoder.Decode(&decoded); err != nil {
		return fmt.Errorf("integer-like value was not valid JSON: %w", err)
	}
	switch typed := decoded.(type) {
	case json.Number:
		parsed, ok := exactInt64(typed.String())
		if !ok {
			return fmt.Errorf("integer-like value %q is not a lossless non-negative int64", typed.String())
		}
		*value = integerLike(parsed)
		return nil
	case string:
		if !validDigits(typed) {
			return fmt.Errorf("integer-like string %q is not a lossless integer", typed)
		}
		parsed, err := strconv.ParseInt(typed, 10, 64)
		if err != nil {
			return fmt.Errorf("integer-like string %q is out of range", typed)
		}
		*value = integerLike(parsed)
		return nil
	default:
		return fmt.Errorf("integer-like value must be an integer or a digit string")
	}
}

// exactInt64 reports the int64 value of a decimal lexical when it denotes an
// exact, in-range, non-negative integer. big.Rat covers exponent and
// fractional lexicals precisely, so 1e3 and 1.0 are accepted while 1.5 and
// out-of-range magnitudes are rejected.
func exactInt64(lexical string) (int64, bool) {
	rational, ok := new(big.Rat).SetString(lexical)
	if !ok || !rational.IsInt() {
		return 0, false
	}
	numerator := rational.Num()
	if numerator.Sign() < 0 || !numerator.IsInt64() {
		return 0, false
	}
	return numerator.Int64(), true
}

func validDigits(value string) bool {
	if len(value) == 0 || len(value) > 18 {
		return false
	}
	for index := 0; index < len(value); index++ {
		if value[index] < '0' || value[index] > '9' {
			return false
		}
	}
	return true
}

func setQueryIntegerLike(query url.Values, key string, value *integerLike) {
	if value != nil {
		query.Set(key, strconv.FormatInt(int64(*value), 10))
	}
}

func compactString(value string) any {
	if value == "" {
		return nil
	}
	return value
}

func derefBool(value *bool) any {
	if value == nil {
		return nil
	}
	return *value
}

func derefInt64(value *int64) any {
	if value == nil {
		return nil
	}
	return *value
}

func setQueryString(query url.Values, key string, value string) {
	if value != "" {
		query.Set(key, value)
	}
}

func setQueryInt(query url.Values, key string, value int) {
	if value > 0 {
		query.Set(key, strconv.Itoa(value))
	}
}
