package agent

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"nofx/logger"
	"regexp"
	"strings"
	"time"
)

// apiCallTool executes HTTP requests against the NOFX API server.
// This is the only tool available to the agent.
type apiCallTool struct {
	baseURL string
	token   string
	client  *http.Client
}

// apiRequest holds the arguments decoded from the LLM's api_request tool call.
type apiRequest struct {
	Method string         `json:"method"`
	Path   string         `json:"path"`
	Body   map[string]any `json:"body"`
}

// allowedRoute is one entry in the LLM tool allowlist. The bot agent runs with
// a real user JWT, so we MUST default-deny: any path not listed here is rejected
// before the HTTP call is made. This prevents prompt-injection (via account
// names, strategy names, etc. injected into the LLM context) from coercing the
// bot into changing the user's password, swapping exchange credentials, or
// pointing the LLM API key at an attacker-controlled URL.
type allowedRoute struct {
	method  string
	pattern *regexp.Regexp
}

// botAPIAllowlist enumerates the endpoints the Telegram LLM agent is permitted
// to call. Keep this LIST SHORT and DEFAULT-DENY. To grant the bot access to a
// new endpoint, add an explicit entry here — never widen wildcards.
//
// Explicitly NOT allowed (and must never be added without a human-in-the-loop
// confirmation flow):
//   - PUT  /api/user/password   (password takeover)
//   - PUT  /api/models          (LLM API key + endpoint swap → exfil)
//   - POST/PUT/DELETE /api/exchanges*  (exchange credential swap → drain)
//   - account recovery (password reset / account wipe) is intentionally
//     CLI-only (`nofx reset-password` / `nofx reset-account`) and has no
//     HTTP endpoint, so the bot cannot reach it
//   - POST /api/wallet/generate, /api/wallet/validate
//   - POST /api/telegram/* (rebind bot)
var botAPIAllowlist = []allowedRoute{
	// Read-only endpoints that surface state to the user.
	{"GET", regexp.MustCompile(`^/api/health$`)},
	{"GET", regexp.MustCompile(`^/api/config$`)},
	{"GET", regexp.MustCompile(`^/api/supported-models$`)},
	{"GET", regexp.MustCompile(`^/api/supported-exchanges$`)},
	{"GET", regexp.MustCompile(`^/api/models$`)},
	{"GET", regexp.MustCompile(`^/api/exchanges$`)},
	{"GET", regexp.MustCompile(`^/api/exchanges/account-state$`)},
	{"GET", regexp.MustCompile(`^/api/strategies(/[^/]+)?$`)},
	{"GET", regexp.MustCompile(`^/api/strategies/active$`)},
	{"GET", regexp.MustCompile(`^/api/strategies/default-config$`)},
	{"GET", regexp.MustCompile(`^/api/strategies/public$`)},
	{"GET", regexp.MustCompile(`^/api/my-traders$`)},
	{"GET", regexp.MustCompile(`^/api/traders$`)},
	{"GET", regexp.MustCompile(`^/api/traders/[^/]+/config$`)},
	{"GET", regexp.MustCompile(`^/api/traders/[^/]+/public-config$`)},
	{"GET", regexp.MustCompile(`^/api/traders/[^/]+/grid-risk$`)},
	{"GET", regexp.MustCompile(`^/api/competition$`)},
	{"GET", regexp.MustCompile(`^/api/top-traders$`)},
	{"GET", regexp.MustCompile(`^/api/equity-history$`)},
	{"GET", regexp.MustCompile(`^/api/klines$`)},
	{"GET", regexp.MustCompile(`^/api/symbols$`)},
	{"GET", regexp.MustCompile(`^/api/status$`)},
	{"GET", regexp.MustCompile(`^/api/account$`)},
	{"GET", regexp.MustCompile(`^/api/positions$`)},
	{"GET", regexp.MustCompile(`^/api/positions/history$`)},
	{"GET", regexp.MustCompile(`^/api/trades$`)},
	{"GET", regexp.MustCompile(`^/api/orders$`)},
	{"GET", regexp.MustCompile(`^/api/orders/[^/]+/fills$`)},
	{"GET", regexp.MustCompile(`^/api/open-orders$`)},
	{"GET", regexp.MustCompile(`^/api/decisions$`)},
	{"GET", regexp.MustCompile(`^/api/decisions/latest$`)},
	{"GET", regexp.MustCompile(`^/api/statistics$`)},
	{"GET", regexp.MustCompile(`^/api/ai-costs$`)},
	{"GET", regexp.MustCompile(`^/api/ai-costs/summary$`)},

	// Write endpoints — trader and strategy lifecycle. These let the bot create
	// traders and strategies the user has asked for, and start/stop them. NOT
	// including any endpoint that mutates credentials, passwords, or pointers
	// to external services (LLM API URL, exchange API keys, telegram binding).
	// Strategy configs are server-side-validated for risk caps in the API
	// layer, so strategy create/update here cannot escape the user's risk
	// boundary.
	{"POST", regexp.MustCompile(`^/api/traders$`)},
	{"PUT", regexp.MustCompile(`^/api/traders/[^/]+$`)},
	{"DELETE", regexp.MustCompile(`^/api/traders/[^/]+$`)},
	{"POST", regexp.MustCompile(`^/api/traders/[^/]+/start$`)},
	{"POST", regexp.MustCompile(`^/api/traders/[^/]+/stop$`)},
	{"POST", regexp.MustCompile(`^/api/traders/[^/]+/sync-balance$`)},
	{"POST", regexp.MustCompile(`^/api/traders/[^/]+/close-position$`)},
	{"PUT", regexp.MustCompile(`^/api/traders/[^/]+/prompt$`)},
	{"PUT", regexp.MustCompile(`^/api/traders/[^/]+/competition$`)},
	{"POST", regexp.MustCompile(`^/api/strategies$`)},
	{"PUT", regexp.MustCompile(`^/api/strategies/[^/]+$`)},
	{"DELETE", regexp.MustCompile(`^/api/strategies/[^/]+$`)},
	{"POST", regexp.MustCompile(`^/api/strategies/[^/]+/activate$`)},
	{"POST", regexp.MustCompile(`^/api/strategies/[^/]+/duplicate$`)},
}

