package provider

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

// RemoteThread — Graph Threads 列表／單則媒體
type RemoteThread struct {
	ID           string
	Text         string
	MediaType    string
	MediaURL     string
	ThumbnailURL string
	Permalink    string
	Shortcode    string
	TopicTag     string
	Username     string
	Timestamp    time.Time
	ReplyControl string
}

// RemoteInsights — media insights
type RemoteInsights struct {
	Views    int
	Likes    int
	Replies  int
	Reposts  int
	Quotes   int
	Shares   int
	Status   string // ok | partial | error | skipped
	ErrorMsg string
}

// RemoteReply — conversation / replies under a media
type RemoteReply struct {
	ID            string
	Text          string
	Username      string
	Timestamp     time.Time
	IsMine        bool
	ParentMediaID string
	LikeCount     int
	HideStatus    string
}

// RemoteMention — GET /{user-id}/mentions（threads_manage_mentions）
type RemoteMention struct {
	ID          string
	Text        string
	Username    string
	Permalink   string
	MediaType   string
	Timestamp   time.Time
	IsReply     bool
	IsQuotePost bool
	HasReplies  bool
	RootPostID  string
	ParentID    string
}

// MediaClient lists own threads + insights + conversation (Meta Graph).
type MediaClient interface {
	ListThreads(ctx context.Context, accessToken string, limit int) ([]RemoteThread, error)
	GetInsights(ctx context.Context, accessToken, mediaID string) (RemoteInsights, error)
	ListConversation(ctx context.Context, accessToken, mediaID string, limit int) ([]RemoteReply, error)
	ListMentions(ctx context.Context, accessToken, threadsUserID string, limit int) ([]RemoteMention, error)
	// ListProfilePosts — 公開帳號貼文（threads_profile_discovery / profile_posts）
	ListProfilePosts(ctx context.Context, accessToken, username string, limit int) ([]RemoteThread, error)
}

const threadsListFields = "id,media_type,media_url,permalink,username,text,topic_tag,timestamp,shortcode,thumbnail_url,is_quote_post,reply_control"
const threadsListFieldsLegacy = "id,media_type,media_url,permalink,username,text,topic_tag,timestamp,shortcode,thumbnail_url,is_quote_post"

// ListThreads GET /v1.0/me/threads
func (m *MetaProvider) ListThreads(ctx context.Context, accessToken string, limit int) ([]RemoteThread, error) {
	list, err := m.listThreads(ctx, accessToken, limit, threadsListFields)
	if err != nil && strings.Contains(strings.ToLower(err.Error()), "reply_control") {
		return m.listThreads(ctx, accessToken, limit, threadsListFieldsLegacy)
	}
	return list, err
}

func (m *MetaProvider) listThreads(ctx context.Context, accessToken string, limit int, fields string) ([]RemoteThread, error) {
	if limit <= 0 {
		limit = 25
	}
	if limit > 50 {
		limit = 50
	}
	q := url.Values{}
	q.Set("fields", fields)
	q.Set("limit", strconv.Itoa(limit))
	q.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/me/threads?" + q.Encode()
	body, err := m.getJSON(ctx, u)
	if err != nil {
		return nil, err
	}
	var resp struct {
		Data []struct {
			ID           string `json:"id"`
			MediaType    string `json:"media_type"`
			MediaURL     string `json:"media_url"`
			Permalink    string `json:"permalink"`
			Username     string `json:"username"`
			Text         string `json:"text"`
			TopicTag     string `json:"topic_tag"`
			Timestamp    string `json:"timestamp"`
			Shortcode    string `json:"shortcode"`
			ThumbnailURL string `json:"thumbnail_url"`
			ReplyControl string `json:"reply_control"`
		} `json:"data"`
		Error *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("threads list parse: %w", err)
	}
	if resp.Error != nil {
		return nil, resp.Error
	}
	out := make([]RemoteThread, 0, len(resp.Data))
	for _, d := range resp.Data {
		out = append(out, RemoteThread{
			ID: d.ID, Text: d.Text, MediaType: d.MediaType, MediaURL: d.MediaURL,
			ThumbnailURL: d.ThumbnailURL, Permalink: d.Permalink, Shortcode: d.Shortcode,
			TopicTag: d.TopicTag, Username: d.Username, Timestamp: parseThreadsTime(d.Timestamp),
			ReplyControl: NormalizeReplyControl(d.ReplyControl),
		})
	}
	return out, nil
}

