// Package notion registers the Notion Builtin Extension ported from
// OpenConnector (baseline 0484cac): REST v1 over Bearer token auth with the
// pinned Notion-Version header and cursor pagination.
//
// The wire contract mirrors upstream verbatim: nothing is re-wrapped and no
// field is re-cased. Request bodies are built with the upstream compactObject
// guards — absent fields are omitted, real values (including false and the
// empty string) are forwarded, and explicit nulls follow the upstream typeof
// guards, which drop them exactly like absent fields. Every Action returns
// the provider response body unchanged (`payload ?? {}` upstream, so an empty
// body answers {}): list outputs keep the Notion envelope keys (object,
// results, next_cursor, has_more), get_page is the sole aggregate ({page,
// block_children}), create_page and update_page return the page object
// itself, and retrieve_page_markdown returns the markdown payload as-is.
//
// Batch 1 ports the page/search/user core: search, get_page, create_page,
// update_page, append_block, retrieve_page_markdown, and list_users. The
// remaining upstream actions (move_page, retrieve_page, update_page_markdown,
// retrieve_page_property, retrieve_user, the block CRUD family, the database
// and data source families, query_data_source, and list_data_source_templates)
// are deferred to a later batch.
package notion

import (
	"context"
	"fmt"

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

const (
	// Domain is the Extension domain.
	Domain = "notion"
	// Revision is the implementation identity folded into the runtime digest.
	// v1 mirrors the upstream OpenConnector wire contract verbatim: output
	// envelopes and request-body null/empty semantics included.
	Revision = "notion-rest-v1"
	// BaseURL is the fixed Notion API v1 origin.
	BaseURL = "https://api.notion.com/v1"
	// APIVersionHeader is the Notion API version pin header sent on every
	// request.
	APIVersionHeader = "Notion-Version"
	// APIVersion is the pinned Notion API version upstream sends
	// (notionCoreVersion at the ported baseline).
	APIVersion = "2026-03-11"
)

// Extension composes the Notion Builtin. It fails closed when the credential
// source or provider client cannot be constructed.
func Extension(credentials runner.CredentialSource) (extension.Extension, error) {
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL: BaseURL,
		Auth:    AuthSpec(),
	})
	if err != nil {
		return extension.Extension{}, fmt.Errorf("notion: build REST client: %w", err)
	}
	executor := runner.NewRESTExecutor(Domain, client, credentials, Handlers())
	return extension.Extension{
		Definition: Definition(),
		Implementation: runner.Implementation{
			Executor:            executor,
			Revision:            Revision,
			ConnectionPolicy:    registry.ConnectionRequired,
			Auth:                AuthSpec(),
			ValidateCredentials: ValidateCredentials,
		},
		Distribution: extension.Distribution{
			AppID:          "app_notion",
			AppSlug:        "notion",
			DefaultName:    Domain,
			DefaultEnabled: true,
			IconURL:        "/app-icons/notion.svg",
		},
	}, nil
}

// AuthSpec declares the integration-secret credential contract.
func AuthSpec() runner.AuthSpec {
	return runner.AuthSpec{
		Kind: runner.AuthBearer,
		Fields: []runner.CredentialField{
			runner.APIKeyField(
				"Notion integration secret",
				"A Notion internal integration secret, sent as an Authorization Bearer header. Share the target pages or databases with the integration so it can reach them.",
			),
		},
	}
}

// staticHeaders pins the Notion API version on every request, mirroring the
// upstream notion-version header. The map is fresh per request so handlers can
// never share mutable header state.
func staticHeaders() map[string]string {
	return map[string]string{APIVersionHeader: APIVersion}
}

// ValidateCredentials live-checks the secret with one bounded /users/me request.
func ValidateCredentials(ctx context.Context, credentials runner.Credentials) error {
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL: BaseURL,
		Auth:    AuthSpec(),
	})
	if err != nil {
		return err
	}
	return validateCredentialsWith(ctx, client, credentials)
}

// validateCredentialsWith resolves the integration bot behind the secret; an
// upstream rejection propagates the classified runtime error.
func validateCredentialsWith(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
) error {
	var profile struct {
		ID string `json:"id"`
	}
	if err := client.CallJSON(ctx, credentials, runner.RESTRequest{
		Path:    "/users/me",
		Headers: staticHeaders(),
	}, &profile); err != nil {
		return err
	}
	if profile.ID == "" {
		return runner.Errorf(
			runner.ErrorProviderAuthUnavailable, "notion rejected the token", nil)
	}
	return nil
}
