package financialmodelingprep

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

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

// Handlers returns the Financial Modeling Prep dispatch table. Most actions
// forward one stable-API route with mapped parameters and return the rows
// verbatim under a per-action key; quote, symbol-search, historical-price,
// and market-mover actions additionally normalize the upstream response.
func Handlers() map[string]runner.RESTHandler {
	return map[string]runner.RESTHandler{
		"search-symbols":              handleSearchSymbols,
		"search-names":                arrayRowsHandler("/search-name", searchQueryKeys, "results"),
		"search-company-screener":     screenerHandler,
		"list-directory":              directoryHandler,
		"get-quote":                   quoteHandler,
		"get-quote-short":             quoteShortHandler,
		"get-asset-quote":             assetQuoteHandler,
		"get-historical-prices":       handleHistoricalPrices,
		"get-intraday-prices":         intradayHandler,
		"get-company-profile":         profileHandler,
		"get-company-profile-by-cik":  arrayRowsHandler("/profile-cik", []string{"cik"}, "profiles"),
		"get-company-peers":           arrayRowsHandler("/stock-peers", []string{"symbol"}, "peers"),
		"get-company-executives":      arrayRowsHandler("/key-executives", []string{"symbol"}, "executives"),
		"get-company-notes":           arrayRowsHandler("/company-notes", []string{"symbol"}, "notes"),
		"get-market-cap":              arrayRowsHandler("/market-capitalization", []string{"symbol"}, "rows"),
		"get-shares-float":            arrayRowsHandler("/shares-float", []string{"symbol"}, "rows"),
		"get-income-statement":        statementHandler("/income-statement"),
		"get-balance-sheet-statement": statementHandler("/balance-sheet-statement"),
		"get-cash-flow-statement":     statementHandler("/cash-flow-statement"),
		"get-financial-statement-growth": endpointRowsHandler(
			"statementType", statementGrowthEndpoints, []string{"symbol", "period", "limit"}, "rows"),
		"get-financial-ratios":  arrayRowsHandler("/ratios", statementQueryKeys, "ratios"),
		"get-key-metrics":       arrayRowsHandler("/key-metrics", statementQueryKeys, "metrics"),
		"get-financial-scores":  arrayRowsHandler("/financial-scores", []string{"symbol"}, "scores"),
		"get-enterprise-values": arrayRowsHandler("/enterprise-values", statementQueryKeys, "values"),
		"get-dcf":               arrayRowsHandler("/discounted-cash-flow", []string{"symbol"}, "valuations"),
		"get-market-movers":     moversHandler,
		"get-market-performance": endpointRowsHandler(
			"type", performanceEndpoints, nil, "rows"),
		"get-news":                newsHandler,
		"get-calendar":            calendarHandler,
		"get-economic-indicators": indicatorsHandler,
		"get-analyst-estimates":   estimatesHandler,
		"get-ratings": endpointRowsHandler(
			"type", ratingsEndpoints, []string{"symbol", "limit"}, "ratings"),
		"get-insider-trades":       insiderTradesHandler,
		"get-congressional-trades": congressionalHandler,
		"get-sec-filings":          secFilingsHandler,
		"get-etf-holdings":         arrayRowsHandler("/etf/holdings", []string{"symbol"}, "holdings"),
		"get-technical-indicator":  technicalIndicatorHandler,
	}
}