// GetInsights GET /v1.0/{media-id}/insights
func (m *MetaProvider) GetInsights(ctx context.Context, accessToken, mediaID string) (RemoteInsights, error) {
	ins := RemoteInsights{Status: "ok"}
	if mediaID == "" {
		ins.Status = "skipped"
		return ins, nil
	}
	q := url.Values{}
	q.Set("metric", "views,likes,replies,reposts,quotes,shares")
	q.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/" + url.PathEscape(mediaID) + "/insights?" + q.Encode()
	body, err := m.getJSON(ctx, u)
	if err != nil {
		ins.Status = "error"
		ins.ErrorMsg = err.Error()
		return ins, nil // 不整批失敗
	}
	var resp struct {
		Data []struct {
			Name   string `json:"name"`
			Values []struct {
				Value json.Number `json:"value"`
			} `json:"values"`
			TotalValue *struct {
				Value json.Number `json:"value"`
			} `json:"total_value"`
		} `json:"data"`
		Error *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		ins.Status = "error"
		ins.ErrorMsg = err.Error()
		return ins, nil
	}
	if resp.Error != nil {
		ins.Status = "error"
		ins.ErrorMsg = resp.Error.Error()
		return ins, nil
	}
	for _, d := range resp.Data {
		v := 0
		if d.TotalValue != nil {
			v, _ = strconv.Atoi(string(d.TotalValue.Value))
		} else if len(d.Values) > 0 {
			v, _ = strconv.Atoi(string(d.Values[0].Value))
		}
		switch d.Name {
		case "views":
			ins.Views = v
		case "likes":
			ins.Likes = v
		case "replies":
			ins.Replies = v
		case "reposts":
			ins.Reposts = v
		case "quotes":
			ins.Quotes = v
		case "shares":
			ins.Shares = v
		}
	}
	return ins, nil
}

// ListConversation 先試 /replies（第一層留言），再試 /conversation（含巢狀）。
// 失敗不整批炸掉，回 empty + 可診斷的 error（由上層 log）。
func (m *MetaProvider) ListConversation(ctx context.Context, accessToken, mediaID string, limit int) ([]RemoteReply, error) {
	if mediaID == "" {
		return nil, nil
	}
	if limit <= 0 {
		limit = 30
	}
	if limit > 50 {
		limit = 50
	}

	// 官方 example 欄位 + username / is_reply_owned_by_me（文件有列）
	fields := "id,text,username,timestamp,media_type,has_replies,root_post,replied_to,is_reply,is_reply_owned_by_me,hide_status"

	// 1) top-level replies
	replies, err1 := m.fetchReplyEdge(ctx, accessToken, mediaID, "replies", fields, limit)
	if len(replies) > 0 {
		return normalizeParents(replies, mediaID), nil
	}

	// 2) full conversation (nested)
	convo, err2 := m.fetchReplyEdge(ctx, accessToken, mediaID, "conversation", fields, limit)
	if len(convo) > 0 {
		return normalizeParents(convo, mediaID), nil
	}

	// 兩者都空：若有錯誤回傳最後一個，方便 log
	if err1 != nil {
		return nil, err1
	}
	if err2 != nil {
		return nil, err2
	}
	return nil, nil
}

