// Package tushare registers the Tushare Builtin Extension: A-share market
// data (stock basics, trading calendar, daily quotes, daily valuation
// indicators, adjustment factors, and important shareholder trades) through
// the official Tushare Pro HTTP interface. Every action posts one JSON
// document to the fixed origin with the token in the request body, exactly
// like the upstream OpenConnector executor; the credential therefore stays
// connection-scoped and is injected by the handlers instead of a header.
package tushare

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 = "tushare"
	// Revision is the implementation identity folded into the runtime digest.
	Revision = "tushare-rest-v1"
	// BaseURL is the fixed Tushare Pro HTTP API origin.
	BaseURL = "https://api.tushare.pro"
	// APIKeyField is the connection credential carrying the Tushare token.
	APIKeyField = "apiKey"
)

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

// AuthSpec declares the Tushare credential contract: the personal token sent
// as the token field of every request body (the handlers inject it there,
// the way the official HTTP interface requires). The credential kind keeps
// the token connection-scoped: the runtime injects no header at all, so the
// token travels only in the POST body exactly as the official endpoint
// expects.
func AuthSpec() runner.AuthSpec {
	return runner.AuthSpec{
		Kind: runner.AuthCredential,
		Fields: []runner.CredentialField{
			{
				Name:        APIKeyField,
				DisplayName: "Tushare token",
				Description: "Tushare token used as the token field in HTTP API requests. Log in, open Personal Center > Account and TOKEN, then copy it as documented at https://tushare.pro/document/1?doc_id=39.",
				Required:    true,
				Secret:      true,
			},
		},
	}
}

// ValidateCredentials live-checks the token with one bounded trade_cal probe
// and requires a well-formed table response, mirroring the upstream
// validation request exactly.
func ValidateCredentials(ctx context.Context, credentials runner.Credentials) error {
	client, err := runner.NewRESTClient(runner.RESTOptions{
		BaseURL:          BaseURL,
		Auth:             AuthSpec(),
		StatusClassifier: ClassifyStatus,
	})
	if err != nil {
		return err
	}
	return ValidateCredentialsWithClient(ctx, client, credentials)
}

// ValidateCredentialsWithClient runs the credential probe against an injected
// client so wire tests exercise the exact validation request.
func ValidateCredentialsWithClient(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials) error {
	probe := map[string]any{
		"exchange":   "",
		"start_date": "20180901",
		"end_date":   "20180902",
	}
	var payload tushareEnvelope
	if err := client.CallJSON(ctx, credentials, request(credentials, probe, "trade_cal", "exchange,cal_date,is_open"), &payload); err != nil {
		return err
	}
	if code := payload.Code; code != nil && *code != 0 {
		return apiError(*code, payload.Message)
	}
	table, err := payload.table()
	if err != nil {
		return err
	}
	if len(table.Fields) == 0 {
		return runner.Errorf(
			runner.ErrorProviderAuthUnavailable, "tushare rejected the credentials", nil)
	}
	return nil
}

// ClassifyStatus maps Tushare HTTP responses onto the closed runtime error
// set. Business failures arrive as a non-zero code inside HTTP 200 bodies and
// are classified by the handlers instead.
func ClassifyStatus(status int, body []byte) runner.ErrorCode {
	switch status {
	case 401, 403:
		return runner.ErrorProviderAuthUnavailable
	case 404:
		return runner.ErrorNotFound
	case 429:
		return runner.ErrorRateLimited
	default:
		if status >= 500 && status < 600 {
			return runner.ErrorUpstreamFailure
		}
		if status >= 400 && status < 500 {
			return runner.ErrorValidation
		}
		return runner.ErrorUpstreamFailure
	}
}
