package identity

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

const socialStateTTL = 5 * time.Minute

// ErrSocialUnbound 表示平台账号未绑定本系统账号(且未开启自动开通)。
var ErrSocialUnbound = errors.New("该企业账号尚未绑定本系统账号，请先用账号密码登录后在「账号安全」中绑定")

type socialIdentity struct {
	UID  string // 平台稳定唯一标识(userid/unionId/union_id)
	Name string
}

type socialChallenge struct {
	ProviderID string `json:"provider_id"`
	Purpose    string `json:"purpose"` // login | bind
	BindUserID string `json:"bind_user_id,omitempty"`
}

type SocialLoginResult struct {
	Purpose      string // login | bind
	SSOCode      string // login 成功后的一次性交换码(前端换取会话)
	BindOK       bool   // bind 流程是否成功
	BindConflict bool   // bind 流程:该平台账号已被他人绑定
}

// socialKindSupported 校验扫码登录平台 kind。
func socialKindSupported(kind string) bool {
	switch strings.ToLower(strings.TrimSpace(kind)) {
	case "wecom", "dingtalk", "feishu":
		return true
	}
	return false
}

// SocialLoginURL 构造跳转企业身份源的登录/绑定 URL。
func (s *Service) SocialLoginURL(ctx context.Context, kind, purpose, bindUserID string) (string, error) {
	if !socialKindSupported(kind) {
		return "", errors.New("不支持的扫码登录平台")
	}
	provider, err := s.repository.GetSocialProviderByKind(ctx, kind)
	if err != nil {
		return "", err
	}
	if !provider.Enabled {
		return "", errors.New("该登录方式未启用")
	}
	if provider.ClientID == "" || provider.RedirectURI == "" || provider.PortalReturnURL == "" {
		return "", errors.New("身份源配置不完整")
	}
	if provider.Kind == "wecom" && provider.AgentID == "" {
		return "", errors.New("企业微信身份源缺少 AgentID")
	}
	secret, err := s.socialSecret(provider)
	if err != nil || secret == "" {
		return "", errors.New("身份源密钥未配置")
	}
	state, err := s.sessions.StoreOneTime(ctx, "social-state", socialChallenge{ProviderID: provider.ID, Purpose: purpose, BindUserID: bindUserID}, socialStateTTL)
	if err != nil {
		return "", err
	}
	redirect, _ := url.Parse(provider.RedirectURI)
	query := redirect.Query()
	query.Set("state", state)
	redirect.RawQuery = query.Encode()
	switch provider.Kind {
	case "wecom":
		target, _ := url.Parse("https://open.work.weixin.qq.com/wwopen/sso/qrConnect")
		q := target.Query()
		q.Set("appid", provider.ClientID)
		q.Set("agentid", provider.AgentID)
		q.Set("redirect_uri", redirect.String())
		q.Set("state", state)
		target.RawQuery = q.Encode()
		return target.String(), nil
	case "dingtalk":
		target, _ := url.Parse("https://login.dingtalk.com/oauth2/auth")
		q := target.Query()
		q.Set("redirect_uri", redirect.String())
		q.Set("response_type", "code")
		q.Set("client_id", provider.ClientID)
		q.Set("scope", "openid")
		q.Set("state", state)
		q.Set("prompt", "consent")
		target.RawQuery = q.Encode()
		return target.String(), nil
	case "feishu":
		target, _ := url.Parse("https://open.feishu.cn/open-apis/authen/v1/authorize")
		q := target.Query()
		q.Set("app_id", provider.ClientID)
		q.Set("redirect_uri", redirect.String())
		q.Set("state", state)
		target.RawQuery = q.Encode()
		return target.String(), nil
	}
	return "", errors.New("不支持的扫码登录平台")
}