// isPathAllowed returns true when the (method, path) pair is in botAPIAllowlist.
// The path argument should already be query-stripped.
func isPathAllowed(method, path string) bool {
	for _, r := range botAPIAllowlist {
		if r.method == method && r.pattern.MatchString(path) {
			return true
		}
	}
	return false
}

func newAPICallTool(port int, token string) *apiCallTool {
	return &apiCallTool{
		baseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
		token:   token,
		client:  &http.Client{Timeout: 30 * time.Second},
	}
}

// execute calls the API and returns the response as a string for LLM consumption.
func (t *apiCallTool) execute(req *apiRequest) string {
	if req.Method == "" || req.Path == "" {
		return "error: method and path are required"
	}
	if !strings.HasPrefix(req.Path, "/") {
		req.Path = "/" + req.Path
	}

	// SECURITY: default-deny allowlist enforcement. Without this, prompt
	// injection via user-controlled fields (account_name, strategy name,
	// trader name) could coerce the LLM into calling sensitive endpoints
	// like PUT /api/user/password or PUT /api/exchanges with the bot's JWT.
	method := strings.ToUpper(req.Method)
	pathOnly := req.Path
	if i := strings.IndexByte(pathOnly, '?'); i >= 0 {
		pathOnly = pathOnly[:i]
	}
	if !isPathAllowed(method, pathOnly) {
		logger.Warnf("Agent: blocked disallowed tool call %s %s (path not in botAPIAllowlist)", method, pathOnly)
		return fmt.Sprintf(
			`{"error":"endpoint not allowed for the chat agent","method":%q,"path":%q,"hint":"ask the user to perform this action in the web UI"}`,
			method, pathOnly,
		)
	}

	var bodyReader io.Reader
	if req.Method != "GET" && len(req.Body) > 0 {
		b, err := json.Marshal(req.Body)
		if err != nil {
			return fmt.Sprintf("error marshaling body: %v", err)
		}
		bodyReader = bytes.NewReader(b)
	}

	httpReq, err := http.NewRequest(req.Method, t.baseURL+req.Path, bodyReader)
	if err != nil {
		return fmt.Sprintf("error creating request: %v", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Authorization", "Bearer "+t.token)

	resp, err := t.client.Do(httpReq)
	if err != nil {
		return fmt.Sprintf("API call failed: %v", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Sprintf("error reading response: %v", err)
	}

	logger.Infof("Agent api_call: %s %s -> %d", req.Method, req.Path, resp.StatusCode)

	if resp.StatusCode >= 400 {
		return fmt.Sprintf("API error %d: %s", resp.StatusCode, string(body))
	}

	// Pretty-print JSON for better LLM readability
	var v any
	if json.Unmarshal(body, &v) == nil {
		if pretty, err := json.MarshalIndent(v, "", "  "); err == nil {
			return string(pretty)
		}
	}
	return string(body)
}
