package threadsapi

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

const (
	threadsAuthorizeURL      = "https://threads.net/oauth/authorize"
	threadsShortTokenURL     = "https://graph.threads.net/oauth/access_token"
	threadsLongLivedTokenURL = "https://graph.threads.net/access_token"
)

// flexString unmarshals JSON string or number (Meta often returns user_id as a number).
type flexString string

func (s *flexString) UnmarshalJSON(data []byte) error {
	if len(data) == 0 || string(data) == "null" {
		*s = ""
		return nil
	}
	if data[0] == '"' {
		var str string
		if err := json.Unmarshal(data, &str); err != nil {
			return err
		}
		*s = flexString(str)
		return nil
	}
	var num json.Number
	if err := json.Unmarshal(data, &num); err != nil {
		return err
	}
	*s = flexString(num.String())
	return nil
}

func (s flexString) String() string {
	return strings.TrimSpace(string(s))
}

type OAuthConfig struct {
	AppID           string
	AppSecret       string
	RedirectURI     string
	SuccessRedirect string
}

func (c OAuthConfig) Enabled() bool {
	return strings.TrimSpace(c.AppID) != "" &&
		strings.TrimSpace(c.AppSecret) != "" &&
		strings.TrimSpace(c.RedirectURI) != ""
}

type TokenExchangeResult struct {
	AccessToken string     `json:"access_token"`
	TokenType   string     `json:"token_type"`
	ExpiresIn   int        `json:"expires_in"`
	UserID      flexString `json:"user_id"`
}

type UserProfile struct {
	ID       flexString `json:"id"`
	Username string     `json:"username"`
	Name     string     `json:"name"`
}

func BuildAuthorizeURL(cfg OAuthConfig, state string) (string, error) {
	if !cfg.Enabled() {
		return "", fmt.Errorf("threads oauth is not configured")
	}
	state = strings.TrimSpace(state)
	if state == "" {
		return "", fmt.Errorf("oauth state is required")
	}
	params := url.Values{}
	params.Set("client_id", strings.TrimSpace(cfg.AppID))
	params.Set("redirect_uri", strings.TrimSpace(cfg.RedirectURI))
	params.Set("scope", OAuthScopeString())
	params.Set("response_type", "code")
	params.Set("state", state)
	return threadsAuthorizeURL + "?" + params.Encode(), nil
}

func ExchangeCodeForToken(ctx context.Context, cfg OAuthConfig, code string) (*TokenExchangeResult, error) {
	if !cfg.Enabled() {
		return nil, fmt.Errorf("threads oauth is not configured")
	}
	code = strings.TrimSpace(code)
	if code == "" {
		return nil, fmt.Errorf("authorization code is required")
	}
	// Meta may append #_ to the redirect code; strip suffix if present.
	if idx := strings.Index(code, "#_"); idx >= 0 {
		code = code[:idx]
	}

	params := url.Values{}
	params.Set("client_id", strings.TrimSpace(cfg.AppID))
	params.Set("client_secret", strings.TrimSpace(cfg.AppSecret))
	params.Set("grant_type", "authorization_code")
	params.Set("redirect_uri", strings.TrimSpace(cfg.RedirectURI))
	params.Set("code", code)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, threadsShortTokenURL+"?"+params.Encode(), nil)
	if err != nil {
		return nil, err
	}

	body, status, err := oauthDoRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var result TokenExchangeResult
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("parse threads token response: %w", err)
	}
	if strings.TrimSpace(result.AccessToken) == "" {
		return nil, fmt.Errorf("threads token exchange returned empty access_token")
	}
	return &result, nil
}

func ExchangeLongLivedToken(ctx context.Context, cfg OAuthConfig, shortLivedToken string) (*TokenExchangeResult, error) {
	if !cfg.Enabled() {
		return nil, fmt.Errorf("threads oauth is not configured")
	}
	shortLivedToken = strings.TrimSpace(shortLivedToken)
	if shortLivedToken == "" {
		return nil, fmt.Errorf("short-lived access token is required")
	}

	params := url.Values{}
	params.Set("grant_type", "th_exchange_token")
	params.Set("client_secret", strings.TrimSpace(cfg.AppSecret))
	params.Set("access_token", shortLivedToken)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, threadsLongLivedTokenURL+"?"+params.Encode(), nil)
	if err != nil {
		return nil, err
	}
	body, status, err := oauthDoRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var result TokenExchangeResult
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("parse long-lived token response: %w", err)
	}
	if strings.TrimSpace(result.AccessToken) == "" {
		return nil, fmt.Errorf("long-lived token exchange returned empty access_token")
	}
	return &result, nil
}

func GetUserProfile(ctx context.Context, accessToken, userID string) (*UserProfile, error) {
	accessToken = strings.TrimSpace(accessToken)
	userID = strings.TrimSpace(userID)
	if accessToken == "" {
		return nil, fmt.Errorf("access token is required")
	}
	if userID == "" {
		return GetMeProfile(ctx, accessToken)
	}

	params := url.Values{}
	params.Set("access_token", accessToken)
	params.Set("fields", "id,username,name")

	endpoint := graphBaseURL + "/" + url.PathEscape(userID) + "?" + params.Encode()
	return fetchUserProfile(ctx, endpoint)
}

// GetMeProfile loads the authenticated Threads user via /me (preferred after OAuth).
func GetMeProfile(ctx context.Context, accessToken string) (*UserProfile, error) {
	accessToken = strings.TrimSpace(accessToken)
	if accessToken == "" {
		return nil, fmt.Errorf("access token is required")
	}
	params := url.Values{}
	params.Set("access_token", accessToken)
	params.Set("fields", "id,username,name")
	endpoint := graphBaseURL + "/me?" + params.Encode()
	return fetchUserProfile(ctx, endpoint)
}

// ThreadsUserIDFromToken picks user_id returned by the OAuth token exchange.
func ThreadsUserIDFromToken(short, long *TokenExchangeResult) string {
	if short != nil {
		if id := short.UserID.String(); id != "" {
			return id
		}
	}
	if long != nil {
		return long.UserID.String()
	}
	return ""
}

// IsPermissionError reports Meta permission / app-review / tester-list errors.
func IsPermissionError(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "threads_basic") ||
		strings.Contains(msg, "app review") ||
		strings.Contains(msg, "testers")
}

func fetchUserProfile(ctx context.Context, endpoint string) (*UserProfile, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	body, status, err := oauthDoRequest(req)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, parseAPIError(body, status)
	}
	var profile UserProfile
	if err := json.Unmarshal(body, &profile); err != nil {
		return nil, fmt.Errorf("parse threads user profile: %w", err)
	}
	if profile.ID.String() == "" {
		return nil, fmt.Errorf("threads user profile missing id")
	}
	return &profile, nil
}

func oauthDoRequest(req *http.Request) ([]byte, int, error) {
	client := &http.Client{Timeout: 25 * time.Second}
	res, err := client.Do(req)
	if err != nil {
		return nil, 0, err
	}
	defer res.Body.Close()
	body, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
	if err != nil {
		return nil, res.StatusCode, err
	}
	return body, res.StatusCode, nil
}
