package threadsapi

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

const (
	// userThreadsListFields lists fields supported on GET /{user-id}/threads.
	userThreadsListFields = "id,text,permalink,timestamp,media_type,media_url,thumbnail_url,topic_tag,shortcode"
)

type MediaItem struct {
	ID           string `json:"id"`
	Shortcode    string `json:"shortcode"`
	Text         string `json:"text"`
	Permalink    string `json:"permalink"`
	Timestamp    string `json:"timestamp"`
	MediaType    string `json:"media_type"`
	MediaURL     string `json:"media_url"`
	ThumbnailURL string `json:"thumbnail_url"`
	TopicTag     string `json:"topic_tag"`
	LikeCount    int    `json:"like_count"`
	ReplyCount   int    `json:"reply_count"`
}

type ProfileLookupResult struct {
	Username          string `json:"username"`
	Name              string `json:"name"`
	ProfilePictureURL string `json:"profile_picture_url,omitempty"`
	Biography         string `json:"biography,omitempty"`
	FollowerCount     int    `json:"follower_count,omitempty"`
	LikesCount        int    `json:"likes_count,omitempty"`
	QuotesCount       int    `json:"quotes_count,omitempty"`
	RepostsCount      int    `json:"reposts_count,omitempty"`
	ViewsCount        int    `json:"views_count,omitempty"`
	IsVerified        bool   `json:"is_verified,omitempty"`
}

type MentionItem 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"`
	RootPostID  string `json:"root_post_id,omitempty"`
	ParentID    string `json:"parent_id,omitempty"`
}

type LocationItem struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type InsightValue struct {
	Name   string `json:"name"`
	Values []struct {
		Value int `json:"value"`
	} `json:"values"`
}

