package search

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

const defaultBaseURL = "https://api.exa.ai/search"

type Hit struct {
	Title   string `json:"title"`
	URL     string `json:"url"`
	Snippet string `json:"snippet"`
}

type Client interface {
	Search(ctx context.Context, apiKey, query string, limit int) ([]Hit, error)
}

// FakeClient for tests / no platform key.
type FakeClient struct {
	Fail bool
}

func (f *FakeClient) Search(_ context.Context, apiKey, query string, limit int) ([]Hit, error) {
	if f.Fail || apiKey == "" {
		return nil, fmt.Errorf("search failed: no key")
	}
	if limit <= 0 {
		limit = 3
	}
	var hits []Hit
	for i := 0; i < limit; i++ {
		hits = append(hits, Hit{
			Title:   fmt.Sprintf("Result %d for %s", i+1, strings.TrimSpace(query)),
			URL:     fmt.Sprintf("https://example.com/r/%d", i+1),
			Snippet: "fake exa snippet",
		})
	}
	return hits, nil
}

// ExaClient calls api.exa.ai (real web search).
type ExaClient struct {
	BaseURL string
	HTTP    *http.Client
	// IncludeDomains optional host filter (e.g. threads.net).
	IncludeDomains []string
}

// NewExa creates a ready HTTP client.
func NewExa() *ExaClient {
	return &ExaClient{
		BaseURL: defaultBaseURL,
		HTTP:    &http.Client{Timeout: 25 * time.Second},
	}
}

func (c *ExaClient) Search(ctx context.Context, apiKey, query string, limit int) ([]Hit, error) {
	apiKey = strings.TrimSpace(apiKey)
	query = strings.TrimSpace(query)
	if apiKey == "" || strings.HasPrefix(apiKey, "fake") {
		return nil, fmt.Errorf("search failed: no key")
	}
	if query == "" {
		return nil, fmt.Errorf("search query required")
	}
	if limit <= 0 {
		limit = 5
	}
	if limit > 20 {
		limit = 20
	}
	base := c.BaseURL
	if base == "" {
		base = defaultBaseURL
	}
	httpClient := c.HTTP
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 25 * time.Second}
	}

	body := map[string]any{
		"query":        query,
		"type":         "auto",
		"numResults":   limit,
		"userLocation": "TW",
		"contents": map[string]any{
			"highlights": true,
			"text":       map[string]any{"maxCharacters": 400},
		},
	}
	if len(c.IncludeDomains) > 0 {
		body["includeDomains"] = c.IncludeDomains
	}

	payload, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, base, bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("x-api-key", apiKey)

	res, err := httpClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	raw, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if res.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("exa status %d: %s", res.StatusCode, truncateRunes(string(raw), 120))
	}

	var parsed struct {
		Results []struct {
			Title      string   `json:"title"`
			URL        string   `json:"url"`
			Highlights []string `json:"highlights"`
			Text       string   `json:"text"`
			Author     string   `json:"author"`
		} `json:"results"`
	}
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return nil, err
	}

	out := make([]Hit, 0, len(parsed.Results))
	for _, item := range parsed.Results {
		u := strings.TrimSpace(item.URL)
		if u == "" {
			continue
		}
		snippet := firstNonEmpty(item.Highlights...)
		if snippet == "" {
			snippet = strings.TrimSpace(item.Text)
		}
		if snippet == "" {
			snippet = strings.TrimSpace(item.Title)
		}
		title := strings.TrimSpace(item.Title)
		if title == "" {
			title = strings.TrimSpace(item.Author)
		}
		if title == "" {
			title = u
		}
		out = append(out, Hit{
			Title:   title,
			URL:     u,
			Snippet: truncateRunes(snippet, 200),
		})
		if len(out) >= limit {
			break
		}
	}
	return out, nil
}

func firstNonEmpty(ss ...string) string {
	for _, s := range ss {
		if t := strings.TrimSpace(s); t != "" {
			return t
		}
	}
	return ""
}

func truncateRunes(s string, n int) string {
	r := []rune(s)
	if len(r) <= n {
		return s
	}
	return string(r[:n]) + "…"
}
