package usecase

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"time"

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

	"github.com/google/uuid"
)

// TokenRenewScheduler schedules delayed threads_token_renew jobs (optional).
type TokenRenewScheduler interface {
	ScheduleTokenRenew(ctx context.Context, ownerUID int64, accountID string, runAfter int64) error
}

// Service implements Threads OAuth + account lifecycle.
type Service struct {
	Repo        domain.Repository
	Provider    domain.Provider
	TokenSecret string
	// APIPublicBase for building fake authorize → callback (e.g. http://127.0.0.1:8888)
	APIPublicBase string
	// CallbackPath relative or absolute redirect_uri registered with Meta
	CallbackPath string // default /api/v1/threads-accounts/oauth/callback
	// WebBase for post-callback browser redirect
	WebBase string
	// Renew schedules worker job ~30d after bind (optional)
	Renew TokenRenewScheduler
}

func New(repo domain.Repository, provider domain.Provider, tokenSecret string) *Service {
	return &Service{
		Repo: repo, Provider: provider, TokenSecret: tokenSecret,
		CallbackPath: "/api/v1/threads-accounts/oauth/callback",
	}
}

func (s *Service) callbackURI() string {
	if s.CallbackPath == "" {
		s.CallbackPath = "/api/v1/threads-accounts/oauth/callback"
	}
	if len(s.CallbackPath) > 0 && s.CallbackPath[0] == '/' {
		base := trimSlash(s.APIPublicBase)
		if base == "" {
			return s.CallbackPath
		}
		return base + s.CallbackPath
	}
	return s.CallbackPath
}

func trimSlash(s string) string {
	for len(s) > 0 && s[len(s)-1] == '/' {
		s = s[:len(s)-1]
	}
	return s
}

func randomState() string {
	var b [16]byte
	_, _ = rand.Read(b[:])
	return hex.EncodeToString(b[:])
}

// OAuthStart — TH-01
func (s *Service) OAuthStart(ctx context.Context, ownerUID int64) (authorizeURL, state string, err error) {
	if ownerUID <= 0 {
		return "", "", domain.ErrForbidden
	}
	if s.Provider == nil || s.Provider.Name() == "unavailable" {
		return "", "", fmt.Errorf("Threads OAuth is not configured")
	}
	state = randomState()
	now := domain.NowNano()
	st := &domain.OAuthState{
		State: state, OwnerUID: ownerUID,
		CreatedAt: now, ExpiresAt: now + int64(15*time.Minute),
	}
	if err := s.Repo.SaveState(ctx, st); err != nil {
		return "", "", err
	}
	uri := s.callbackURI()
	return s.Provider.AuthorizeURL(state, uri), state, nil
}