// CompleteSocialLogin 处理平台回调:校验 state、换取平台身份、按目的登录或绑定。
// meta 携带回调请求的登录环境(IP/UA)。
func (s *Service) CompleteSocialLogin(ctx context.Context, kind, state, code string, meta SessionMeta) (SocialLoginResult, error) {
	kind = strings.ToLower(strings.TrimSpace(kind))
	if !socialKindSupported(kind) || strings.TrimSpace(state) == "" || strings.TrimSpace(code) == "" {
		return SocialLoginResult{}, errors.New("扫码登录回调参数无效")
	}
	var challenge socialChallenge
	if err := s.sessions.ConsumeOneTime(ctx, "social-state", state, &challenge); err != nil {
		return SocialLoginResult{}, errors.New("登录状态无效或已过期，请重新扫码")
	}
	provider, err := s.repository.GetSocialProviderByKind(ctx, kind)
	if err != nil || provider.ID != challenge.ProviderID || !provider.Enabled {
		return SocialLoginResult{}, errors.New("登录身份源无效或已停用")
	}
	identity, err := s.exchangeSocial(ctx, provider, code)
	if err != nil {
		return SocialLoginResult{}, err
	}
	if challenge.Purpose == "bind" {
		return s.completeSocialBind(ctx, provider, challenge.BindUserID, identity)
	}
	if challenge.Purpose != "login" {
		return SocialLoginResult{}, errors.New("登录状态无效")
	}
	accountID, err := s.repository.FindProviderBinding(ctx, provider.Kind, identity.UID)
	if errors.Is(err, ErrNotFound) {
		if !provider.AutoProvision {
			return SocialLoginResult{}, ErrSocialUnbound
		}
		account, provisionErr := s.repository.resolveExternalAccount(ctx, externalProvider{
			ID: provider.ID, Code: provider.Code, AuthSource: provider.Kind, AutoProvision: true, DefaultDepartmentID: provider.DefaultDepartmentID,
		}, externalClaims{Subject: identity.UID, Name: identity.Name})
		if provisionErr != nil {
			return SocialLoginResult{}, provisionErr
		}
		accountID = account.ID
	} else if err != nil {
		return SocialLoginResult{}, err
	}
	account, err := s.findByID(ctx, KindPortal, accountID)
	if err != nil {
		return SocialLoginResult{}, err
	}
	if !account.Active {
		return SocialLoginResult{}, ErrAccountDisabled
	}
	token, err := s.sessions.CreateWithMeta(ctx, principalFor(account), meta.IP, meta.UserAgent)
	if err != nil {
		return SocialLoginResult{}, err
	}
	s.NotifyLogin(ctx, account.ID, meta)
	exchange, err := s.sessions.StoreOneTime(ctx, "oidc-exchange", oidcExchange{Token: token}, time.Minute)
	if err != nil {
		return SocialLoginResult{}, err
	}
	return SocialLoginResult{Purpose: "login", SSOCode: exchange}, nil
}

// completeSocialBind 处理绑定回调:同一平台账号只能绑到一个本系统账号。
func (s *Service) completeSocialBind(ctx context.Context, provider SocialProvider, bindUserID string, identity socialIdentity) (SocialLoginResult, error) {
	if bindUserID == "" {
		return SocialLoginResult{}, errors.New("绑定状态无效")
	}
	account, err := s.findByID(ctx, KindPortal, bindUserID)
	if err != nil || !account.Active {
		return SocialLoginResult{}, errors.New("绑定账号不存在或已停用")
	}
	if existing, err := s.repository.FindProviderBinding(ctx, provider.Kind, identity.UID); err == nil {
		if existing == bindUserID {
			return SocialLoginResult{Purpose: "bind", BindOK: true}, nil
		}
		return SocialLoginResult{Purpose: "bind", BindConflict: true}, nil
	}
	if err := s.repository.BindProvider(ctx, bindUserID, provider.Kind, identity.UID); err != nil {
		if strings.Contains(err.Error(), "已被其他") {
			return SocialLoginResult{Purpose: "bind", BindConflict: true}, nil
		}
		return SocialLoginResult{}, err
	}
	return SocialLoginResult{Purpose: "bind", BindOK: true}, nil
}

