package threadsapi

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"unicode/utf8"
)

var threadsPermalinkRe = regexp.MustCompile(`(?i)threads\.(?:com|net)/@([^/]+)/post/([^/?#]+)`)

// IsNumericMediaID reports whether the value looks like a Threads Graph media id.
func IsNumericMediaID(raw string) bool {
	raw = strings.TrimSpace(raw)
	if len(raw) < 8 {
		return false
	}
	for _, r := range raw {
		if r < '0' || r > '9' {
			return false
		}
	}
	return true
}

func parsePermalinkParts(permalink string) (username, shortcode string) {
	permalink = strings.TrimSpace(permalink)
	if permalink == "" {
		return "", ""
	}
	permalink = strings.Split(permalink, "?")[0]
	permalink = strings.Split(permalink, "#")[0]
	match := threadsPermalinkRe.FindStringSubmatch(permalink)
	if len(match) < 3 {
		return "", ""
	}
	return strings.TrimSpace(match[1]), strings.TrimSpace(match[2])
}

type ResolveReplyMediaInput struct {
	ExternalID string
	Permalink  string
	HintText   string
}

// ResolveReplyMediaID returns a Graph API media id suitable for reply_to_id.
func (c *Client) ResolveReplyMediaID(ctx context.Context, externalID, permalink string) (string, error) {
	return c.ResolveReplyMedia(ctx, ResolveReplyMediaInput{
		ExternalID: externalID,
		Permalink:  permalink,
	})
}

// ResolveReplyMedia resolves shortcode/permalink targets into numeric media ids for reply_to_id.
func (c *Client) ResolveReplyMedia(ctx context.Context, in ResolveReplyMediaInput) (string, error) {
	if !c.Enabled() {
		return "", fmt.Errorf("threads api access token is required")
	}
	candidate := strings.TrimSpace(in.ExternalID)
	if IsNumericMediaID(candidate) {
		return candidate, nil
	}
	username, shortcode := parsePermalinkParts(in.Permalink)
	if !IsNumericMediaID(candidate) && candidate != "" {
		shortcode = candidate
	}
	if IsNumericMediaID(shortcode) {
		return shortcode, nil
	}
	if shortcode == "" {
		return "", fmt.Errorf("缺少有效的 Threads 貼文 ID")
	}
	if username == "" {
		return "", fmt.Errorf("permalink 缺少作者資訊，無法解析貼文 ID")
	}

	for _, query := range keywordQueriesFromHint(in.HintText) {
		if id, err := c.lookupMediaIDByKeywordText(ctx, username, shortcode, query); err == nil && id != "" {
			return id, nil
		}
	}
	if id, err := c.lookupMediaIDViaProfilePosts(ctx, username, shortcode); err == nil && id != "" {
		return id, nil
	}
	if id, err := c.lookupMediaIDByAuthorAndShortcode(ctx, username, shortcode); err == nil && id != "" {
		return id, nil
	}
	return "", fmt.Errorf(
		"無法將貼文 shortcode 轉成 Threads media id（%s/@%s/post/%s）。請確認 OAuth 具備 threads_profile_discovery 與 threads_keyword_search，或重新以 API 海巡抓取貼文",
		shortcode, username, shortcode,
	)
}

func keywordQueriesFromHint(hint string) []string {
	hint = strings.TrimSpace(hint)
	if hint == "" {
		return nil
	}
	hint = strings.Join(strings.Fields(hint), " ")
	seen := map[string]struct{}{}
	out := make([]string, 0, 3)
	add := func(query string) {
		query = strings.TrimSpace(query)
		if query == "" || utf8.RuneCountInString(query) < 4 {
			return
		}
		if _, ok := seen[query]; ok {
			return
		}
		seen[query] = struct{}{}
		out = append(out, query)
	}
	add(trimRunes(hint, 80))
	if idx := strings.IndexAny(hint, "。！？!?.\n"); idx > 0 {
		add(trimRunes(hint[:idx], 60))
	}
	add(trimRunes(hint, 32))
	return out
}

func trimRunes(raw string, max int) string {
	raw = strings.TrimSpace(raw)
	if max <= 0 || utf8.RuneCountInString(raw) <= max {
		return raw
	}
	runes := []rune(raw)
	return strings.TrimSpace(string(runes[:max]))
}

func (c *Client) lookupMediaIDViaProfilePosts(ctx context.Context, username, shortcode string) (string, error) {
	return c.FindMediaIDByAuthorShortcode(ctx, username, shortcode)
}

func (c *Client) lookupMediaIDByKeywordText(ctx context.Context, username, shortcode, query string) (string, error) {
	query = strings.TrimSpace(query)
	if query == "" {
		return "", fmt.Errorf("keyword query is required")
	}
	for _, searchType := range []string{"TOP", "RECENT"} {
		id, err := c.keywordSearchMediaByShortcode(ctx, username, query, searchType, true, shortcode)
		if err == nil && id != "" {
			return id, nil
		}
	}
	return "", fmt.Errorf("threads keyword_search did not return media for @%s post %s", username, shortcode)
}

