package tushare

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

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

// Handlers returns the Tushare dispatch table. Every handler posts one JSON
// document with the token in the body (the official HTTP interface shape),
// normalizes the table response, and projects rows through the per-action
// field map, mirroring the upstream executor exactly.
func Handlers() map[string]runner.RESTHandler {
	return map[string]runner.RESTHandler{
		ActionListStocks:           mappedHandler("stock_basic", stockBasicFields, stockBasicParams, "stocks", stockBasicFieldMap),
		ActionGetTradeCalendar:     mappedHandler("trade_cal", tradeCalendarFields, tradeCalendarParams, "calendar", tradeCalendarFieldMap),
		ActionGetDailyQuotes:       datedMappedHandler("daily", dailyQuoteFields, "quotes", dailyQuoteFieldMap),
		ActionGetDailyBasic:        datedMappedHandler("daily_basic", dailyBasicFields, "dailyBasics", dailyBasicFieldMap),
		ActionGetAdjustmentFactors: datedMappedHandler("adj_factor", adjustmentFactorFields, "adjustmentFactors", adjustmentFactorFieldMap),
		ActionGetShareholderTrades: shareholderTradesHandler,
	}
}

// tushareEnvelope is the official response envelope. code is absent on some
// routes; msg may carry a business message.
type tushareEnvelope struct {
	Code      *int            `json:"code"`
	Message   string          `json:"msg"`
	RequestID string          `json:"request_id"`
	Data      json.RawMessage `json:"data"`
}

type tableData struct {
	Fields []string            `json:"fields"`
	Items  [][]json.RawMessage `json:"items"`
}

// request builds the single POST every Tushare API uses. The token travels in
// the body; fields is omitted entirely when not requested.
func request(credentials runner.Credentials, params map[string]any, apiName, fields string) runner.RESTRequest {
	body := map[string]any{
		"api_name": apiName,
		"token":    credentials[APIKeyField],
		"params":   params,
	}
	if fields != "" {
		body["fields"] = fields
	}
	return runner.RESTRequest{
		Method: "POST",
		Path:   "/",
		Body:   body,
	}
}

func mappedHandler(apiName string, fields []string, params []paramBinding, outputKey string, fieldMap map[string]string) runner.RESTHandler {
	return func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
		parsed, err := decodeMappedInput(input, params)
		if err != nil {
			return nil, err
		}
		return mappedTableCall(ctx, client, credentials, apiName, fields, parsed, outputKey, fieldMap)
	}
}

func datedMappedHandler(apiName string, fields []string, outputKey string, fieldMap map[string]string) runner.RESTHandler {
	return func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
		var parsed datedInput
		if err := decodeInput(input, &parsed); err != nil {
			return nil, err
		}
		parsed.trimStrings()
		if err := parsed.assertBounded(); err != nil {
			return nil, err
		}
		params := map[string]any{}
		parsed.emit(params)
		return mappedTableCall(ctx, client, credentials, apiName, fields, params, outputKey, fieldMap)
	}
}

func shareholderTradesHandler(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
	var parsed shareholderTradesInput
	if err := decodeInput(input, &parsed); err != nil {
		return nil, err
	}
	parsed.trimStrings()
	if err := parsed.assertBounded(); err != nil {
		return nil, err
	}
	params := map[string]any{}
	parsed.emit(params)
	return mappedTableCall(ctx, client, credentials, "stk_holdertrade", shareholderTradeFields, params, "shareholderTrades", shareholderTradeFieldMap)
}

func mappedTableCall(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	apiName string,
	fields []string,
	params map[string]any,
	outputKey string,
	fieldMap map[string]string,
) (json.RawMessage, error) {
	envelope, table, err := callTushare(ctx, client, credentials, params, apiName, strings.Join(fields, ","))
	if err != nil {
		return nil, err
	}
	rows := projectMappedRows(table, fieldMap)
	output := map[string]any{
		"requestId": envelope.RequestID,
		"message":   envelope.nullableMessage(),
		outputKey:   rows,
	}
	return json.Marshal(output)
}