// UnbindProvider 解除扫码绑定(仅本人)。
func (s *Service) UnbindProvider(ctx context.Context, portalUserID, kind string) error {
	return s.repository.UnbindProvider(ctx, portalUserID, kind)
}

// ProviderBindings 返回账号的扫码绑定列表。
func (s *Service) ProviderBindings(ctx context.Context, portalUserID string) ([]ProviderBinding, error) {
	return s.repository.ListProviderBindings(ctx, portalUserID)
}

// socialSecret 解密平台 AppSecret。
func (s *Service) socialSecret(p SocialProvider) (string, error) {
	if s.idpCipher == nil || len(p.EncryptedCredentials) == 0 {
		return "", ErrUnavailable
	}
	plaintext, err := s.idpCipher.Decrypt(p.EncryptedCredentials, p.CredentialKEKVersion)
	if err != nil {
		return "", err
	}
	var credentials socialCredentials
	if err := json.Unmarshal(plaintext, &credentials); err != nil {
		return "", err
	}
	return credentials.Secret, nil
}

// exchangeSocial 用回调 code 换取平台身份。全部为固定公网端点,复用公共地址
// 白名单拨号的 oidcClient,不引入新的出站面。
func (s *Service) exchangeSocial(ctx context.Context, p SocialProvider, code string) (socialIdentity, error) {
	secret, err := s.socialSecret(p)
	if err != nil || secret == "" {
		return socialIdentity{}, errors.New("身份源密钥不可用")
	}
	switch p.Kind {
	case "wecom":
		return s.exchangeWeCom(ctx, p, secret, code)
	case "dingtalk":
		return s.exchangeDingTalk(ctx, p, secret, code)
	case "feishu":
		return s.exchangeFeishu(ctx, p, secret, code)
	}
	return socialIdentity{}, errors.New("不支持的扫码登录平台")
}

func (s *Service) exchangeWeCom(ctx context.Context, p SocialProvider, secret, code string) (socialIdentity, error) {
	tokenURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", url.QueryEscape(p.ClientID), url.QueryEscape(secret))
	var token struct {
		ErrCode     int    `json:"errcode"`
		ErrMsg      string `json:"errmsg"`
		AccessToken string `json:"access_token"`
	}
	if err := s.socialGetJSON(ctx, tokenURL, &token); err != nil || token.ErrCode != 0 || token.AccessToken == "" {
		return socialIdentity{}, errors.New("企业微信 access_token 获取失败")
	}
	var user struct {
		ErrCode int    `json:"errcode"`
		UserID  string `json:"userid"`
		OpenID  string `json:"openid"`
	}
	userURL := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=%s&code=%s", url.QueryEscape(token.AccessToken), url.QueryEscape(code))
	if err := s.socialGetJSON(ctx, userURL, &user); err != nil || user.ErrCode != 0 {
		return socialIdentity{}, errors.New("企业微信用户信息获取失败")
	}
	uid := strings.TrimSpace(user.UserID)
	if uid == "" {
		uid = strings.TrimSpace(user.OpenID)
	}
	if uid == "" {
		return socialIdentity{}, errors.New("企业微信未返回用户标识")
	}
	return socialIdentity{UID: uid, Name: uid}, nil
}

