package usecase

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

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

// ThreadSearchProvider finds public Threads posts for a Scout scan.
type ThreadSearchProvider interface {
	SearchThreads(ctx context.Context, terms []string, limit int) ([]ThreadSearchResult, error)
}

type ThreadSearchResult struct {
	URL     string
	Title   string
	Snippet string
	// PublishedAt unix nanoseconds when known (Exa publishedDate / crawler parse).
	PublishedAt int64
	// MatchedQuery is set by fan-out search to the query that found this hit.
	MatchedQuery string
	// Track: top | recent | both — crawler dual-track; empty for Exa/API.
	Track string
	// SerpRank is 1-based rank within the source SERP track (crawler); 0 = unknown.
	SerpRank int
}

// ExaThreadsProvider searches only Threads-owned domains through Exa.
type ExaThreadsProvider struct {
	APIKey  string
	BaseURL string
	HTTP    *http.Client
}

func NewExaThreadsProvider(apiKey string) *ExaThreadsProvider {
	return &ExaThreadsProvider{
		APIKey:  strings.TrimSpace(apiKey),
		BaseURL: exaSearchURL,
		HTTP:    &http.Client{Timeout: 25 * time.Second},
	}
}

func newDefaultExaThreadsProvider() *ExaThreadsProvider {
	key := os.Getenv("EXA_API_KEY")
	if strings.TrimSpace(key) == "" {
		key = os.Getenv("EXA_KEY")
	}
	return NewExaThreadsProvider(key)
}

func (p *ExaThreadsProvider) SearchThreads(ctx context.Context, terms []string, limit int) ([]ThreadSearchResult, error) {
	if p == nil || strings.TrimSpace(p.APIKey) == "" {
		return nil, fmt.Errorf("exa Threads search is not configured")
	}
	if limit <= 0 {
		limit = 10
	}
	if limit > 20 {
		limit = 20
	}
	// 單次呼叫：建議只傳 1 條完整 query（fan-out 在上層）
	clean := nonEmptyTerms(terms)
	if len(clean) == 0 {
		return nil, fmt.Errorf("exa Threads search query required")
	}
	query := clean[0]
	if len(clean) > 1 {
		// 相容舊呼叫：仍可 join，但 fan-out 路徑不會走這裡
		query = strings.Join(clean, " ")
	}

	payload, err := json.Marshal(map[string]any{
		"query":          query,
		"type":           "auto",
		"numResults":     limit,
		"includeDomains": []string{"threads.net", "threads.com"},
		"contents": map[string]any{
			"highlights": true,
			"text":       map[string]any{"maxCharacters": 400},
		},
	})
	if err != nil {
		return nil, err
	}
	baseURL := p.BaseURL
	if baseURL == "" {
		baseURL = exaSearchURL
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL, 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", p.APIKey)
	httpClient := p.HTTP
	if httpClient == nil {
		httpClient = &http.Client{Timeout: 25 * time.Second}
	}
	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, truncate(string(raw), 120))
	}

	var response struct {
		Results []struct {
			Title         string   `json:"title"`
			URL           string   `json:"url"`
			PublishedDate string   `json:"publishedDate"`
			Highlights    []string `json:"highlights"`
			Text          string   `json:"text"`
		} `json:"results"`
	}
	if err := json.Unmarshal(raw, &response); err != nil {
		return nil, err
	}
	results := make([]ThreadSearchResult, 0, len(response.Results))
	for _, hit := range response.Results {
		url := strings.TrimSpace(hit.URL)
		if !isThreadsURL(url) {
			continue
		}
		snippet := firstNonEmpty(hit.Highlights)
		if snippet == "" {
			snippet = strings.TrimSpace(hit.Text)
		}
		if snippet == "" {
			snippet = strings.TrimSpace(hit.Title)
		}
		results = append(results, ThreadSearchResult{
			URL:         url,
			Title:       strings.TrimSpace(hit.Title),
			Snippet:     truncate(snippet, 400),
			PublishedAt: parsePublishedDateNano(hit.PublishedDate),
		})
	}
	return results, nil
}

func parsePublishedDateNano(raw string) int64 {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return 0
	}
	// Exa 常見 RFC3339 / date-only
	layouts := []string{
		time.RFC3339,
		time.RFC3339Nano,
		"2006-01-02T15:04:05.000Z",
		"2006-01-02T15:04:05Z",
		"2006-01-02",
	}
	for _, layout := range layouts {
		if t, err := time.Parse(layout, raw); err == nil {
			return t.UTC().UnixNano()
		}
	}
	return 0
}

func isThreadsURL(raw string) bool {
	u, err := url.Parse(raw)
	if err != nil {
		return false
	}
	host := strings.ToLower(u.Hostname())
	return host == "threads.net" || strings.HasSuffix(host, ".threads.net") || host == "threads.com" || strings.HasSuffix(host, ".threads.com")
}

func nonEmptyTerms(terms []string) []string {
	out := make([]string, 0, len(terms))
	for _, term := range terms {
		if term = strings.TrimSpace(term); term != "" {
			out = append(out, term)
		}
	}
	return out
}

func firstNonEmpty(values []string) string {
	for _, value := range values {
		if value = strings.TrimSpace(value); value != "" {
			return value
		}
	}
	return ""
}
