package llm

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/nexuscrm/sentry/internal/types"
)

// QwenClient calls the DashScope OpenAI-compatible chat-completions API.
// One shot per Translate call; no streaming.
type QwenClient struct {
	url    string // full URL including /chat/completions
	apiKey string
	model  string
	http   *http.Client
}

// NewQwenClient constructs a client.
// urlBase may be either the DashScope compatible-mode base URL (e.g.
// "https://dashscope.aliyuncs.com/compatible-mode/v1") or the full
// chat-completions URL (ending in "/chat/completions"). The constructor
// normalizes either form.
func NewQwenClient(urlBase, apiKey, model string, timeout time.Duration) *QwenClient {
	url := urlBase
	if !strings.HasSuffix(url, "/chat/completions") {
		url = strings.TrimRight(url, "/") + "/chat/completions"
	}
	if model == "" {
		model = "qwen-max"
	}
	return &QwenClient{
		url:    url,
		apiKey: apiKey,
		model:  model,
		http:   &http.Client{Timeout: timeout},
	}
}

type chatReq struct {
	Model       string        `json:"model"`
	Messages    []chatMessage `json:"messages"`
	Temperature float64       `json:"temperature"`
	MaxTokens   int           `json:"max_tokens"`
}

type chatMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type chatResp struct {
	Choices []struct {
		Message chatMessage `json:"message"`
	} `json:"choices"`
}

// Translate converts a structured rule result into natural-language text.
// Returns an error on any transport failure, non-2xx response, or missing
// content. The caller is responsible for falling back to FormatTemplate.
func (c *QwenClient) Translate(ctx context.Context, r types.RuleResult) (string, error) {
	user, err := buildUserPrompt(r)
	if err != nil {
		return "", err
	}
	body, err := json.Marshal(chatReq{
		Model: c.model,
		Messages: []chatMessage{
			{Role: "system", Content: systemPrompt},
			{Role: "user", Content: user},
		},
		Temperature: 0.2,
		MaxTokens:   200,
	})
	if err != nil {
		return "", fmt.Errorf("llm: marshal request: %w", err)
	}

	backoffs := []time.Duration{0, 500 * time.Millisecond, 2 * time.Second}
	var lastErr error
	for attempt, wait := range backoffs {
		if wait > 0 {
			select {
			case <-ctx.Done():
				return "", ctx.Err()
			case <-time.After(wait):
			}
		}

		req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(body))
		if err != nil {
			return "", err
		}
		req.Header.Set("Authorization", "Bearer "+c.apiKey)
		req.Header.Set("Content-Type", "application/json")

		resp, err := c.http.Do(req)
		if err != nil {
			lastErr = err
			continue
		}
		respBody, readErr := io.ReadAll(resp.Body)
		_ = resp.Body.Close()
		if readErr != nil {
			lastErr = readErr
			continue
		}

		if resp.StatusCode >= 500 {
			lastErr = fmt.Errorf("llm: dashscope 5xx status %d (attempt %d): %s", resp.StatusCode, attempt+1, truncate(string(respBody), 200))
			continue
		}
		if resp.StatusCode >= 400 {
			return "", fmt.Errorf("llm: dashscope 4xx status %d: %s", resp.StatusCode, truncate(string(respBody), 200))
		}

		var parsed chatResp
		if err := json.Unmarshal(respBody, &parsed); err != nil {
			return "", fmt.Errorf("llm: decode response: %w", err)
		}
		if len(parsed.Choices) == 0 || parsed.Choices[0].Message.Content == "" {
			return "", errors.New("llm: empty response content")
		}
		return parsed.Choices[0].Message.Content, nil
	}
	if lastErr == nil {
		lastErr = errors.New("llm: exhausted retries")
	}
	return "", lastErr
}

func truncate(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "..."
}