// OAuthCallback — TH-02 / TH-03 / TH-04
func (s *Service) OAuthCallback(ctx context.Context, code, state, oauthErr string) (*domain.Account, error) {
	if oauthErr != "" {
		return nil, domain.ErrOAuthDenied
	}
	if state == "" {
		return nil, domain.ErrInvalidState
	}
	st, err := s.Repo.TakeState(ctx, state)
	if err != nil {
		return nil, err
	}
	if code == "" {
		return nil, domain.ErrOAuthExchange
	}
	tok, err := s.Provider.ExchangeCode(ctx, code, s.callbackURI())
	if err != nil {
		return nil, domain.ErrOAuthExchange
	}
	accEnc, err := domain.Seal(s.TokenSecret, tok.AccessToken)
	if err != nil {
		return nil, err
	}
	refEnc, err := domain.Seal(s.TokenSecret, tok.RefreshToken)
	if err != nil {
		return nil, err
	}
	if tok.RefreshToken == "" {
		// long-lived access doubles as refresh handle
		refEnc, _ = domain.Seal(s.TokenSecret, tok.AccessToken)
	}
	now := domain.NowNano()
	exp := expiresAtFromSeconds(now, tok.ExpiresIn)
	username := tok.Username
	if username == "" {
		username = "user_" + tok.UserID
	}
	display := tok.DisplayName
	if display == "" {
		display = username
	}

	// Same Threads user under same member → overwrite session (do not create duplicates).
	if tok.UserID != "" {
		if existing, ferr := s.Repo.FindByOwnerAndThreadsUser(ctx, st.OwnerUID, tok.UserID); ferr == nil && existing != nil {
			existing.Username = username
			existing.DisplayName = display
			existing.ThreadsUserID = tok.UserID
			existing.Connection = domain.ConnectionConnected
			existing.IsUsable = true
			existing.ErrorMessage = ""
			existing.AvatarColor = domain.AvatarColorFor(username)
			if tok.AvatarURL != "" {
				existing.AvatarURL = tok.AvatarURL
			}
			existing.SessionExpiresAt = exp
			existing.SessionRefreshedAt = now
			existing.AccessTokenEnc = accEnc
			existing.RefreshTokenEnc = refEnc
			existing.UpdatedAt = now
			if err := s.Repo.Update(ctx, existing); err != nil {
				return nil, err
			}
			s.scheduleRenew(ctx, existing)
			return existing, nil
		}
	}

	a := &domain.Account{
		ID: uuid.NewString(), OwnerUID: st.OwnerUID,
		Username: username, DisplayName: display, ThreadsUserID: tok.UserID,
		Connection: domain.ConnectionConnected, IsUsable: true,
		AvatarColor: domain.AvatarColorFor(username), AvatarURL: tok.AvatarURL,
		SessionExpiresAt: exp, SessionRefreshedAt: now,
		AccessTokenEnc: accEnc, RefreshTokenEnc: refEnc,
		CreatedAt: now, UpdatedAt: now,
	}
	if err := s.Repo.Insert(ctx, a); err != nil {
		return nil, err
	}
	s.scheduleRenew(ctx, a)
	return a, nil
}

func (s *Service) scheduleRenew(ctx context.Context, a *domain.Account) {
	if s == nil || s.Renew == nil || a == nil {
		return
	}
	// 約 30 天後 renew（長效 token ~60 天；若到期更近則提前）
	runAfter := domain.NowNano() + int64(30*24*time.Hour)
	if a.SessionExpiresAt > 0 {
		// 到期前 30 天；若不足 7 天則到期前 1 天
		early := a.SessionExpiresAt - int64(30*24*time.Hour)
		minLead := a.SessionExpiresAt - int64(7*24*time.Hour)
		if early > domain.NowNano() {
			runAfter = early
		} else if minLead > domain.NowNano() {
			runAfter = minLead
		} else {
			runAfter = domain.NowNano() + int64(time.Hour) // 幾乎到期，盡快
		}
	}
	_ = s.Renew.ScheduleTokenRenew(ctx, a.OwnerUID, a.ID, runAfter)
}

// List — TH-05
func (s *Service) List(ctx context.Context, ownerUID int64) ([]*domain.Account, error) {
	return s.Repo.ListByOwner(ctx, ownerUID)
}

// Get returns account by id (no ownership check — caller enforces).
func (s *Service) Get(ctx context.Context, accountID string) (*domain.Account, error) {
	return s.Repo.FindByID(ctx, accountID)
}

// AccessToken decrypts stored access token for publish transport.
func (s *Service) AccessToken(_ context.Context, a *domain.Account) (string, error) {
	if a == nil {
		return "", domain.ErrNotFound
	}
	return domain.Open(s.TokenSecret, a.AccessTokenEnc)
}

