package service

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/echochat/backend/app/auth/dao"
	"github.com/echochat/backend/app/auth/model"
	"github.com/echochat/backend/app/constants"
	"github.com/echochat/backend/app/dto"
	"github.com/echochat/backend/config"
	"github.com/echochat/backend/pkg/utils"
	"github.com/redis/go-redis/v9"
	"gorm.io/gorm"
)

const (
	oauthStateKeyPrefix = "echo:auth:oauth_state:"
	oauthStateTTL       = 10 * time.Minute
)

var (
	ErrOAuthProviderUnsupported = errors.New("不支持的第三方登录方式")
	ErrOAuthProviderDisabled    = errors.New("第三方登录未配置或未启用")
	ErrOAuthStateInvalid        = errors.New("第三方登录状态已失效")
)

type OAuthService struct {
	userDAO         *dao.UserDAO
	oauthAccountDAO *dao.OAuthAccountDAO
	roleDAO         *dao.RoleDAO
	authService     *AuthService
	cfg             *config.OAuthConfig
	redis           *redis.Client
	httpClient      *http.Client
}

type OAuthProfile struct {
	Provider     string
	OpenID       string
	UnionID      string
	Nickname     string
	Avatar       string
	AccessToken  string
	RefreshToken string
	ExpiresIn    int64
}

type oauthTokenResponse struct {
	AccessToken  string `json:"access_token"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
	OpenID       string `json:"openid"`
	UnionID      string `json:"unionid"`
	ErrCode      int    `json:"errcode"`
	ErrMsg       string `json:"errmsg"`
}

type qqTokenResponse struct {
	AccessToken  string `json:"access_token"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
}

type qqOpenIDResponse struct {
	ClientID string `json:"client_id"`
	OpenID   string `json:"openid"`
}

type wxUserInfoResponse struct {
	OpenID   string `json:"openid"`
	Nickname string `json:"nickname"`
	HeadImg  string `json:"headimgurl"`
	UnionID  string `json:"unionid"`
	ErrCode  int    `json:"errcode"`
	ErrMsg   string `json:"errmsg"`
}

type qqUserInfoResponse struct {
	Ret      int    `json:"ret"`
	Msg      string `json:"msg"`
	Nickname string `json:"nickname"`
	Avatar   string `json:"figureurl_qq_2"`
	Avatar1  string `json:"figureurl_qq_1"`
}

func NewOAuthService(userDAO *dao.UserDAO, oauthAccountDAO *dao.OAuthAccountDAO, roleDAO *dao.RoleDAO, authService *AuthService, cfg *config.OAuthConfig, redisClient *redis.Client) *OAuthService {
	return &OAuthService{
		userDAO:         userDAO,
		oauthAccountDAO: oauthAccountDAO,
		roleDAO:         roleDAO,
		authService:     authService,
		cfg:             cfg,
		redis:           redisClient,
		httpClient:      &http.Client{Timeout: 10 * time.Second},
	}
}

func (s *OAuthService) BuildAuthorizeURL(ctx context.Context, provider string) (string, error) {
	provider = normalizeOAuthProvider(provider)
	app, err := s.providerConfig(provider)
	if err != nil {
		return "", err
	}
	state, err := randomState()
	if err != nil {
		return "", err
	}
	if err = s.saveOAuthState(ctx, provider, state); err != nil {
		return "", err
	}
	redirectURI := url.QueryEscape(app.RedirectURI)
	scope := "snsapi_login"
	if provider == "qq" {
		scope = "get_user_info"
	}
	if provider == "wechat" {
		return fmt.Sprintf("https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s#wechat_redirect", url.QueryEscape(app.ClientID), redirectURI, scope, state), nil
	}
	return fmt.Sprintf("https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s", url.QueryEscape(app.ClientID), redirectURI, url.QueryEscape(scope), state), nil
}

func (s *OAuthService) Callback(ctx context.Context, provider, code, state, clientIP string) (*dto.LoginResponse, error) {
	provider = normalizeOAuthProvider(provider)
	if err := s.validateOAuthState(ctx, provider, state); err != nil {
		return nil, err
	}
	profile, err := s.fetchProfile(ctx, provider, code)
	if err != nil {
		return nil, err
	}
	account, err := s.oauthAccountDAO.FindByProviderOpenID(ctx, profile.Provider, profile.OpenID)
	var user *model.User
	if err == nil {
		user, err = s.userDAO.FindByID(ctx, account.UserID)
		if err != nil {
			return nil, err
		}
		account.Nickname = profile.Nickname
		account.Avatar = profile.Avatar
		account.AccessToken = profile.AccessToken
		account.RefreshToken = profile.RefreshToken
		account.UnionID = profile.UnionID
		account.ExpiresAt = time.Now().Add(time.Duration(profile.ExpiresIn) * time.Second)
		_ = s.oauthAccountDAO.Update(ctx, account)
	} else if errors.Is(err, gorm.ErrRecordNotFound) {
		user, account, err = s.createOAuthUser(ctx, profile)
		if err != nil {
			return nil, err
		}
	} else {
		return nil, err
	}
	return s.authService.BuildLoginResponseForUser(ctx, user, clientIP, constants.ClientTypeFrontend)
}

