package financialmodelingprep

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

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

// fmpGet performs one bounded GET against the stable API (the runtime
// injects the apikey query parameter) and returns the decoded JSON payload. Failures inside
// HTTP 200 bodies ({error}, {message}, {"Error Message"}) are extracted and
// classified here with the upstream marker list; an empty or non-JSON 2xx
// body fails closed like the upstream reader.
func fmpGet(
	ctx context.Context,
	client *runner.RESTClient,
	credentials runner.Credentials,
	path string,
	query map[string]string,
) (any, error) {
	values := map[string][]string{}
	for name, value := range query {
		if value != "" {
			values[name] = []string{value}
		}
	}
	response, err := client.CallResponse(ctx, credentials, runner.RESTRequest{
		Method: "GET",
		Path:   path,
		Query:  values,
		Response: runner.RESTResponseSpec{
			Mode:     runner.RESTResponseText,
			MaxBytes: maxResponseBytes,
		},
	})
	if err != nil {
		return nil, err
	}
	body := response.Body
	if len(strings.TrimSpace(string(body))) == 0 {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure, "Financial Modeling Prep returned an empty response", nil)
	}
	var parsed any
	if jsonErr := json.Unmarshal(body, &parsed); jsonErr != nil {
		return nil, runner.Errorf(
			runner.ErrorUpstreamFailure,
			"Financial Modeling Prep returned invalid JSON: "+clip(string(body)), nil)
	}
	if message, failed := objectErrorMessage(parsed); failed {
		return nil, classifyMessage(message)
	}
	return parsed, nil
}

// maxResponseBytes uses the full shared response ceiling.
const maxResponseBytes = int64(4 << 20)

// objectErrorMessage extracts the stable API error keys: error, message, and
// "Error Message" all mark a failed call.
func objectErrorMessage(payload any) (string, bool) {
	record, ok := payload.(map[string]any)
	if !ok {
		return "", false
	}
	for _, key := range []string{"error", "message", "Error Message"} {
		if message, isString := record[key].(string); isString && strings.TrimSpace(message) != "" {
			return message, true
		}
	}
	return "", false
}

// bodyErrorMessage extracts the same keys for the HTTP-level classifier.
func bodyErrorMessage(body []byte) (string, bool) {
	var parsed any
	if err := json.Unmarshal(body, &parsed); err != nil {
		return strings.TrimSpace(string(body)), strings.TrimSpace(string(body)) != ""
	}
	return objectErrorMessage(parsed)
}

// classifySpecificMessage maps an extracted message onto the closed set when
// it matches one of the upstream marker families; the boolean reports
// whether a specific marker matched.
func classifySpecificMessage(message string) (runner.ErrorCode, bool) {
	lowered := strings.ToLower(message)
	switch {
	case strings.Contains(lowered, "rate limit"), strings.Contains(lowered, "limit exceeded"):
		return runner.ErrorRateLimited, true
	case strings.Contains(lowered, "restricted endpoint"), strings.Contains(lowered, "upgrade your plan"):
		return runner.ErrorValidation, true
	case strings.Contains(lowered, "api key"), strings.Contains(lowered, "apikey"),
		strings.Contains(lowered, "unauthorized"), strings.Contains(lowered, "invalid key"):
		return runner.ErrorProviderAuthUnavailable, true
	default:
		return "", false
	}
}

// classifyMessage classifies a body error on a 2xx response: specific
// markers first, then the upstream default of an upstream failure (its
// executor maps unmatched 200-body errors to a 502-equivalent).
func classifyMessage(message string) error {
	if code, marked := classifySpecificMessage(message); marked {
		return runner.Errorf(code, message, nil)
	}
	return runner.Errorf(runner.ErrorUpstreamFailure, message, nil)
}

// clip bounds an error snippet like the upstream 160-character slice.
func clip(text string) string {
	if len(text) > 160 {
		return text[:160]
	}
	return text
}
