package wecom

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

	"golang.org/x/oauth2"

	"github.com/chaitin/panda-wiki/log"
	"github.com/chaitin/panda-wiki/store/cache"
)

const (
	// AuthURL api doc https://developer.work.weixin.qq.com/document/path/98152
	AuthWebURL    = "https://login.work.weixin.qq.com/wwlogin/sso/login"
	AuthAPPURL    = "https://open.weixin.qq.com/connect/oauth2/authorize"
	TokenURL      = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
	UserInfoURL   = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
	UserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
	// DepartmentListURL https://developer.work.weixin.qq.com/document/path/90344
	DepartmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list"
	// UserListUrl https://developer.work.weixin.qq.com/document/path/90337
	UserListUrl  = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
	callbackPath = "/share/pro/v1/openapi/wecom/callback"
)

// Client 企业微信客户端
type Client struct {
	context     context.Context
	cache       *cache.Cache
	httpClient  *http.Client
	oauthConfig *oauth2.Config
	logger      *log.Logger
	corpID      string
	agentID     string
}

type TokenResponse struct {
	ErrCode     int    `json:"errcode"`
	ErrMsg      string `json:"errmsg"`
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
}

type UserInfoResponse struct {
	ErrCode        int    `json:"errcode"`
	ErrMsg         string `json:"errmsg"`
	UserID         string `json:"userid"`
	UserTicket     string `json:"user_ticket"`
	OpenID         string `json:"openid"`
	ExternalUserid string `json:"external_userid"`
}

type UserDetailResponse struct {
	Errcode    int    `json:"errcode"`
	Errmsg     string `json:"errmsg"`
	Userid     string `json:"userid"`
	Name       string `json:"name"`
	Mobile     string `json:"mobile"`
	Gender     string `json:"gender"`
	Email      string `json:"email"`
	Avatar     string `json:"avatar"`
	OpenUserid string `json:"open_userid"`
}
type DepartmentListResponse struct {
	Errcode    int    `json:"errcode"`
	Errmsg     string `json:"errmsg"`
	Department []struct {
		Id               int      `json:"id"`
		Name             string   `json:"name"`
		NameEn           string   `json:"name_en"`
		DepartmentLeader []string `json:"department_leader"`
		Parentid         int      `json:"parentid"`
		Order            int      `json:"order"`
	} `json:"department"`
}

type UserListResponse struct {
	Errcode  int    `json:"errcode"`
	Errmsg   string `json:"errmsg"`
	Userlist []struct {
		Name       string `json:"name"`
		Department []int  `json:"department"`
		Position   string `json:"position"`
		Status     int    `json:"status"`
		Email      string `json:"email"`
		Avatar     string `json:"avatar"`
		Enable     int    `json:"enable"`
		Isleader   int    `json:"isleader"`
		Extattr    struct {
			Attrs []interface{} `json:"attrs"`
		} `json:"extattr"`
		HideMobile      int    `json:"hide_mobile"`
		Telephone       string `json:"telephone"`
		Order           []int  `json:"order"`
		ExternalProfile struct {
			ExternalAttr     []interface{} `json:"external_attr"`
			ExternalCorpName string        `json:"external_corp_name"`
		} `json:"external_profile"`
		MainDepartment int           `json:"main_department"`
		Alias          string        `json:"alias"`
		IsLeaderInDept []int         `json:"is_leader_in_dept"`
		Userid         string        `json:"userid"`
		DirectLeader   []interface{} `json:"direct_leader"`
	} `json:"userlist"`
}

func NewClient(ctx context.Context, logger *log.Logger, corpID, corpSecret, agentID, redirectURI string, cache *cache.Cache, isApp bool) (*Client, error) {
	redirectURL, _ := url.Parse(redirectURI)
	redirectURL.Path = callbackPath
	redirectURI = redirectURL.String()
	authUrl := AuthWebURL
	if isApp {
		authUrl = AuthAPPURL
	}

	oauthConfig := &oauth2.Config{
		ClientID:     corpID,
		ClientSecret: corpSecret,
		RedirectURL:  redirectURI,
		Endpoint: oauth2.Endpoint{
			AuthURL:  authUrl,
			TokenURL: TokenURL,
		},
		Scopes: []string{"snsapi_privateinfo"},
	}

	return &Client{
		context:     ctx,
		httpClient:  &http.Client{},
		cache:       cache,
		logger:      logger.WithModule("wecom.client"),
		oauthConfig: oauthConfig,
		corpID:      corpID,
		agentID:     agentID,
	}, nil
}

// GenerateAuthURL 生成授权 URL
func (c *Client) GenerateAuthURL(state string) string {
	params := url.Values{}
	params.Set("appid", c.corpID)
	params.Set("redirect_uri", c.oauthConfig.RedirectURL)
	params.Set("response_type", "code")
	params.Set("scope", "snsapi_privateinfo")
	params.Set("login_type", "CorpApp")
	params.Set("agentid", c.agentID)
	params.Set("state", state)

	authUrl := fmt.Sprintf("%s?%s", c.oauthConfig.Endpoint.AuthURL, params.Encode())
	if c.oauthConfig.Endpoint.AuthURL == AuthAPPURL {
		authUrl += "#wechat_redirect"
	}
	return authUrl
}