func (s *OAuthService) createOAuthUser(ctx context.Context, profile *OAuthProfile) (*model.User, *model.OAuthAccount, error) {
	nickname := strings.TrimSpace(profile.Nickname)
	if nickname == "" {
		nickname = profile.Provider + "用户"
	}
	username := s.uniqueUsername(ctx, profile.Provider)
	passwordHash, err := utils.HashPassword(randomPassword())
	if err != nil {
		return nil, nil, err
	}
	user := &model.User{
		Username:     username,
		Email:        username + "@oauth.echochat.local",
		PasswordHash: passwordHash,
		Nickname:     nickname,
		Avatar:       profile.Avatar,
		Status:       constants.UserStatusActive,
	}
	if err = s.userDAO.Create(ctx, user); err != nil {
		return nil, nil, err
	}
	defaultRole, roleErr := s.roleDAO.FindByCode(ctx, constants.RoleUser)
	if roleErr == nil {
		_ = s.roleDAO.AssignRole(ctx, user.ID, defaultRole.ID)
	}
	account := &model.OAuthAccount{
		UserID:       user.ID,
		Provider:     profile.Provider,
		OpenID:       profile.OpenID,
		UnionID:      profile.UnionID,
		Nickname:     profile.Nickname,
		Avatar:       profile.Avatar,
		AccessToken:  profile.AccessToken,
		RefreshToken: profile.RefreshToken,
		ExpiresAt:    time.Now().Add(time.Duration(profile.ExpiresIn) * time.Second),
	}
	if err = s.oauthAccountDAO.Create(ctx, account); err != nil {
		return nil, nil, err
	}
	return user, account, nil
}

func (s *OAuthService) fetchProfile(ctx context.Context, provider, code string) (*OAuthProfile, error) {
	app, err := s.providerConfig(provider)
	if err != nil {
		return nil, err
	}
	if provider == "wechat" {
		return s.fetchWechatProfile(ctx, app, code)
	}
	return s.fetchQQProfile(ctx, app, code)
}

func (s *OAuthService) fetchWechatProfile(ctx context.Context, app config.OAuthApp, code string) (*OAuthProfile, error) {
	tokenURL := fmt.Sprintf("https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code", url.QueryEscape(app.ClientID), url.QueryEscape(app.ClientSecret), url.QueryEscape(code))
	var token oauthTokenResponse
	if err := s.getJSON(ctx, tokenURL, &token); err != nil {
		return nil, err
	}
	if token.ErrCode != 0 || token.AccessToken == "" || token.OpenID == "" {
		return nil, fmt.Errorf("微信登录失败: %s", token.ErrMsg)
	}
	infoURL := fmt.Sprintf("https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN", url.QueryEscape(token.AccessToken), url.QueryEscape(token.OpenID))
	var info wxUserInfoResponse
	if err := s.getJSON(ctx, infoURL, &info); err != nil {
		return nil, err
	}
	if info.ErrCode != 0 {
		return nil, fmt.Errorf("微信用户信息获取失败: %s", info.ErrMsg)
	}
	return &OAuthProfile{Provider: "wechat", OpenID: token.OpenID, UnionID: firstNonEmpty(info.UnionID, token.UnionID), Nickname: info.Nickname, Avatar: info.HeadImg, AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresIn: token.ExpiresIn}, nil
}