func (c *Client) lookupMediaIDByAuthorAndShortcode(ctx context.Context, username, shortcode string) (string, error) {
	username = strings.TrimPrefix(strings.TrimSpace(username), "@")
	shortcode = strings.TrimSpace(shortcode)
	if username == "" || shortcode == "" {
		return "", fmt.Errorf("username and shortcode are required")
	}
	type lookupStrategy struct {
		searchType string
		withAuthor bool
	}
	strategies := []lookupStrategy{
		{searchType: "TOP", withAuthor: true},
		{searchType: "RECENT", withAuthor: true},
		{searchType: "TOP", withAuthor: false},
		{searchType: "RECENT", withAuthor: false},
	}
	var lastErr error
	for _, strategy := range strategies {
		id, err := c.keywordSearchMediaByShortcode(ctx, username, shortcode, strategy.searchType, strategy.withAuthor, shortcode)
		if err == nil && id != "" {
			return id, nil
		}
		if err != nil {
			lastErr = err
		}
	}
	if lastErr != nil {
		return "", lastErr
	}
	return "", fmt.Errorf("threads keyword_search did not return media for @%s post %s", username, shortcode)
}

func (c *Client) keywordSearchMediaByShortcode(
	ctx context.Context,
	username, query, searchType string,
	withAuthor bool,
	wantShortcode string,
) (string, error) {
	query = strings.TrimSpace(query)
	wantShortcode = strings.TrimSpace(wantShortcode)
	if query == "" || wantShortcode == "" {
		return "", fmt.Errorf("query and shortcode are required")
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("q", query)
	params.Set("search_type", searchType)
	if withAuthor {
		params.Set("author_username", username)
	}
	params.Set("fields", "id,permalink,shortcode")
	params.Set("limit", "25")
	endpoint := graphBaseURL + "/keyword_search?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return "", err
	}
	res, err := c.http.Do(req)
	if err != nil {
		return "", err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return "", err
	}
	if res.StatusCode != http.StatusOK {
		return "", parseAPIError(body, res.StatusCode)
	}
	var payload struct {
		Data []struct {
			ID        string `json:"id"`
			Permalink string `json:"permalink"`
			Shortcode string `json:"shortcode"`
		} `json:"data"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return "", err
	}
	wantSuffix := "/post/" + wantShortcode
	for _, item := range payload.Data {
		id := strings.TrimSpace(item.ID)
		if !IsNumericMediaID(id) {
			continue
		}
		if withAuthor {
			link := strings.TrimSpace(item.Permalink)
			if link != "" && !strings.Contains(strings.ToLower(link), "/@"+strings.ToLower(username)+"/") {
				continue
			}
		}
		if strings.EqualFold(strings.TrimSpace(item.Shortcode), wantShortcode) {
			return id, nil
		}
		link := strings.TrimSpace(item.Permalink)
		if link != "" && strings.HasSuffix(strings.Split(link, "?")[0], wantSuffix) {
			return id, nil
		}
	}
	return "", fmt.Errorf("threads keyword_search did not return media for @%s post %s", username, wantShortcode)
}

// PrimeReplyTargetForPublish registers a public post via Threads API search before reply_to_id publish.
// Meta only allows replying to public posts that were recently returned by keyword_search.
func (c *Client) PrimeReplyTargetForPublish(ctx context.Context, in ResolveReplyMediaInput, mediaID string) error {
	if !c.Enabled() {
		return fmt.Errorf("threads api access token is required")
	}
	username, shortcode := parsePermalinkParts(in.Permalink)
	if candidate := strings.TrimSpace(in.ExternalID); !IsNumericMediaID(candidate) && candidate != "" {
		shortcode = candidate
	}
	mediaID = strings.TrimSpace(mediaID)
	if username == "" || shortcode == "" || mediaID == "" {
		return fmt.Errorf("缺少作者、shortcode 或 media id，無法預先搜尋貼文")
	}

	// Fast path: shortcode / author queries with author_username filter (fewer API round-trips).
	for _, query := range []string{shortcode, username, "@" + username} {
		for _, searchType := range []string{"TOP", "RECENT"} {
			id, err := c.keywordSearchMediaByShortcode(ctx, username, query, searchType, true, shortcode)
			if err == nil && strings.TrimSpace(id) == mediaID {
				return nil
			}
		}
	}
	for _, query := range keywordQueriesFromHint(in.HintText) {
		for _, searchType := range []string{"TOP", "RECENT"} {
			id, err := c.keywordSearchMediaByShortcode(ctx, username, query, searchType, true, shortcode)
			if err == nil && strings.TrimSpace(id) == mediaID {
				return nil
			}
		}
	}
	if id, err := c.FindMediaIDByAuthorShortcode(ctx, username, shortcode); err == nil && strings.TrimSpace(id) == mediaID {
		for _, searchType := range []string{"TOP", "RECENT"} {
			if _, err := c.keywordSearchMediaByShortcode(ctx, username, shortcode, searchType, true, shortcode); err == nil {
				return nil
			}
		}
	}
	return fmt.Errorf(
		"Threads API 搜尋不到 @%s 的這篇貼文（%s）。回覆他人貼文前必須先用 keyword_search 找到它，請確認 threads_keyword_search 權限已核准，或改用「開始海巡（API）」重新抓取",
		username, shortcode,
	)
}
