package qichacha

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

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

// actionPathByName maps every catalog action onto its OpenAPI route.
var actionPathByName = map[string]string{
	ActionListCompanyShareholders:          shareholdersPath,
	ActionListCompanyHistoricalInvestments: historicalInvestmentsPath,
}

// 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{
		ActionListCompanyShareholders:          handleListRecords(ActionListCompanyShareholders),
		ActionListCompanyHistoricalInvestments: handleListRecords(ActionListCompanyHistoricalInvestments),
	}
}

type searchInput struct {
	SearchKey string       `json:"searchKey"`
	PageIndex *json.Number `json:"pageIndex"`
	PageSize  *json.Number `json:"pageSize"`
}

// credentialQuery merges the operation parameters with the AppKey identity
// parameter every Qichacha request carries. The credential probe and the
// action handlers share this one construction path so their wires cannot
// drift apart.
func credentialQuery(credentials runner.Credentials, params url.Values) url.Values {
	query := url.Values{"key": []string{strings.TrimSpace(credentials["appKey"])}}
	for name, values := range params {
		query[name] = values
	}
	return query
}

func handleListRecords(action string) runner.RESTHandler {
	outputKey := "shareholders"
	if action == ActionListCompanyHistoricalInvestments {
		outputKey = "historicalInvestments"
	}
	return func(
		ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage,
	) (json.RawMessage, error) {
		var parsed searchInput
		if len(input) > 0 {
			if err := json.Unmarshal(input, &parsed); err != nil {
				return nil, runner.Errorf(runner.ErrorValidation, "action input was not decodable", err)
			}
		}
		// The pinned optionalString normalization: leading and trailing
		// whitespace drop, internal spacing is preserved.
		parsed.SearchKey = strings.TrimSpace(parsed.SearchKey)
		if err := runner.FormatIntegerQueryValuesInPlace(parsed.PageIndex, parsed.PageSize); err != nil {
			return nil, err
		}
		pageIndex, pageSize := json.Number("1"), json.Number("10")
		if parsed.PageIndex != nil {
			pageIndex = *parsed.PageIndex
		}
		if parsed.PageSize != nil {
			pageSize = *parsed.PageSize
		}
		query := credentialQuery(credentials, url.Values{
			"searchKey": []string{parsed.SearchKey},
			"pageIndex": []string{pageIndex.String()},
			"pageSize":  []string{pageSize.String()},
		})
		raw, err := client.Call(ctx, credentials, runner.RESTRequest{
			Method: http.MethodGet,
			Path:   actionPathByName[action],
			Query:  query,
		})
		if err != nil {
			return nil, err
		}
		return normalizeListResponse(raw, outputKey, pageIndex, pageSize)
	}
}

// listEnvelope is the Pascal-case Qichacha response envelope.
type listEnvelope struct {
	Status      string          `json:"Status"`
	Message     string          `json:"Message"`
	OrderNumber *string         `json:"OrderNumber"`
	Result      json.RawMessage `json:"Result"`
	Paging      *pagingEnvelope `json:"Paging"`
}

type pagingEnvelope struct {
	PageIndex    *json.Number `json:"PageIndex"`
	PageSize     *json.Number `json:"PageSize"`
	TotalRecords *json.Number `json:"TotalRecords"`
}

// resultEnvelope is the object shape of Result: Data records plus the
// VerifyResult flag.
type resultEnvelope struct {
	VerifyResult *json.Number      `json:"VerifyResult"`
	Data         []json.RawMessage `json:"Data"`
}

// normalizeListResponse applies the upstream normalization: business status
// first, then the Result/Data records, the paging echo, and the
// per-action record mapping.
func normalizeListResponse(
	raw json.RawMessage, outputKey string, pageIndex, pageSize json.Number,
) (json.RawMessage, error) {
	var envelope listEnvelope
	if err := json.Unmarshal(raw, &envelope); err != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "qichacha returned an invalid response", nil)
	}
	if envelope.Status != "200" {
		return nil, businessError(envelope.Message)
	}
	records, resultObject := decodeResult(envelope.Result)
	normalized := make([]map[string]any, 0, len(records))
	for _, record := range records {
		if !isObjectRecord(record) {
			continue
		}
		if outputKey == "shareholders" {
			normalized = append(normalized, normalizeShareholder(record))
		} else {
			normalized = append(normalized, normalizeHistoricalInvestment(record))
		}
	}
	output := map[string]any{
		outputKey:      normalized,
		"pageIndex":    pageIndex,
		"pageSize":     pageSize,
		"totalRecords": json.Number("0"),
		"verifyResult": nil,
		"orderNumber":  nil,
	}
	if envelope.Paging != nil {
		if envelope.Paging.PageIndex != nil {
			formatted, err := formatResponseInteger(envelope.Paging.PageIndex, "PageIndex", 1)
			if err != nil {
				return nil, err
			}
			output["pageIndex"] = formatted
		}
		if envelope.Paging.PageSize != nil {
			formatted, err := formatResponseInteger(envelope.Paging.PageSize, "PageSize", 1)
			if err != nil {
				return nil, err
			}
			output["pageSize"] = formatted
		}
		if envelope.Paging.TotalRecords != nil {
			formatted, err := formatResponseInteger(envelope.Paging.TotalRecords, "TotalRecords", 0)
			if err != nil {
				return nil, err
			}
			output["totalRecords"] = formatted
		}
	} else {
		output["totalRecords"] = json.Number(strconv.Itoa(len(records)))
	}
	if resultObject != nil && resultObject.VerifyResult != nil {
		formatted, err := formatResponseInteger(resultObject.VerifyResult, "VerifyResult", 0, 1)
		if err != nil {
			return nil, err
		}
		output["verifyResult"] = formatted
	}
	if envelope.OrderNumber != nil {
		output["orderNumber"] = *envelope.OrderNumber
	}
	encoded, err := json.Marshal(output)
	if err != nil {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, "action output was not encodable", err)
	}
	return encoded, nil
}