// UserThreads lists the authenticated user's own posts (threads_basic).
func (c *Client) UserThreads(ctx context.Context, userID string, limit int) ([]MediaItem, error) {
	if !c.Enabled() {
		return nil, fmt.Errorf("threads api access token is required")
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return nil, fmt.Errorf("threads user id is required")
	}
	if limit <= 0 {
		limit = 25
	}
	if limit > 100 {
		limit = 100
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("fields", userThreadsListFields)
	params.Set("limit", fmt.Sprintf("%d", limit))
	endpoint := graphBaseURL + "/" + url.PathEscape(userID) + "/threads?" + params.Encode()
	return c.getMediaList(ctx, endpoint)
}

// DeleteMedia deletes a Threads post (threads_delete).
func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error {
	if !c.Enabled() {
		return fmt.Errorf("threads api access token is required")
	}
	mediaID = strings.TrimSpace(mediaID)
	if mediaID == "" {
		return fmt.Errorf("threads media id is required")
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	endpoint := graphBaseURL + "/" + url.PathEscape(mediaID) + "?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, endpoint, nil)
	if err != nil {
		return err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return err
	}
	if status != http.StatusOK {
		return parseAPIError(body, status)
	}
	return nil
}

// ProfilePosts lists recent public posts for a username (threads_profile_discovery).
func (c *Client) ProfilePosts(ctx context.Context, username string, limit int, after string) ([]MediaItem, string, error) {
	if !c.Enabled() {
		return nil, "", fmt.Errorf("threads api access token is required")
	}
	username = strings.TrimPrefix(strings.TrimSpace(username), "@")
	if username == "" {
		return nil, "", fmt.Errorf("username is required")
	}
	if limit <= 0 {
		limit = 50
	}
	if limit > 100 {
		limit = 100
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("username", username)
	params.Set("fields", "id,shortcode,permalink,text,timestamp")
	params.Set("limit", fmt.Sprintf("%d", limit))
	if after = strings.TrimSpace(after); after != "" {
		params.Set("after", after)
	}
	endpoint := graphBaseURL + "/profile_posts?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, "", err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, "", err
	}
	if status != http.StatusOK {
		return nil, "", parseAPIError(body, status)
	}
	var payload struct {
		Data   []MediaItem `json:"data"`
		Paging struct {
			Cursors struct {
				After string `json:"after"`
			} `json:"cursors"`
		} `json:"paging"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, "", fmt.Errorf("parse profile_posts: %w", err)
	}
	return payload.Data, strings.TrimSpace(payload.Paging.Cursors.After), nil
}

// FindMediaIDByAuthorShortcode scans profile_posts pages until shortcode is found.
func (c *Client) FindMediaIDByAuthorShortcode(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")
	}
	wantSuffix := "/post/" + shortcode
	after := ""
	for page := 0; page < 3; page++ {
		items, next, err := c.ProfilePosts(ctx, username, 50, after)
		if err != nil {
			return "", err
		}
		for _, item := range items {
			id := strings.TrimSpace(item.ID)
			if !IsNumericMediaID(id) {
				continue
			}
			if strings.EqualFold(strings.TrimSpace(item.Shortcode), shortcode) {
				return id, nil
			}
			link := strings.TrimSpace(item.Permalink)
			if link != "" && strings.HasSuffix(strings.Split(link, "?")[0], wantSuffix) {
				return id, nil
			}
		}
		if next == "" || len(items) == 0 {
			break
		}
		after = next
	}
	return "", fmt.Errorf("profile_posts did not include @%s post %s", username, shortcode)
}

// ProfileLookup fetches a public profile by username (threads_profile_discovery).
func (c *Client) ProfileLookup(ctx context.Context, username string) (*ProfileLookupResult, error) {
	if !c.Enabled() {
		return nil, fmt.Errorf("threads api access token is required")
	}
	username = strings.TrimPrefix(strings.TrimSpace(username), "@")
	if username == "" {
		return nil, fmt.Errorf("username is required")
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("username", username)
	params.Set("fields", "username,name,profile_picture_url,biography,follower_count,likes_count,quotes_count,reposts_count,views_count,is_verified")
	endpoint := graphBaseURL + "/profile_lookup?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var out ProfileLookupResult
	if err := json.Unmarshal(body, &out); err != nil {
		return nil, fmt.Errorf("parse profile_lookup: %w", err)
	}
	return &out, nil
}

type mentionItemRaw 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"`
	RootPost    json.RawMessage `json:"root_post"`
	ParentID    string          `json:"parent_id"`
}

func normalizeMentionItem(raw mentionItemRaw) MentionItem {
	return MentionItem{
		ID:          strings.TrimSpace(raw.ID),
		Text:        strings.TrimSpace(raw.Text),
		Username:    strings.TrimSpace(raw.Username),
		Permalink:   strings.TrimSpace(raw.Permalink),
		Timestamp:   strings.TrimSpace(raw.Timestamp),
		MediaType:   strings.TrimSpace(raw.MediaType),
		IsReply:     raw.IsReply,
		IsQuotePost: raw.IsQuotePost,
		HasReplies:  raw.HasReplies,
		RootPostID:  parseMediaRefID(raw.RootPost),
		ParentID:    strings.TrimSpace(raw.ParentID),
	}
}

// UserMentions lists the first page of posts mentioning the user (threads_manage_mentions).
func (c *Client) UserMentions(ctx context.Context, userID string, limit int) ([]MentionItem, error) {
	items, _, err := c.UserMentionsPage(ctx, userID, limit, "")
	return items, err
}

// UserMentionsPage lists one cursor page of mentions. nextAfter is empty when there is no next page.
func (c *Client) UserMentionsPage(ctx context.Context, userID string, limit int, after string) ([]MentionItem, string, error) {
	if !c.Enabled() {
		return nil, "", fmt.Errorf("threads api access token is required")
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return nil, "", fmt.Errorf("threads user id is required")
	}
	if limit <= 0 {
		limit = 25
	}
	if limit > 100 {
		limit = 100
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("fields", "id,text,username,permalink,timestamp,media_type,is_reply,is_quote_post,has_replies,root_post,parent_id")
	params.Set("limit", fmt.Sprintf("%d", limit))
	if after = strings.TrimSpace(after); after != "" {
		params.Set("after", after)
	}
	endpoint := graphBaseURL + "/" + url.PathEscape(userID) + "/mentions?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, "", err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, "", err
	}
	if status != http.StatusOK {
		return nil, "", parseAPIError(body, status)
	}
	var payload struct {
		Data   []mentionItemRaw `json:"data"`
		Paging struct {
			Cursors struct {
				After string `json:"after"`
			} `json:"cursors"`
		} `json:"paging"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, "", fmt.Errorf("parse mentions: %w", err)
	}
	out := make([]MentionItem, 0, len(payload.Data))
	for _, raw := range payload.Data {
		out = append(out, normalizeMentionItem(raw))
	}
	return out, strings.TrimSpace(payload.Paging.Cursors.After), nil
}

// LocationSearch searches public locations (threads_location_tagging).
func (c *Client) LocationSearch(ctx context.Context, query string, limit int) ([]LocationItem, error) {
	if !c.Enabled() {
		return nil, fmt.Errorf("threads api access token is required")
	}
	query = strings.TrimSpace(query)
	if query == "" {
		return nil, fmt.Errorf("location query is required")
	}
	if limit <= 0 {
		limit = 10
	}
	if limit > 50 {
		limit = 50
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("q", query)
	params.Set("fields", "id,name")
	params.Set("limit", fmt.Sprintf("%d", limit))
	endpoint := graphBaseURL + "/location_search?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var payload struct {
		Data []LocationItem `json:"data"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("parse location_search: %w", err)
	}
	return payload.Data, nil
}

// ShareToInstagram cross-posts a Threads media to linked Instagram (threads_share_to_instagram).
func (c *Client) ShareToInstagram(ctx context.Context, mediaID string) error {
	if !c.Enabled() {
		return fmt.Errorf("threads api access token is required")
	}
	mediaID = strings.TrimSpace(mediaID)
	if mediaID == "" {
		return fmt.Errorf("threads media id is required")
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	endpoint := graphBaseURL + "/" + url.PathEscape(mediaID) + "/crosspost?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
	if err != nil {
		return err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return err
	}
	if status != http.StatusOK {
		return parseAPIError(body, status)
	}
	return nil
}

// HideReply hides a reply on the user's thread (threads_manage_replies).
func (c *Client) HideReply(ctx context.Context, replyID string, hide bool) error {
	if !c.Enabled() {
		return fmt.Errorf("threads api access token is required")
	}
	replyID = strings.TrimSpace(replyID)
	if replyID == "" {
		return fmt.Errorf("reply id is required")
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("hide", fmt.Sprintf("%t", hide))
	endpoint := graphBaseURL + "/" + url.PathEscape(replyID) + "?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
	if err != nil {
		return err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return err
	}
	if status != http.StatusOK {
		return parseAPIError(body, status)
	}
	return nil
}

// MediaInsights fetches per-post lifetime metrics (threads_manage_insights).
func (c *Client) MediaInsights(ctx context.Context, mediaID string, metrics []string) (map[string]int, error) {
	if !c.Enabled() {
		return nil, fmt.Errorf("threads api access token is required")
	}
	mediaID = strings.TrimSpace(mediaID)
	if mediaID == "" {
		return nil, fmt.Errorf("threads media id is required")
	}
	if len(metrics) == 0 {
		metrics = []string{"views", "likes", "replies", "reposts", "quotes", "shares"}
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("metric", strings.Join(metrics, ","))
	endpoint := graphBaseURL + "/" + url.PathEscape(mediaID) + "/insights?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var payload struct {
		Data []InsightValue `json:"data"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("parse media insights: %w", err)
	}
	out := make(map[string]int, len(payload.Data))
	for _, item := range payload.Data {
		name := strings.TrimSpace(strings.ToLower(item.Name))
		if name == "" || len(item.Values) == 0 {
			continue
		}
		out[name] = item.Values[0].Value
	}
	return out, nil
}

// UserInsights fetches profile-level insights (threads_manage_insights).
func (c *Client) UserInsights(ctx context.Context, userID string, metrics []string) ([]InsightValue, error) {
	if !c.Enabled() {
		return nil, fmt.Errorf("threads api access token is required")
	}
	userID = strings.TrimSpace(userID)
	if userID == "" {
		return nil, fmt.Errorf("threads user id is required")
	}
	if len(metrics) == 0 {
		metrics = []string{"views", "likes", "replies", "reposts", "quotes", "followers_count"}
	}
	params := url.Values{}
	params.Set("access_token", c.accessToken)
	params.Set("metric", strings.Join(metrics, ","))
	endpoint := graphBaseURL + "/" + url.PathEscape(userID) + "/threads_insights?" + params.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	body, status, err := doRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var payload struct {
		Data []InsightValue `json:"data"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("parse threads_insights: %w", err)
	}
	return payload.Data, nil
}

func (c *Client) getMediaList(ctx context.Context, endpoint string) ([]MediaItem, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	res, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return nil, err
	}
	if res.StatusCode != http.StatusOK {
		return nil, parseAPIError(body, res.StatusCode)
	}
	var payload struct {
		Data []MediaItem `json:"data"`
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("parse threads media list: %w", err)
	}
	return payload.Data, nil
}
