package notion

import (
	"bytes"
	"context"
	"encoding/json"
	"net/url"
	"strconv"

	"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{
		ActionSearch:               handleSearch,
		ActionGetPage:              handleGetPage,
		ActionCreatePage:           handleCreatePage,
		ActionUpdatePage:           handleUpdatePage,
		ActionAppendBlock:          handleAppendBlock,
		ActionRetrievePageMarkdown: handleRetrievePageMarkdown,
		ActionListUsers:            handleListUsers,
	}
}

// passthrough returns the provider response body verbatim, mirroring the
// upstream executors' `payload ?? {}`: every Action answers one JSON value
// with the provider's own keys (lists keep object/results/next_cursor/
// has_more, pages keep the Notion page object), and an empty or null body
// becomes {} instead of null. Nothing is re-wrapped or re-cased.
func passthrough(payload json.RawMessage) json.RawMessage {
	trimmed := bytes.TrimSpace(payload)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		return json.RawMessage("{}")
	}
	return json.RawMessage(trimmed)
}

// body builds a compact JSON object from key/value pairs, skipping nil values.
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 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
}

type searchInput struct {
	Query       string         `json:"query"`
	Filter      map[string]any `json:"filter"`
	Sort        map[string]any `json:"sort"`
	PageSize    *int64         `json:"pageSize"`
	StartCursor string         `json:"startCursor"`
}

func handleSearch(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed searchInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	payload, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method:  "POST",
		Path:    "/search",
		Headers: staticHeaders(),
		Body: body(
			"query", parsed.Query,
			"filter", derefObject(parsed.Filter),
			"sort", derefObject(parsed.Sort),
			"page_size", derefInt64(parsed.PageSize),
			"start_cursor", compactString(parsed.StartCursor),
		),
	})
	if err != nil {
		return nil, err
	}
	return passthrough(payload), nil
}

type pageLookupInput struct {
	PageID string `json:"pageId"`
}

func handleGetPage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed pageLookupInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.PageID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "pageId is required", nil)
	}
	page, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path:    "/pages/" + url.PathEscape(parsed.PageID),
		Headers: staticHeaders(),
	})
	if err != nil {
		return nil, err
	}
	children, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path:    "/blocks/" + url.PathEscape(parsed.PageID) + "/children",
		Headers: staticHeaders(),
	})
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{
		"page":           passthrough(page),
		"block_children": passthrough(children),
	})
}

type createPageInput struct {
	Parent     *map[string]any  `json:"parent"`
	ParentID   string           `json:"parentId"`
	Title      Nullable[string] `json:"title"`
	Properties map[string]any   `json:"properties"`
	Children   []map[string]any `json:"children"`
	Markdown   Nullable[string] `json:"markdown"`
	Icon       map[string]any   `json:"icon"`
	Cover      map[string]any   `json:"cover"`
	Template   map[string]any   `json:"template"`
}

func handleCreatePage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed createPageInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	requestBody, err := buildCreatePageBody(parsed)
	if err != nil {
		return nil, err
	}
	page, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method:  "POST",
		Path:    "/pages",
		Headers: staticHeaders(),
		Body:    requestBody,
	})
	if err != nil {
		return nil, err
	}
	return passthrough(page), nil
}

// buildCreatePageBody ports the upstream create-page body builder: an explicit
// parent object wins, a simple parentId + title pair builds a page parent and a
// title property, and markdown-only creation is allowed. A markdown string is
// forwarded even when empty (upstream compactObject keeps ""), while an absent
// or null markdown is omitted.
func buildCreatePageBody(parsed createPageInput) (map[string]any, error) {
	var children any
	if len(parsed.Children) > 0 {
		children = parsed.Children
	}
	markdown := parsed.Markdown.Compact()
	markdownFilled := parsed.Markdown.Present() && parsed.Markdown.Value != ""
	if markdownFilled && children != nil {
		return nil, runner.Errorf(
			runner.ErrorValidation, "markdown cannot be used with children", nil)
	}
	if parsed.Parent != nil {
		if parsed.Title.Present() {
			return nil, runner.Errorf(
				runner.ErrorValidation, "title cannot be used with parent; use properties instead", nil)
		}
		if parsed.ParentID != "" && !isSamePageParent(*parsed.Parent, parsed.ParentID) {
			return nil, runner.Errorf(
				runner.ErrorValidation, "parent and parentId must describe the same page parent", nil)
		}
		return body(
			"parent", *parsed.Parent,
			"properties", derefObject(parsed.Properties),
			"children", children,
			"markdown", markdown,
			"template", derefObject(parsed.Template),
			"icon", derefObject(parsed.Icon),
			"cover", derefObject(parsed.Cover),
		), nil
	}
	title := ""
	if parsed.Title.Present() {
		title = parsed.Title.Value
	}
	if parsed.ParentID == "" && !markdownFilled {
		return nil, runner.Errorf(
			runner.ErrorValidation, "parent, parentId + title, or markdown is required", nil)
	}
	if parsed.ParentID != "" && title == "" {
		return nil, runner.Errorf(
			runner.ErrorValidation, "title is required with parentId", nil)
	}
	var parent any
	if parsed.ParentID != "" {
		parent = map[string]any{"page_id": parsed.ParentID}
	}
	properties := derefObject(parsed.Properties)
	if title != "" {
		merged := make(map[string]any, len(parsed.Properties)+1)
		for name, value := range parsed.Properties {
			merged[name] = value
		}
		merged["title"] = titleProperty(title)
		properties = merged
	}
	return body(
		"parent", parent,
		"properties", properties,
		"children", children,
		"markdown", markdown,
		"template", derefObject(parsed.Template),
		"icon", derefObject(parsed.Icon),
		"cover", derefObject(parsed.Cover),
	), nil
}