var (
	searchQueryKeys    = []string{"query", "limit", "exchange"}
	statementQueryKeys = []string{"symbol", "period", "limit"}

	statementGrowthEndpoints = map[string]string{
		"income": "/income-statement-growth", "balance_sheet": "/balance-sheet-statement-growth",
		"cash_flow": "/cash-flow-statement-growth", "financial": "/financial-statement-growth",
	}
	performanceEndpoints = map[string]string{
		"sector_performance":   "/sector-performance-snapshot",
		"industry_performance": "/industry-performance-snapshot",
		"sector_pe":            "/sector-pe-snapshot", "industry_pe": "/industry-pe-snapshot",
	}
	newsLatestEndpoints = map[string]string{
		"general": "/news/general-latest", "press_releases": "/news/press-releases-latest",
		"stock": "/news/stock-latest", "crypto": "/news/crypto-latest", "forex": "/news/forex-latest",
	}
	newsSearchEndpoints = map[string]string{
		"general": "/news/general-latest", "press_releases": "/news/press-releases",
		"stock": "/news/stock", "crypto": "/news/crypto", "forex": "/news/forex",
	}
	calendarEndpoints = map[string]string{
		"earnings": "/earnings-calendar", "dividends": "/dividends-calendar",
		"splits": "/splits-calendar", "ipos": "/ipos-calendar", "economic": "/economic-calendar",
	}
	ratingsEndpoints = map[string]string{
		"ratings_snapshot": "/ratings-snapshot", "ratings_historical": "/ratings-historical",
		"grades": "/grades", "grades_historical": "/grades-historical",
		"grades_summary": "/grades-summary", "grades_consensus": "/grades-consensus",
	}
	directoryEndpoints = map[string]string{
		"stocks": "/stock-list", "financial_symbols": "/financial-symbols-list",
		"financial_statement_symbols": "/financial-statement-symbol-list", "cik": "/cik-list",
		"symbol_changes": "/symbol-changes-list", "etfs": "/etf-list",
		"actively_trading": "/actively-trading-list", "exchanges": "/available-exchanges",
		"sectors": "/available-sectors", "industries": "/available-industries",
		"countries": "/available-countries",
	}
	assetQuoteEndpoints = map[string]string{
		"commodity": "/batch-commodity-quotes", "crypto": "/batch-crypto-quotes",
		"forex": "/batch-forex-quotes", "index": "/batch-index-quotes",
	}
	intradayEndpoints = map[string]string{
		"1min": "/historical-chart/1min", "5min": "/historical-chart/5min",
		"15min": "/historical-chart/15min", "30min": "/historical-chart/30min",
		"1hour": "/historical-chart/1hour", "4hour": "/historical-chart/4hour",
	}
)

// ---- shared action shapes ----

// arrayRowsHandler forwards one fixed route with the listed input keys as
// query parameters and returns the rows under outputKey.
func arrayRowsHandler(path string, keys []string, outputKey string) runner.RESTHandler {
	return func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
		parsed, err := decodeInput(input)
		if err != nil {
			return nil, err
		}
		query, err := parsed.pickQuery(keys)
		if err != nil {
			return nil, err
		}
		return arrayRows(ctx, client, credentials, path, query, outputKey)
	}
}

// endpointRowsHandler maps one enumerated input value onto its route.
func endpointRowsHandler(selector string, endpoints map[string]string, keys []string, outputKey string) runner.RESTHandler {
	return func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
		parsed, err := decodeInput(input)
		if err != nil {
			return nil, err
		}
		value, err := parsed.requiredString(selector)
		if err != nil {
			return nil, err
		}
		path, ok := endpoints[value]
		if !ok {
			return nil, runner.Errorf(runner.ErrorValidation, selector+" is not supported", nil)
		}
		query, err := parsed.pickQuery(keys)
		if err != nil {
			return nil, err
		}
		return arrayRows(ctx, client, credentials, path, query, outputKey)
	}
}

func statementHandler(path string) runner.RESTHandler {
	return func(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, input json.RawMessage) (json.RawMessage, error) {
		parsed, err := decodeInput(input)
		if err != nil {
			return nil, err
		}
		query, err := parsed.pickQuery(statementQueryKeys)
		if err != nil {
			return nil, err
		}
		payload, err := fmpGet(ctx, client, credentials, path, query)
		if err != nil {
			return nil, err
		}
		rows, err := requireArray(payload)
		if err != nil {
			return nil, err
		}
		return json.Marshal(map[string]any{"statements": rows})
	}
}

func arrayRows(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, path string, query map[string]string, outputKey string) (json.RawMessage, error) {
	payload, err := fmpGet(ctx, client, credentials, path, query)
	if err != nil {
		return nil, err
	}
	rows, err := requireArray(payload)
	if err != nil {
		return nil, err
	}
	return json.Marshal(map[string]any{outputKey: rows})
}