// callTushare posts one request and validates the envelope + table shape,
// failing closed on malformed 2xx bodies and classifying business codes.
func callTushare(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	params map[string]any,
	apiName,
	fields string,
) (tushareEnvelope, tableData, error) {
	req := request(credentials, params, apiName, fields)
	var payload tushareEnvelope
	if err := client.CallJSON(ctx, credentials, req, &payload); err != nil {
		return tushareEnvelope{}, tableData{}, err
	}
	if payload.Code != nil && *payload.Code != 0 {
		return tushareEnvelope{}, tableData{}, apiError(*payload.Code, payload.Message)
	}
	table, err := payload.table()
	if err != nil {
		return tushareEnvelope{}, tableData{}, err
	}
	return payload, table, nil
}

// apiError classifies Tushare business codes carried inside HTTP 200 bodies:
// 40101 token failures and 2002 permission denials are credential problems,
// 2003/2004 are the hourly/daily access caps, everything else is upstream.
func apiError(code int, message string) error {
	if strings.TrimSpace(message) == "" {
		message = fmt.Sprintf("Tushare request failed: code %d", code)
	} else {
		message = "Tushare request failed: " + message
	}
	switch code {
	case 40101, 2002:
		return runner.Errorf(runner.ErrorProviderAuthUnavailable, message, nil)
	case 2003, 2004:
		return runner.Errorf(runner.ErrorRateLimited, message, nil)
	default:
		return runner.Errorf(runner.ErrorUpstreamFailure, message, nil)
	}
}

// table decodes and validates the data.fields/data.items table, failing
// closed on malformed shapes.
func (envelope tushareEnvelope) table() (tableData, error) {
	var data struct {
		Fields []string            `json:"fields"`
		Items  [][]json.RawMessage `json:"items"`
	}
	if len(envelope.Data) == 0 {
		return tableData{}, runner.Errorf(runner.ErrorUpstreamFailure, "tushare response data must be an object", nil)
	}
	if err := json.Unmarshal(envelope.Data, &data); err != nil {
		return tableData{}, runner.Errorf(runner.ErrorUpstreamFailure, "tushare response data must be an object", nil)
	}
	if data.Fields == nil {
		return tableData{}, runner.Errorf(runner.ErrorUpstreamFailure, "tushare response data.fields must be an array", nil)
	}
	for index, field := range data.Fields {
		if field == "" {
			return tableData{}, runner.Errorf(runner.ErrorUpstreamFailure, fmt.Sprintf("tushare response data.fields[%d] must be a string", index), nil)
		}
	}
	if data.Items == nil {
		return tableData{}, runner.Errorf(runner.ErrorUpstreamFailure, "tushare response data.items must be an array", nil)
	}
	return tableData{Fields: data.Fields, Items: data.Items}, nil
}

// nullableMessage mirrors the upstream readOptionalStringOrNull: absent,
// null, and non-string values all become null; strings pass through.
func (envelope tushareEnvelope) nullableMessage() any {
	trimmed := strings.TrimSpace(envelope.Message)
	if trimmed == "" {
		return nil
	}
	return trimmed
}

func projectRows(table tableData) []map[string]json.RawMessage {
	rows := make([]map[string]json.RawMessage, 0, len(table.Items))
	for _, item := range table.Items {
		row := make(map[string]json.RawMessage, len(table.Fields))
		for index, field := range table.Fields {
			if index < len(item) {
				row[field] = item[index]
			} else {
				row[field] = json.RawMessage("null")
			}
		}
		rows = append(rows, row)
	}
	return rows
}

func projectMappedRows(table tableData, fieldMap map[string]string) []map[string]json.RawMessage {
	rows := projectRows(table)
	mapped := make([]map[string]json.RawMessage, 0, len(rows))
	for _, row := range rows {
		target := make(map[string]json.RawMessage, len(fieldMap))
		for source, destination := range fieldMap {
			if value, ok := row[source]; ok {
				target[destination] = value
			} else {
				target[destination] = json.RawMessage("null")
			}
		}
		mapped = append(mapped, target)
	}
	return mapped
}

func decodeInput(input json.RawMessage, target any) error {
	if err := json.Unmarshal(input, target); err != nil {
		return runner.Errorf(runner.ErrorValidation, "invalid input: "+err.Error(), nil)
	}
	return nil
}