// formatResponseInteger accepts an exact JSON-number integer from Qichacha,
// enforces the output schema range, and returns plain decimal JSON. It is
// intentionally response-specific rather than reusing the query formatter:
// a provider payload violation is upstream_failure, not input validation.
func formatResponseInteger(
	value *json.Number, field string, minimum int64, maximum ...int64,
) (json.Number, error) {
	if value == nil {
		return "", runner.Errorf(
			runner.ErrorUpstreamFailure, "qichacha returned invalid "+field, nil)
	}
	var lexical json.Number
	if err := json.Unmarshal([]byte(value.String()), &lexical); err != nil {
		return "", runner.Errorf(
			runner.ErrorUpstreamFailure, "qichacha returned invalid "+field, nil)
	}
	var rational big.Rat
	if _, ok := rational.SetString(lexical.String()); !ok || !rational.IsInt() {
		return "", runner.Errorf(
			runner.ErrorUpstreamFailure, "qichacha returned invalid "+field, nil)
	}
	integer := rational.Num()
	if integer.Cmp(big.NewInt(minimum)) < 0 ||
		(len(maximum) == 1 && integer.Cmp(big.NewInt(maximum[0])) > 0) {
		return "", runner.Errorf(
			runner.ErrorUpstreamFailure, "qichacha returned out-of-range "+field, nil)
	}
	return json.Number(integer.String()), nil
}

// decodeResult splits the Result member: an array is the record list, an
// object carries Data records plus VerifyResult.
func decodeResult(result json.RawMessage) ([]json.RawMessage, *resultEnvelope) {
	if len(result) == 0 || string(result) == "null" {
		return []json.RawMessage{}, nil
	}
	var object resultEnvelope
	if err := json.Unmarshal(result, &object); err == nil && object.Data != nil {
		return object.Data, &object
	}
	var records []json.RawMessage
	if err := json.Unmarshal(result, &records); err == nil && records != nil {
		return records, nil
	}
	return []json.RawMessage{}, nil
}

// businessError classifies a Status!=200 envelope by its message markers,
// mirroring the upstream classifier.
func businessError(message string) error {
	normalized := strings.ToLower(message)
	for _, marker := range []string{"appkey", "secretkey", "token", "签名", "密钥", "验证"} {
		if strings.Contains(normalized, marker) {
			return runner.Errorf(
				runner.ErrorProviderAuthUnavailable, businessMessage(message), nil)
		}
	}
	for _, marker := range []string{"频率", "限流", "too many"} {
		if strings.Contains(normalized, marker) {
			return runner.Errorf(
				runner.ErrorRateLimited, businessMessage(message), nil)
		}
	}
	return runner.Errorf(runner.ErrorUpstreamFailure, businessMessage(message), nil)
}

func businessMessage(message string) string {
	if message == "" {
		return "qichacha rejected the request"
	}
	return message
}

// ClassifyStatus maps Qichacha responses onto the closed runtime error set.
// The message markers refine HTTP failures first; unrecognized statuses keep
// the shared default table.
func ClassifyStatus(status int, body []byte) runner.ErrorCode {
	if len(body) > 0 {
		var envelope struct {
			Message string `json:"Message"`
		}
		if err := json.Unmarshal(body, &envelope); err == nil {
			normalized := strings.ToLower(envelope.Message)
			for _, marker := range []string{"appkey", "secretkey", "token", "签名", "密钥", "验证"} {
				if strings.Contains(normalized, marker) {
					return runner.ErrorProviderAuthUnavailable
				}
			}
			for _, marker := range []string{"频率", "限流", "too many"} {
				if strings.Contains(normalized, marker) {
					return runner.ErrorRateLimited
				}
			}
		}
	}
	switch {
	case status == http.StatusUnauthorized || status == http.StatusForbidden:
		return runner.ErrorProviderAuthUnavailable
	case status == http.StatusNotFound:
		return runner.ErrorNotFound
	case status == http.StatusTooManyRequests:
		return runner.ErrorRateLimited
	case status >= 400 && status < 500:
		return runner.ErrorValidation
	default:
		return runner.ErrorUpstreamFailure
	}
}