func (m *MetaProvider) fetchReplyEdge(ctx context.Context, accessToken, mediaID, edge, fields string, limit int) ([]RemoteReply, error) {
	q := url.Values{}
	q.Set("fields", fields)
	q.Set("limit", strconv.Itoa(limit))
	q.Set("reverse", "false")
	q.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/" + url.PathEscape(mediaID) + "/" + edge + "?" + q.Encode()
	body, err := m.getJSON(ctx, u)
	if err != nil {
		return nil, err
	}
	var resp struct {
		Data []struct {
			ID               string `json:"id"`
			Text             string `json:"text"`
			Username         string `json:"username"`
			Timestamp        string `json:"timestamp"`
			IsReplyOwnedByMe bool   `json:"is_reply_owned_by_me"`
			IsReply          bool   `json:"is_reply"`
			HideStatus       string `json:"hide_status"`
			RepliedTo        *struct {
				ID string `json:"id"`
			} `json:"replied_to"`
			RootPost *struct {
				ID string `json:"id"`
			} `json:"root_post"`
		} `json:"data"`
		Error *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("threads %s parse: %w", edge, err)
	}
	if resp.Error != nil {
		return nil, resp.Error
	}
	out := make([]RemoteReply, 0, len(resp.Data))
	for _, d := range resp.Data {
		if strings.TrimSpace(d.ID) == "" || d.ID == mediaID {
			continue
		}
		parent := ""
		if d.RepliedTo != nil {
			parent = d.RepliedTo.ID
		}
		out = append(out, RemoteReply{
			ID: d.ID, Text: d.Text, Username: d.Username,
			Timestamp: parseThreadsTime(d.Timestamp), IsMine: d.IsReplyOwnedByMe,
			ParentMediaID: parent, HideStatus: strings.TrimSpace(d.HideStatus),
		})
	}
	return out, nil
}

// normalizeParents — 回覆根貼的 replied_to = root media id，對 UI 應視為第一層（parent 空）。
// conversation 有時會把主貼自己也列進來，隱藏時不能拿主貼 id 打 /manage_reply。
func normalizeParents(list []RemoteReply, rootMediaID string) []RemoteReply {
	out := make([]RemoteReply, 0, len(list))
	for _, r := range list {
		if r.ID == "" || r.ID == rootMediaID {
			continue
		}
		if r.ParentMediaID == rootMediaID {
			r.ParentMediaID = ""
		}
		out = append(out, r)
	}
	return out
}

type graphErr struct {
	Message string `json:"message"`
	Type    string `json:"type"`
	Code    int    `json:"code"`
}

func (e *graphErr) Error() string {
	if e == nil {
		return "graph error"
	}
	return fmt.Sprintf("threads graph: %s (code %d)", e.Message, e.Code)
}

func (m *MetaProvider) getJSON(ctx context.Context, fullURL string) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
	if err != nil {
		return nil, err
	}
	cli := m.HTTPClient
	if cli == nil {
		cli = &http.Client{Timeout: 25 * time.Second}
	}
	resp, err := cli.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 300 {
		var ge struct {
			Error *graphErr `json:"error"`
		}
		_ = json.Unmarshal(body, &ge)
		if ge.Error != nil {
			return nil, ge.Error
		}
		return nil, fmt.Errorf("threads graph HTTP %d: %s", resp.StatusCode, truncateBody(string(body), 200))
	}
	return body, nil
}

// ReplyControlEveryone is Threads' default audience.
const ReplyControlEveryone = "everyone"

// ValidReplyControls are the official Threads publishing reply_control values.
var ValidReplyControls = map[string]struct{}{
	"everyone":                {},
	"accounts_you_follow":     {},
	"mentioned_only":          {},
	"parent_post_author_only": {},
	"followers_only":          {},
}

func NormalizeReplyControl(raw string) string {
	s := strings.ToLower(strings.TrimSpace(raw))
	if s == "" {
		return ""
	}
	if _, ok := ValidReplyControls[s]; ok {
		return s
	}
	return ""
}

