package provider

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

	"apps/backend/internal/module/threads/domain"
)

// MetaProvider — Meta Threads OAuth (when AppId/Secret configured).
type MetaProvider struct {
	AppID      string
	AppSecret  string
	HTTPClient *http.Client
	AuthBase   string // default https://threads.net
	GraphBase  string // default https://graph.threads.net
}

func NewMeta(appID, appSecret string) *MetaProvider {
	return &MetaProvider{
		AppID: appID, AppSecret: appSecret,
		HTTPClient: &http.Client{Timeout: 20 * time.Second},
		AuthBase:   "https://threads.net",
		GraphBase:  "https://graph.threads.net",
	}
}

func (m *MetaProvider) Name() string { return "meta" }

func (m *MetaProvider) AuthorizeURL(state, redirectURI string) string {
	q := url.Values{}
	q.Set("client_id", m.AppID)
	q.Set("redirect_uri", redirectURI)
	// basic + 發文 + 回覆/對話 + insights + 提及 + 公開人設探索（對標帳號 profile_posts）
	q.Set("scope", "threads_basic,threads_content_publish,threads_read_replies,threads_manage_replies,threads_manage_insights,threads_manage_mentions,threads_profile_discovery")
	q.Set("response_type", "code")
	q.Set("state", state)
	return strings.TrimRight(m.AuthBase, "/") + "/oauth/authorize?" + q.Encode()
}

func (m *MetaProvider) ExchangeCode(ctx context.Context, code, redirectURI string) (*domain.TokenPair, error) {
	// 1) code → short-lived token
	form := url.Values{}
	form.Set("client_id", m.AppID)
	form.Set("client_secret", m.AppSecret)
	form.Set("grant_type", "authorization_code")
	form.Set("redirect_uri", redirectURI)
	form.Set("code", code)
	endpoint := strings.TrimRight(m.GraphBase, "/") + "/oauth/access_token"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	resp, err := m.HTTPClient.Do(req)
	if err != nil {
		return nil, domain.ErrOAuthExchange
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("%w: %s", domain.ErrOAuthExchange, string(body))
	}
	var tok struct {
		AccessToken string `json:"access_token"`
		UserID      int64  `json:"user_id"`
		ExpiresIn   int64  `json:"expires_in"`
	}
	if err := json.Unmarshal(body, &tok); err != nil || tok.AccessToken == "" {
		return nil, domain.ErrOAuthExchange
	}
	// Meta short-lived 常不回 expires_in（約 1h）
	expSec := tok.ExpiresIn
	if expSec <= 0 {
		expSec = 3600
	}
	pair := &domain.TokenPair{
		AccessToken: tok.AccessToken,
		ExpiresIn:   expSec,
		UserID:      fmt.Sprintf("%d", tok.UserID),
	}
	// 2) short-lived → long-lived（之後「延長 token」才能用 th_refresh_token，無需重走授權頁）
	if long, lerr := m.exchangeLongLived(ctx, pair.AccessToken); lerr == nil && long != nil {
		pair.AccessToken = long.AccessToken
		pair.RefreshToken = long.AccessToken // 長效 token 本身就是 refresh 把手
		if long.ExpiresIn > 0 {
			pair.ExpiresIn = long.ExpiresIn
		} else {
			pair.ExpiresIn = 60 * 24 * 3600 // ~60d
		}
	} else {
		// 長效失敗仍回短效，延長 token 可能失敗 → 使用者需重連
		pair.RefreshToken = pair.AccessToken
	}
	_ = m.fillProfile(ctx, pair)
	return pair, nil
}

// exchangeLongLived: GET /access_token?grant_type=th_exchange_token&client_secret=…&access_token=…
func (m *MetaProvider) exchangeLongLived(ctx context.Context, shortLived string) (*domain.TokenPair, error) {
	q := url.Values{}
	q.Set("grant_type", "th_exchange_token")
	q.Set("client_secret", m.AppSecret)
	q.Set("access_token", shortLived)
	u := strings.TrimRight(m.GraphBase, "/") + "/access_token?" + q.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := m.HTTPClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("long-lived exchange failed: %s", string(body))
	}
	var tok struct {
		AccessToken string `json:"access_token"`
		ExpiresIn   int64  `json:"expires_in"`
		TokenType   string `json:"token_type"`
	}
	if err := json.Unmarshal(body, &tok); err != nil || tok.AccessToken == "" {
		return nil, fmt.Errorf("long-lived exchange parse: %s", string(body))
	}
	return &domain.TokenPair{
		AccessToken:  tok.AccessToken,
		RefreshToken: tok.AccessToken,
		ExpiresIn:    tok.ExpiresIn,
	}, nil
}

// Refresh uses the stored long-lived token (not browser re-auth).
// GET /refresh_access_token?grant_type=th_refresh_token&access_token={long-lived}
func (m *MetaProvider) Refresh(ctx context.Context, longLivedToken string) (*domain.TokenPair, error) {
	if strings.TrimSpace(longLivedToken) == "" {
		return nil, domain.ErrRefreshFailed
	}
	q := url.Values{}
	q.Set("grant_type", "th_refresh_token")
	q.Set("access_token", longLivedToken)
	u := strings.TrimRight(m.GraphBase, "/") + "/refresh_access_token?" + q.Encode()
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, err
	}
	resp, err := m.HTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("%w: network", domain.ErrRefreshFailed)
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode >= 300 {
		return nil, fmt.Errorf("%w: %s", domain.ErrRefreshFailed, truncateBody(string(body), 240))
	}
	var tok struct {
		AccessToken string `json:"access_token"`
		ExpiresIn   int64  `json:"expires_in"`
	}
	if err := json.Unmarshal(body, &tok); err != nil || tok.AccessToken == "" {
		return nil, fmt.Errorf("%w: empty token", domain.ErrRefreshFailed)
	}
	// 新長效 token 同時當 access 與下次 refresh 把手
	exp := tok.ExpiresIn
	if exp <= 0 {
		exp = 60 * 24 * 3600
	}
	return &domain.TokenPair{
		AccessToken:  tok.AccessToken,
		RefreshToken: tok.AccessToken,
		ExpiresIn:    exp,
	}, nil
}

func truncateBody(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n] + "…"
}

func (m *MetaProvider) fillProfile(ctx context.Context, pair *domain.TokenPair) error {
	u := strings.TrimRight(m.GraphBase, "/") + "/me?fields=id,username,name,threads_profile_picture_url&access_token=" + url.QueryEscape(pair.AccessToken)
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return err
	}
	resp, err := m.HTTPClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	var p struct {
		ID       string `json:"id"`
		Username string `json:"username"`
		Name     string `json:"name"`
		Pic      string `json:"threads_profile_picture_url"`
	}
	if err := json.Unmarshal(body, &p); err != nil {
		return err
	}
	if p.ID != "" {
		pair.UserID = p.ID
	}
	pair.Username = p.Username
	pair.DisplayName = p.Name
	if pair.DisplayName == "" {
		pair.DisplayName = p.Username
	}
	pair.AvatarURL = p.Pic
	return nil
}

var _ domain.Provider = (*MetaProvider)(nil)