func (s *Service) exchangeDingTalk(ctx context.Context, p SocialProvider, secret, code string) (socialIdentity, error) {
	payload, _ := json.Marshal(map[string]string{"clientId": p.ClientID, "clientSecret": secret, "code": code, "grantType": "authorization_code"})
	var token struct {
		AccessToken string `json:"accessToken"`
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.dingtalk.com/v1.0/oauth2/userAccessToken", bytes.NewReader(payload))
	if err != nil {
		return socialIdentity{}, err
	}
	request.Header.Set("Content-Type", "application/json")
	response, err := s.oidcHTTPClient().Do(request)
	if err != nil {
		return socialIdentity{}, errors.New("钉钉 access_token 获取失败")
	}
	defer response.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<16))
	if response.StatusCode/100 != 2 || json.Unmarshal(raw, &token) != nil || token.AccessToken == "" {
		return socialIdentity{}, errors.New("钉钉 access_token 获取失败")
	}
	userRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.dingtalk.com/v1.0/contact/users/me", nil)
	if err != nil {
		return socialIdentity{}, err
	}
	userRequest.Header.Set("x-acs-dingtalk-access-token", token.AccessToken)
	userResponse, err := s.oidcHTTPClient().Do(userRequest)
	if err != nil {
		return socialIdentity{}, errors.New("钉钉用户信息获取失败")
	}
	defer userResponse.Body.Close()
	raw, _ = io.ReadAll(io.LimitReader(userResponse.Body, 1<<16))
	var user struct {
		UnionID string `json:"unionId"`
		OpenID  string `json:"openId"`
		Nick    string `json:"nick"`
	}
	if userResponse.StatusCode/100 != 2 || json.Unmarshal(raw, &user) != nil {
		return socialIdentity{}, errors.New("钉钉用户信息获取失败")
	}
	uid := strings.TrimSpace(user.UnionID)
	if uid == "" {
		uid = strings.TrimSpace(user.OpenID)
	}
	if uid == "" {
		return socialIdentity{}, errors.New("钉钉未返回用户标识")
	}
	return socialIdentity{UID: uid, Name: firstNonEmpty(user.Nick, uid)}, nil
}

func (s *Service) exchangeFeishu(ctx context.Context, p SocialProvider, secret, code string) (socialIdentity, error) {
	payload, _ := json.Marshal(map[string]string{"app_id": p.ClientID, "app_secret": secret, "code": code, "grant_type": "authorization_code"})
	var token struct {
		Code int `json:"code"`
		Data struct {
			AccessToken string `json:"access_token"`
		} `json:"data"`
	}
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token", bytes.NewReader(payload))
	if err != nil {
		return socialIdentity{}, err
	}
	request.Header.Set("Content-Type", "application/json")
	response, err := s.oidcHTTPClient().Do(request)
	if err != nil {
		return socialIdentity{}, errors.New("飞书 access_token 获取失败")
	}
	defer response.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<16))
	if response.StatusCode/100 != 2 || json.Unmarshal(raw, &token) != nil || token.Code != 0 || token.Data.AccessToken == "" {
		return socialIdentity{}, errors.New("飞书 access_token 获取失败")
	}
	userRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://open.feishu.cn/open-apis/authen/v1/user_info", nil)
	if err != nil {
		return socialIdentity{}, err
	}
	userRequest.Header.Set("Authorization", "Bearer "+token.Data.AccessToken)
	userResponse, err := s.oidcHTTPClient().Do(userRequest)
	if err != nil {
		return socialIdentity{}, errors.New("飞书用户信息获取失败")
	}
	defer userResponse.Body.Close()
	raw, _ = io.ReadAll(io.LimitReader(userResponse.Body, 1<<16))
	var user struct {
		Code int `json:"code"`
		Data struct {
			Name    string `json:"name"`
			OpenID  string `json:"open_id"`
			UnionID string `json:"union_id"`
		} `json:"data"`
	}
	if userResponse.StatusCode/100 != 2 || json.Unmarshal(raw, &user) != nil || user.Code != 0 {
		return socialIdentity{}, errors.New("飞书用户信息获取失败")
	}
	uid := strings.TrimSpace(user.Data.UnionID)
	if uid == "" {
		uid = strings.TrimSpace(user.Data.OpenID)
	}
	if uid == "" {
		return socialIdentity{}, errors.New("飞书未返回用户标识")
	}
	return socialIdentity{UID: uid, Name: firstNonEmpty(user.Data.Name, uid)}, nil
}

// socialGetJSON 执行 GET 并解码 JSON(限长)。
func (s *Service) socialGetJSON(ctx context.Context, endpoint string, target any) error {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return err
	}
	response, err := s.oidcHTTPClient().Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(response.Body, 1<<16))
	if response.StatusCode/100 != 2 {
		return errors.New("platform http error")
	}
	return json.Unmarshal(raw, target)
}