// ManageReply POST /{reply-id}/manage_reply  hide=true|false
func (m *MetaProvider) ManageReply(ctx context.Context, accessToken, replyID string, hide bool) error {
	replyID = strings.TrimSpace(replyID)
	if replyID == "" {
		return fmt.Errorf("reply id is required")
	}
	form := url.Values{}
	if hide {
		form.Set("hide", "true")
	} else {
		form.Set("hide", "false")
	}
	form.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/" + url.PathEscape(replyID) + "/manage_reply"
	body, err := m.postForm(ctx, u, form)
	if err != nil {
		return err
	}
	var resp struct {
		Success bool      `json:"success"`
		Error   *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return fmt.Errorf("threads manage_reply parse: %w", err)
	}
	if resp.Error != nil {
		return resp.Error
	}
	if !resp.Success {
		return fmt.Errorf("threads manage_reply did not succeed")
	}
	return nil
}

// SetReplyControl POST /{media-id} reply_control=…
// Official docs set this at publish time; Graph also accepts it on the published media id.
func (m *MetaProvider) SetReplyControl(ctx context.Context, accessToken, mediaID, control string) error {
	mediaID = strings.TrimSpace(mediaID)
	control = NormalizeReplyControl(control)
	if mediaID == "" {
		return fmt.Errorf("media id is required")
	}
	if control == "" {
		return fmt.Errorf("invalid reply_control")
	}
	form := url.Values{}
	form.Set("reply_control", control)
	form.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/" + url.PathEscape(mediaID)
	body, err := m.postForm(ctx, u, form)
	if err != nil {
		return err
	}
	var resp struct {
		Success bool      `json:"success"`
		ID      string    `json:"id"`
		Error   *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return fmt.Errorf("threads reply_control parse: %w", err)
	}
	if resp.Error != nil {
		return resp.Error
	}
	return nil
}

func (m *MetaProvider) postForm(ctx context.Context, fullURL string, form url.Values) ([]byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	cli := m.HTTPClient
	if cli == nil {
		cli = &http.Client{Timeout: 25 * time.Second}
	}
	resp, err := cli.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 300 {
		var ge struct {
			Error *graphErr `json:"error"`
		}
		_ = json.Unmarshal(body, &ge)
		if ge.Error != nil {
			return nil, ge.Error
		}
		return nil, fmt.Errorf("threads graph HTTP %d: %s", resp.StatusCode, truncateBody(string(body), 200))
	}
	return body, nil
}

func parseThreadsTime(s string) time.Time {
	s = strings.TrimSpace(s)
	if s == "" {
		return time.Time{}
	}
	// 2023-10-17T05:42:03+0000
	layouts := []string{
		"2006-01-02T15:04:05-0700",
		"2006-01-02T15:04:05+0000",
		time.RFC3339,
	}
	for _, l := range layouts {
		if t, err := time.Parse(l, s); err == nil {
			return t.UTC()
		}
	}
	// try fix +0000 → Z
	if strings.HasSuffix(s, "+0000") {
		if t, err := time.Parse(time.RFC3339, strings.TrimSuffix(s, "+0000")+"Z"); err == nil {
			return t.UTC()
		}
	}
	return time.Time{}
}