// isSamePageParent reports whether an explicit parent object describes the
// same page parent as a simple parentId.
func isSamePageParent(parent map[string]any, parentID string) bool {
	pageID, ok := parent["page_id"].(string)
	return ok && pageID == parentID
}

// titleProperty builds the Notion title property from a plain string title.
func titleProperty(title string) map[string]any {
	return map[string]any{
		"title": []any{
			map[string]any{
				"type": "text",
				"text": map[string]any{"content": title},
			},
		},
	}
}

// updatePageInput decodes the update patch. Title and the boolean flags use
// Nullable so absent, explicit null, and value stay distinct: upstream guards
// each with a typeof check that forwards real values (including "" and false)
// and drops null exactly like absent.
type updatePageInput struct {
	PageID       string           `json:"pageId"`
	Title        Nullable[string] `json:"title"`
	Properties   map[string]any   `json:"properties"`
	Icon         map[string]any   `json:"icon"`
	Cover        map[string]any   `json:"cover"`
	Template     map[string]any   `json:"template"`
	InTrash      Nullable[bool]   `json:"in_trash"`
	IsLocked     Nullable[bool]   `json:"is_locked"`
	EraseContent Nullable[bool]   `json:"erase_content"`
}

func handleUpdatePage(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed updatePageInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.PageID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "pageId is required", nil)
	}
	properties := derefObject(parsed.Properties)
	if parsed.Title.Present() {
		merged := make(map[string]any, len(parsed.Properties)+1)
		for name, value := range parsed.Properties {
			merged[name] = value
		}
		merged["title"] = titleProperty(parsed.Title.Value)
		properties = merged
	}
	page, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method:  "PATCH",
		Path:    "/pages/" + url.PathEscape(parsed.PageID),
		Headers: staticHeaders(),
		Body: body(
			"properties", properties,
			"icon", derefObject(parsed.Icon),
			"cover", derefObject(parsed.Cover),
			"template", derefObject(parsed.Template),
			"in_trash", parsed.InTrash.Compact(),
			"is_locked", parsed.IsLocked.Compact(),
			"erase_content", parsed.EraseContent.Compact(),
		),
	})
	if err != nil {
		return nil, err
	}
	return passthrough(page), nil
}

type appendBlockInput struct {
	PageID string `json:"pageId"`
	Text   string `json:"text"`
}

func handleAppendBlock(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed appendBlockInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.PageID == "" || parsed.Text == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "pageId and text are required", nil)
	}
	payload, err := client.Call(ctx, credentials, runner.RESTRequest{
		Method:  "PATCH",
		Path:    "/blocks/" + url.PathEscape(parsed.PageID) + "/children",
		Headers: staticHeaders(),
		Body: body(
			"children", []any{
				map[string]any{
					"object": "block",
					"type":   "paragraph",
					"paragraph": map[string]any{
						"rich_text": []any{
							map[string]any{
								"type": "text",
								"text": map[string]any{"content": parsed.Text},
							},
						},
					},
				},
			},
		),
	})
	if err != nil {
		return nil, err
	}
	return passthrough(payload), nil
}

type retrievePageMarkdownInput struct {
	PageID            string `json:"pageId"`
	IncludeTranscript *bool  `json:"includeTranscript"`
}

func handleRetrievePageMarkdown(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed retrievePageMarkdownInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	if parsed.PageID == "" {
		return nil, runner.Errorf(runner.ErrorValidation, "pageId is required", nil)
	}
	query := url.Values{}
	if parsed.IncludeTranscript != nil {
		query.Set("include_transcript", strconv.FormatBool(*parsed.IncludeTranscript))
	}
	payload, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path:    "/pages/" + url.PathEscape(parsed.PageID) + "/markdown",
		Query:   query,
		Headers: staticHeaders(),
	})
	if err != nil {
		return nil, err
	}
	return passthrough(payload), nil
}

type listUsersInput struct {
	PageSize    int    `json:"pageSize"`
	StartCursor string `json:"startCursor"`
}

func handleListUsers(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	input json.RawMessage,
) (json.RawMessage, error) {
	var parsed listUsersInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	query := url.Values{}
	setQueryInt(query, "page_size", parsed.PageSize)
	setQueryString(query, "start_cursor", parsed.StartCursor)
	payload, err := client.Call(ctx, credentials, runner.RESTRequest{
		Path:    "/users",
		Query:   query,
		Headers: staticHeaders(),
	})
	if err != nil {
		return nil, err
	}
	return passthrough(payload), nil
}

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

// derefObject returns nil for an absent object field so the body builder skips
// it; a present (even empty) object is passed through as upstream does.
func derefObject(value map[string]any) 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))
	}
}