// Refresh — TH-06 / TH-07 / TH-10
// 用庫裡已存的長效 token 向 Threads 換新 token（不開授權頁、不重新 OAuth）。
// 若 token 已失效無法 refresh，標記 error，需使用者再按「連帳」走授權。
func (s *Service) Refresh(ctx context.Context, ownerUID int64, accountID string) (*domain.Account, error) {
	a, err := s.Repo.FindByID(ctx, accountID)
	if err != nil {
		return nil, err
	}
	if a.OwnerUID != ownerUID {
		return nil, domain.ErrForbidden
	}
	// 優先用 refresh 把手；沒有則退回 access（兩者在長效模型下通常相同）
	ref, err := domain.Open(s.TokenSecret, a.RefreshTokenEnc)
	if err != nil || ref == "" {
		ref, err = domain.Open(s.TokenSecret, a.AccessTokenEnc)
	}
	if err != nil || ref == "" {
		a.Connection = domain.ConnectionError
		a.IsUsable = false
		a.ErrorMessage = "no stored token — reconnect account"
		_ = s.Repo.Update(ctx, a)
		return a, domain.ErrRefreshFailed
	}
	tok, err := s.Provider.Refresh(ctx, ref)
	if err != nil {
		a.Connection = domain.ConnectionError
		a.IsUsable = false
		a.ErrorMessage = "token refresh failed — reconnect account"
		_ = s.Repo.Update(ctx, a)
		return a, domain.ErrRefreshFailed
	}
	a.AccessTokenEnc, _ = domain.Seal(s.TokenSecret, tok.AccessToken)
	// 長效 refresh：新 token 同時當下次 refresh 把手
	handle := tok.RefreshToken
	if handle == "" {
		handle = tok.AccessToken
	}
	a.RefreshTokenEnc, _ = domain.Seal(s.TokenSecret, handle)
	now := domain.NowNano()
	a.SessionRefreshedAt = now
	a.SessionExpiresAt = expiresAtFromSeconds(now, tok.ExpiresIn)
	a.Connection = domain.ConnectionConnected
	a.IsUsable = true
	a.ErrorMessage = ""
	if tok.Username != "" {
		a.Username = tok.Username
	}
	if tok.DisplayName != "" {
		a.DisplayName = tok.DisplayName
	}
	a.UpdatedAt = now
	if err := s.Repo.Update(ctx, a); err != nil {
		return nil, err
	}
	return a, nil
}

// Disconnect — TH-08 / TH-10：真刪綁定（列表不再出現）
func (s *Service) Disconnect(ctx context.Context, ownerUID int64, accountID string) error {
	a, err := s.Repo.FindByID(ctx, accountID)
	if err != nil {
		return err
	}
	if a.OwnerUID != ownerUID {
		return domain.ErrForbidden
	}
	return s.Repo.Delete(ctx, accountID)
}

// WebRedirect builds FE redirect after callback.
// WebRedirect builds where the browser lands after the Threads OAuth round trip.
// An unset WebBase yields a relative path on purpose: falling back to a hardcoded loopback
// address would silently strand every production user on their own machine.
func (s *Service) WebRedirect(ok bool, msg string) string {
	base := trimSlash(s.WebBase)
	if ok {
		return base + "/app/crew?oauth=ok"
	}
	if msg == "" {
		msg = "error"
	}
	return fmt.Sprintf("%s/app/crew?oauth=error&msg=%s", base, urlQueryEscape(msg))
}

// expiresAtFromSeconds: nowUnixNano + expiresInSec → absolute unix nanoseconds.
// expiresInSec<=0 → 預設 60 天（Threads 長效常見）。
func expiresAtFromSeconds(nowNano, expiresInSec int64) int64 {
	const nsPerSec = int64(1_000_000_000)
	if expiresInSec <= 0 {
		expiresInSec = 60 * 24 * 3600
	}
	// 防呆：若誤傳毫秒級（> 10 年以秒計）當毫秒
	if expiresInSec > 10*365*24*3600 {
		expiresInSec = expiresInSec / 1000
	}
	return nowNano + expiresInSec*nsPerSec
}

func urlQueryEscape(s string) string {
	// minimal escape
	r := make([]byte, 0, len(s)*2)
	for i := 0; i < len(s); i++ {
		c := s[i]
		if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' {
			r = append(r, c)
		} else if c == ' ' {
			r = append(r, '+')
		} else {
			r = append(r, '%')
			const hex = "0123456789ABCDEF"
			r = append(r, hex[c>>4], hex[c&15])
		}
	}
	return string(r)
}