// ListProfilePosts GET /v1.0/profile_posts?username=… — 公開個人頁貼文（threads_profile_discovery）
// 用「已連帳」的 access token 即可讀其他公開帳號的貼文，不需 Chrome 爬蟲。
func (m *MetaProvider) ListProfilePosts(ctx context.Context, accessToken, username string, limit int) ([]RemoteThread, error) {
	username = strings.TrimPrefix(strings.TrimSpace(username), "@")
	if username == "" {
		return nil, fmt.Errorf("username is required")
	}
	if limit <= 0 {
		limit = 12
	}
	if limit > 50 {
		limit = 50
	}
	fields := "id,media_type,media_url,permalink,username,text,topic_tag,timestamp,shortcode,thumbnail_url,is_quote_post"
	q := url.Values{}
	q.Set("username", username)
	q.Set("fields", fields)
	q.Set("limit", strconv.Itoa(limit))
	q.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/profile_posts?" + q.Encode()
	body, err := m.getJSON(ctx, u)
	if err != nil {
		return nil, err
	}
	var resp struct {
		Data []struct {
			ID           string `json:"id"`
			MediaType    string `json:"media_type"`
			MediaURL     string `json:"media_url"`
			Permalink    string `json:"permalink"`
			Username     string `json:"username"`
			Text         string `json:"text"`
			TopicTag     string `json:"topic_tag"`
			Timestamp    string `json:"timestamp"`
			Shortcode    string `json:"shortcode"`
			ThumbnailURL string `json:"thumbnail_url"`
		} `json:"data"`
		Error *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("profile_posts parse: %w", err)
	}
	if resp.Error != nil {
		return nil, resp.Error
	}
	out := make([]RemoteThread, 0, len(resp.Data))
	for _, d := range resp.Data {
		if strings.TrimSpace(d.ID) == "" && strings.TrimSpace(d.Text) == "" {
			continue
		}
		out = append(out, RemoteThread{
			ID: d.ID, Text: d.Text, MediaType: d.MediaType, MediaURL: d.MediaURL,
			ThumbnailURL: d.ThumbnailURL, Permalink: d.Permalink, Shortcode: d.Shortcode,
			TopicTag: d.TopicTag, Username: d.Username, Timestamp: parseThreadsTime(d.Timestamp),
		})
	}
	return out, nil
}

// ListMentions GET /v1.0/{user-id|me}/mentions — 別人 @ 你的貼文／回覆／引用
func (m *MetaProvider) ListMentions(ctx context.Context, accessToken, threadsUserID string, limit int) ([]RemoteMention, error) {
	if limit <= 0 {
		limit = 25
	}
	if limit > 100 {
		limit = 100
	}
	uid := strings.TrimSpace(threadsUserID)
	if uid == "" {
		uid = "me"
	}
	fields := "id,text,username,permalink,timestamp,media_type,is_reply,is_quote_post,has_replies,root_post,parent_id"
	q := url.Values{}
	q.Set("fields", fields)
	q.Set("limit", strconv.Itoa(limit))
	q.Set("access_token", accessToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/v1.0/" + url.PathEscape(uid) + "/mentions?" + q.Encode()
	body, err := m.getJSON(ctx, u)
	if err != nil {
		return nil, err
	}
	var resp struct {
		Data []struct {
			ID          string `json:"id"`
			Text        string `json:"text"`
			Username    string `json:"username"`
			Permalink   string `json:"permalink"`
			Timestamp   string `json:"timestamp"`
			MediaType   string `json:"media_type"`
			IsReply     bool   `json:"is_reply"`
			IsQuotePost bool   `json:"is_quote_post"`
			HasReplies  bool   `json:"has_replies"`
			ParentID    string `json:"parent_id"`
			RootPost    *struct {
				ID string `json:"id"`
			} `json:"root_post"`
		} `json:"data"`
		Error *graphErr `json:"error"`
	}
	if err := json.Unmarshal(body, &resp); err != nil {
		return nil, fmt.Errorf("mentions parse: %w", err)
	}
	if resp.Error != nil {
		return nil, resp.Error
	}
	out := make([]RemoteMention, 0, len(resp.Data))
	for _, d := range resp.Data {
		if strings.TrimSpace(d.ID) == "" {
			continue
		}
		rootID := ""
		if d.RootPost != nil {
			rootID = strings.TrimSpace(d.RootPost.ID)
		}
		out = append(out, RemoteMention{
			ID: d.ID, Text: d.Text, Username: d.Username, Permalink: d.Permalink,
			MediaType: d.MediaType, Timestamp: parseThreadsTime(d.Timestamp),
			IsReply: d.IsReply, IsQuotePost: d.IsQuotePost, HasReplies: d.HasReplies,
			RootPostID: rootID, ParentID: strings.TrimSpace(d.ParentID),
		})
	}
	return out, nil
}

// Ensure MetaProvider implements MediaClient.
var _ MediaClient = (*MetaProvider)(nil)