func (s *OAuthService) fetchQQProfile(ctx context.Context, app config.OAuthApp, code string) (*OAuthProfile, error) {
	tokenURL := fmt.Sprintf("https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=%s&client_secret=%s&code=%s&redirect_uri=%s&fmt=json", url.QueryEscape(app.ClientID), url.QueryEscape(app.ClientSecret), url.QueryEscape(code), url.QueryEscape(app.RedirectURI))
	var token qqTokenResponse
	if err := s.getJSON(ctx, tokenURL, &token); err != nil {
		return nil, err
	}
	if token.AccessToken == "" {
		return nil, errors.New("QQ登录失败")
	}
	openidURL := fmt.Sprintf("https://graph.qq.com/oauth2.0/me?access_token=%s&fmt=json", url.QueryEscape(token.AccessToken))
	var openid qqOpenIDResponse
	if err := s.getJSON(ctx, openidURL, &openid); err != nil {
		return nil, err
	}
	if openid.OpenID == "" {
		return nil, errors.New("QQ openid 获取失败")
	}
	infoURL := fmt.Sprintf("https://graph.qq.com/user/get_user_info?access_token=%s&oauth_consumer_key=%s&openid=%s&fmt=json", url.QueryEscape(token.AccessToken), url.QueryEscape(app.ClientID), url.QueryEscape(openid.OpenID))
	var info qqUserInfoResponse
	if err := s.getJSON(ctx, infoURL, &info); err != nil {
		return nil, err
	}
	if info.Ret != 0 {
		return nil, fmt.Errorf("QQ用户信息获取失败: %s", info.Msg)
	}
	return &OAuthProfile{Provider: "qq", OpenID: openid.OpenID, Nickname: info.Nickname, Avatar: firstNonEmpty(info.Avatar, info.Avatar1), AccessToken: token.AccessToken, RefreshToken: token.RefreshToken, ExpiresIn: token.ExpiresIn}, nil
}

func (s *OAuthService) providerConfig(provider string) (config.OAuthApp, error) {
	provider = normalizeOAuthProvider(provider)
	if provider != "wechat" && provider != "qq" {
		return config.OAuthApp{}, ErrOAuthProviderUnsupported
	}
	app := config.OAuthApp{}
	if s.cfg != nil && s.cfg.Providers != nil {
		app = s.cfg.Providers[provider]
	}
	prefix := "ECHOCHAT_OAUTH_" + strings.ToUpper(provider) + "_"
	if strings.TrimSpace(app.ClientID) == "" {
		app.ClientID = strings.TrimSpace(os.Getenv(prefix + "CLIENT_ID"))
	}
	if strings.TrimSpace(app.ClientSecret) == "" {
		app.ClientSecret = strings.TrimSpace(os.Getenv(prefix + "CLIENT_SECRET"))
	}
	if strings.TrimSpace(app.RedirectURI) == "" {
		app.RedirectURI = strings.TrimSpace(os.Getenv(prefix + "REDIRECT_URI"))
	}
	if !app.Enabled {
		app.Enabled = strings.EqualFold(strings.TrimSpace(os.Getenv(prefix+"ENABLED")), "true")
	}
	if !app.Enabled || app.ClientID == "" || app.ClientSecret == "" || app.RedirectURI == "" {
		return config.OAuthApp{}, ErrOAuthProviderDisabled
	}
	return app, nil
}

func (s *OAuthService) saveOAuthState(ctx context.Context, provider, state string) error {
	if s.redis == nil {
		return errors.New("OAuth state storage not initialized")
	}
	return s.redis.Set(ctx, oauthStateKey(provider, state), "1", oauthStateTTL).Err()
}

func (s *OAuthService) validateOAuthState(ctx context.Context, provider, state string) error {
	provider = normalizeOAuthProvider(provider)
	state = strings.TrimSpace(state)
	if provider != "wechat" && provider != "qq" || state == "" {
		return ErrOAuthStateInvalid
	}
	if s.redis == nil {
		return ErrOAuthStateInvalid
	}
	_, err := s.redis.Get(ctx, oauthStateKey(provider, state)).Result()
	if errors.Is(err, redis.Nil) {
		return ErrOAuthStateInvalid
	}
	if err != nil {
		return err
	}
	_ = s.redis.Del(ctx, oauthStateKey(provider, state)).Err()
	return nil
}

func oauthStateKey(provider, state string) string {
	return oauthStateKeyPrefix + normalizeOAuthProvider(provider) + ":" + strings.TrimSpace(state)
}

func normalizeOAuthProvider(provider string) string {
	return strings.ToLower(strings.TrimSpace(provider))
}

func (s *OAuthService) getJSON(ctx context.Context, rawURL string, out interface{}) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
	if err != nil {
		return err
	}
	resp, err := s.httpClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return fmt.Errorf("OAuth HTTP %d", resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

func (s *OAuthService) uniqueUsername(ctx context.Context, provider string) string {
	for i := 0; i < 5; i++ {
		name := fmt.Sprintf("%s_%s", provider, randomString(10))
		if existing, _ := s.userDAO.FindByUsername(ctx, name); existing == nil {
			return name
		}
	}
	return fmt.Sprintf("%s_%d", provider, time.Now().UnixNano())
}

func randomState() (string, error) {
	buf := make([]byte, 16)
	_, err := rand.Read(buf)
	if err != nil {
		return "", err
	}
	return hex.EncodeToString(buf), nil
}

func randomString(n int) string {
	buf := make([]byte, n)
	_, _ = rand.Read(buf)
	return hex.EncodeToString(buf)[:n]
}

func randomPassword() string {
	return "oauth_" + randomString(32)
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return value
		}
	}
	return ""
}
