// Copyright 2021 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package idp

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

	"golang.org/x/oauth2"
)

type WeComIdProvider struct {
	Client *http.Client
	Config *oauth2.Config

	UseIdAsName bool
}

func NewWeComIdProvider(clientId string, clientSecret string, redirectUrl string, useIdAsName bool) *WeComIdProvider {
	idp := &WeComIdProvider{}

	config := idp.getConfig(clientId, clientSecret, redirectUrl)
	idp.Config = config
	idp.UseIdAsName = useIdAsName

	return idp
}

func (idp *WeComIdProvider) SetHttpClient(client *http.Client) {
	idp.Client = client
}

// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow
func (idp *WeComIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config {
	endpoint := oauth2.Endpoint{
		TokenURL: "https://graph.qq.com/oauth2.0/token",
	}

	config := &oauth2.Config{
		Scopes:       []string{"snsapi_login"},
		Endpoint:     endpoint,
		ClientID:     clientId,
		ClientSecret: clientSecret,
		RedirectURL:  redirectUrl,
	}

	return config
}

type WeComProviderToken struct {
	Errcode             int    `json:"errcode"`
	Errmsg              string `json:"errmsg"`
	ProviderAccessToken string `json:"provider_access_token"`
	ExpiresIn           int    `json:"expires_in"`
}

// GetToken use code get access_token (*operation of getting code ought to be done in front)
// get more detail via: https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
func (idp *WeComIdProvider) GetToken(code string) (*oauth2.Token, error) {
	pTokenParams := &struct {
		CorpId         string `json:"corpid"`
		ProviderSecret string `json:"provider_secret"`
	}{idp.Config.ClientID, idp.Config.ClientSecret}
	data, err := idp.postWithBody(pTokenParams, "https://qyapi.weixin.qq.com/cgi-bin/service/get_provider_token")
	if err != nil {
		return nil, err
	}
	pToken := &WeComProviderToken{}
	err = json.Unmarshal(data, pToken)
	if err != nil {
		return nil, err
	}
	if pToken.Errcode != 0 {
		return nil, fmt.Errorf("pToken.Errcode = %d, pToken.Errmsg = %s", pToken.Errcode, pToken.Errmsg)
	}

	token := &oauth2.Token{
		AccessToken: pToken.ProviderAccessToken,
		Expiry:      time.Unix(time.Now().Unix()+int64(pToken.ExpiresIn), 0),
	}

	raw := make(map[string]interface{})
	raw["code"] = code
	token = token.WithExtra(raw)

	return token, nil
}

/*
{
   "errcode":0,
   "errmsg":"ok",
   "usertype": 1,
   "user_info":{
       "userid":"xxxx",
       "open_userid":"xxx",
       "name":"xxxx",
       "avatar":"xxxx"
   },
   "corp_info":{
       "corpid":"wxCorpId",
    },
   "agent":[
       {"agentid":0,"auth_type":1},
       {"agentid":1,"auth_type":1},
       {"agentid":2,"auth_type":1}
   ],
   "auth_info":{
       "department":[
           {
               "id":2,
               "writable":true
           }
       ]
   }
}
*/

type WeComUserInfo struct {
	Errcode  int    `json:"errcode"`
	Errmsg   string `json:"errmsg"`
	Usertype int    `json:"usertype"`
	UserInfo struct {
		Userid     string `json:"userid"`
		OpenUserid string `json:"open_userid"`
		Name       string `json:"name"`
		Avatar     string `json:"avatar"`
	} `json:"user_info"`
	CorpInfo struct {
		Corpid string `json:"corpid"`
	} `json:"corp_info"`
	Agent []struct {
		Agentid  int `json:"agentid"`
		AuthType int `json:"auth_type"`
	} `json:"agent"`
	AuthInfo struct {
		Department []struct {
			Id       int  `json:"id"`
			Writable bool `json:"writable"`
		} `json:"department"`
	} `json:"auth_info"`
}

// GetUserInfo use WeComProviderToken gotten before return WeComUserInfo
// get more detail via: https://work.weixin.qq.com/api/doc/90001/90143/91125
func (idp *WeComIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) {
	accessToken := token.AccessToken
	code := token.Extra("code").(string)

	requestBody := &struct {
		AuthCode string `json:"auth_code"`
	}{code}
	data, err := idp.postWithBody(requestBody, fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/service/get_login_info?access_token=%s", accessToken))
	if err != nil {
		return nil, err
	}

	wecomUserInfo := WeComUserInfo{}
	err = json.Unmarshal(data, &wecomUserInfo)
	if err != nil {
		return nil, err
	}
	if wecomUserInfo.Errcode != 0 {
		return nil, fmt.Errorf("wecomUserInfo.Errcode = %d, wecomUserInfo.Errmsg = %s", wecomUserInfo.Errcode, wecomUserInfo.Errmsg)
	}

	userInfo := UserInfo{
		Id:          wecomUserInfo.UserInfo.OpenUserid,
		Username:    wecomUserInfo.UserInfo.Name,
		DisplayName: wecomUserInfo.UserInfo.Name,
		AvatarUrl:   wecomUserInfo.UserInfo.Avatar,
	}

	if idp.UseIdAsName {
		userInfo.Username = userInfo.Id
	}
	return &userInfo, nil
}

func (idp *WeComIdProvider) postWithBody(body interface{}, url string) ([]byte, error) {
	bs, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	r := strings.NewReader(string(bs))
	resp, err := idp.Client.Post(url, "application/json;charset=UTF-8", r)
	if err != nil {
		return nil, err
	}
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)

	return data, nil
}