// GetAccessToken 获取企业微信访问令牌
func (c *Client) GetAccessToken(ctx context.Context) (string, error) {

	cacheKey := fmt.Sprintf("wecom-access-token:%s", c.oauthConfig.ClientID)
	cachedData, err := c.cache.Get(ctx, cacheKey).Result()
	if err == nil && cachedData != "" {
		return cachedData, nil
	}

	params := url.Values{}
	params.Set("corpid", c.corpID)
	params.Set("corpsecret", c.oauthConfig.ClientSecret)

	resp, err := c.httpClient.Get(fmt.Sprintf("%s?%s", TokenURL, params.Encode()))
	if err != nil {
		return "", fmt.Errorf("failed to get access token: %w", err)
	}
	defer resp.Body.Close()

	var tokenResp TokenResponse
	if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
		return "", fmt.Errorf("failed to decode token response: %w", err)
	}

	if tokenResp.ErrCode != 0 {
		return "", fmt.Errorf("failed to get access token: %s", tokenResp.ErrMsg)
	}

	if err := c.cache.Set(ctx, cacheKey, tokenResp.AccessToken, time.Duration(tokenResp.ExpiresIn-300)*time.Second).Err(); err != nil {
		c.logger.Warn("failed to set cache", log.Error(err))
	}

	return tokenResp.AccessToken, nil
}

// GetUserInfoByCode 通过授权码获取用户信息
func (c *Client) GetUserInfoByCode(ctx context.Context, code string) (*UserDetailResponse, error) {

	accessToken, err := c.GetAccessToken(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get access token: %w", err)
	}

	params := url.Values{}
	params.Set("access_token", accessToken)
	params.Set("code", code)

	userInfoURL := fmt.Sprintf("%s?%s", UserInfoURL, params.Encode())

	c.logger.Debug("GetUserInfoByCode", log.Any("userInfoURL", userInfoURL))

	resp, err := c.httpClient.Get(userInfoURL)
	if err != nil {
		return nil, fmt.Errorf("failed to get user info: %w", err)
	}

	rawBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read body: %w", err)
	}
	defer resp.Body.Close()

	c.logger.Debug("GetUserInfoByCode raw resp:", log.Any("raw", string(rawBody)))

	resp.Body = io.NopCloser(bytes.NewReader(rawBody))

	var userInfoResp UserInfoResponse
	if err := json.NewDecoder(resp.Body).Decode(&userInfoResp); err != nil {
		return nil, fmt.Errorf("failed to decode user info response: %w", err)
	}

	c.logger.Debug("GetUserInfoByCode resp:", log.Any("resp", userInfoResp))

	if userInfoResp.ErrCode != 0 {
		return nil, fmt.Errorf("failed to get user info: %s", userInfoResp.ErrMsg)
	}

	detailParams := url.Values{}
	detailParams.Set("access_token", accessToken)
	detailParams.Set("userid", userInfoResp.UserID)

	userDetailURL := fmt.Sprintf("%s?%s", UserDetailURL, detailParams.Encode())

	detailResp, err := c.httpClient.Get(userDetailURL)
	if err != nil {
		return nil, fmt.Errorf("failed to get user detail: %w", err)
	}
	defer detailResp.Body.Close()

	var UserDetailResp UserDetailResponse
	if err := json.NewDecoder(detailResp.Body).Decode(&UserDetailResp); err != nil {
		return nil, fmt.Errorf("failed to decode user detail response: %w", err)
	}

	c.logger.Debug("GetUserInfoByCode detail info", log.Any("resp", UserDetailResp))

	if UserDetailResp.Errcode != 0 {
		return nil, fmt.Errorf("failed to get user detail: %s", UserDetailResp.Errmsg)
	}

	return &UserDetailResp, nil
}

func (c *Client) GetDepartmentList(ctx context.Context) (*DepartmentListResponse, error) {

	accessToken, err := c.GetAccessToken(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get access token: %w", err)
	}

	params := url.Values{}
	params.Set("access_token", accessToken)

	departmentListURL := fmt.Sprintf("%s?%s", DepartmentListURL, params.Encode())

	resp, err := c.httpClient.Get(departmentListURL)
	if err != nil {
		return nil, fmt.Errorf("failed to get department list: %w", err)
	}

	rawBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read body: %w", err)
	}
	c.logger.Debug("GetDepartmentList raw resp:", log.Any("raw", string(rawBody)))
	defer resp.Body.Close()

	resp.Body = io.NopCloser(bytes.NewReader(rawBody))

	var departmentListResponse DepartmentListResponse
	if err := json.NewDecoder(resp.Body).Decode(&departmentListResponse); err != nil {
		return nil, fmt.Errorf("failed to decode department list response: %w", err)
	}

	c.logger.Debug("GetDepartmentList resp:", log.Any("resp", departmentListResponse))

	if departmentListResponse.Errcode != 0 {
		return nil, fmt.Errorf("failed to get user info: %s", departmentListResponse.Errmsg)
	}

	return &departmentListResponse, nil
}

func (c *Client) GetUserList(ctx context.Context, deptID string) (*UserListResponse, error) {

	accessToken, err := c.GetAccessToken(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to get access token: %w", err)
	}

	params := url.Values{}
	params.Set("access_token", accessToken)
	params.Set("department_id", deptID)

	userListUrl := fmt.Sprintf("%s?%s", UserListUrl, params.Encode())

	resp, err := c.httpClient.Get(userListUrl)
	if err != nil {
		return nil, fmt.Errorf("failed to get user list: %w", err)
	}

	rawBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read body: %w", err)
	}

	c.logger.Debug("GetUserList raw resp:", log.Any("raw", string(rawBody)))

	resp.Body = io.NopCloser(bytes.NewReader(rawBody))

	var userListResponse UserListResponse
	if err := json.NewDecoder(resp.Body).Decode(&userListResponse); err != nil {
		return nil, fmt.Errorf("failed to decode user list response: %w", err)
	}

	c.logger.Debug("GetUserList resp:", log.Any("resp", userListResponse))

	if userListResponse.Errcode != 0 {
		return nil, fmt.Errorf("failed to get user info: %s", userListResponse.Errmsg)
	}

	return &userListResponse, nil
}