// fetchQuote normalizes the /quote row for both get-quote and the credential
// probe.
func fetchQuote(ctx context.Context, client *runner.RESTClient, credentials runner.Credentials, symbol string) (map[string]any, error) {
	payload, err := fmpGet(ctx, client, credentials, "/quote", map[string]string{"symbol": symbol})
	if err != nil {
		return nil, err
	}
	rows, err := requireArray(payload)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, "payload must contain a quote", nil)
	}
	first, ok := rows[0].(map[string]any)
	if !ok {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, "payload[0] must be an object", nil)
	}
	return normalizeQuote(first)
}

// ---- input decoding ----

type actionInput struct {
	raw map[string]json.RawMessage
}

func decodeInput(input json.RawMessage) (actionInput, error) {
	parsed := actionInput{raw: map[string]json.RawMessage{}}
	if len(input) == 0 {
		return parsed, nil
	}
	if err := json.Unmarshal(input, &parsed.raw); err != nil {
		return actionInput{}, runner.Errorf(runner.ErrorValidation, "invalid input: "+err.Error(), nil)
	}
	return parsed, nil
}

func (input actionInput) requiredString(name string) (string, error) {
	value, err := input.optionalString(name)
	if err != nil {
		return "", err
	}
	if value == "" {
		return "", runner.Errorf(runner.ErrorValidation, name+" is required", nil)
	}
	return value, nil
}

func (input actionInput) optionalString(name string) (string, error) {
	raw, ok := input.raw[name]
	if !ok || len(raw) == 0 || string(raw) == "null" {
		return "", nil
	}
	var value string
	if err := json.Unmarshal(raw, &value); err != nil {
		return "", runner.Errorf(runner.ErrorValidation, name+" must be a string", nil)
	}
	return strings.TrimSpace(value), nil
}

var integerQueryFields = map[string]struct{}{
	"limit": {}, "marketCapMoreThan": {}, "marketCapLowerThan": {},
	"volumeMoreThan": {}, "volumeLowerThan": {}, "page": {},
	"periodLength": {},
}

// pickQuery collects the listed keys, trims strings, renders schema integer
// values as plain decimal query text, and stringifies booleans. Missing and
// empty values are omitted.
func (input actionInput) pickQuery(keys []string) (map[string]string, error) {
	query := map[string]string{}
	for _, name := range keys {
		raw, ok := input.raw[name]
		if !ok || len(raw) == 0 || string(raw) == "null" {
			continue
		}
		var text string
		if err := json.Unmarshal(raw, &text); err == nil {
			if strings.TrimSpace(text) != "" {
				query[name] = strings.TrimSpace(text)
			}
			continue
		}
		var number json.Number
		if err := json.Unmarshal(raw, &number); err == nil {
			if _, isInteger := integerQueryFields[name]; isInteger {
				formatted, err := runner.FormatIntegerQueryValue(number)
				if err != nil {
					return nil, err
				}
				query[name] = formatted
			} else {
				query[name] = number.String()
			}
			continue
		}
		var boolean bool
		if err := json.Unmarshal(raw, &boolean); err == nil {
			query[name] = strconv.FormatBool(boolean)
			continue
		}
		return nil, runner.Errorf(
			runner.ErrorValidation, name+" must be a string, number, or boolean", nil)
	}
	return query, nil
}

// ---- payload validation helpers (fail closed on malformed 2xx bodies) ----

func requireArray(payload any) ([]any, error) {
	rows, ok := payload.([]any)
	if !ok {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, "payload must be an array", nil)
	}
	return rows, nil
}

func requireObject(value any, field string) (map[string]any, error) {
	record, ok := value.(map[string]any)
	if !ok {
		return nil, runner.Errorf(runner.ErrorUpstreamFailure, field+" must be an object", nil)
	}
	return record, nil
}

func requireString(record map[string]any, key string) (string, error) {
	value, ok := record[key].(string)
	if !ok || strings.TrimSpace(value) == "" {
		return "", runner.Errorf(runner.ErrorUpstreamFailure, key+" must be a string", nil)
	}
	return value, nil
}
