// Copyright 2019 Yunion
//
// 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 feishu

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"
	"yunion.io/x/pkg/util/httputils"
)

const (
	// 获取 tenant_access_token（企业自建应用）
	ApiTenantAccessTokenInternal = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
	// 获取群列表
	ApiChatList = "https://open.feishu.cn/open-apis/chat/v4/list"
	// 机器人发送消息
	ApiRobotSendMessage = "https://open.feishu.cn/open-apis/message/v4/send/"
	// 批量发送消息
	ApiRobotBatchSendMessage = "https://open.feishu.cn/open-apis/message/v4/batch_send/"
	// 使用手机号或邮箱获取用户ID
	ApiFetchUserID = "https://open.feishu.cn/open-apis/user/v1/batch_get_id"
	// 使用 webhook 机器人发送消息
	ApiWebhookRobotSendMessage = "https://open.feishu.cn/open-apis/bot/hook/"
)

var (
	cli = &http.Client{
		Transport: httputils.GetTransport(true),
	}
	ctx = context.Background()
)

func Request(method httputils.THttpMethod, url string, header http.Header, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
	_, resp, err := httputils.JSONRequest(cli, ctx, method, url, header, body, false)
	return resp, err
}

func checkErr(resp CommonResponser) error {
	if resp.GetCode() != 0 {
		return errors.Error(fmt.Sprintf("response error, code: %d, msg: %s", resp.GetCode(), resp.GetMsg()))
	}
	return nil
}

func unmarshal(resp jsonutils.JSONObject, obj CommonResponser) error {
	if err := resp.Unmarshal(obj); err != nil {
		return errors.Wrap(err, "unmarshal json")
	}
	return checkErr(obj)
}

// 获取 tenant_access_token（企业自建应用）https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM
func GetTenantAccessTokenInternal(appId string, appSecret string) (*TenantAccesstokenResp, error) {
	body := jsonutils.NewDict()
	body.Add(jsonutils.NewString(appId), "app_id")
	body.Add(jsonutils.NewString(appSecret), "app_secret")
	ret, err := Request(httputils.POST, ApiTenantAccessTokenInternal, http.Header{}, body)
	if err != nil {
		return nil, err
	}
	obj := new(TenantAccesstokenResp)
	err = unmarshal(ret, obj)
	return obj, err
}

type Tenant struct {
	AppId       string
	AppSecret   string
	AccessToken string
	Cache       ICache
}

func BuildTokenHeader(token string) http.Header {
	h := http.Header{}
	h.Add("Authorization", fmt.Sprintf("Bearer %s", token))
	return h
}

func NewTenant(appId, appSecret string) (*Tenant, error) {
	t := &Tenant{
		AppId:       appId,
		AppSecret:   appSecret,
		AccessToken: "",
		Cache:       nil,
	}
	t.Cache = NewFileCache(fmt.Sprintf(".%s_auth_file", appId))
	err := t.RefreshAccessToken()
	return t, err
}

// RefreshAccessToken is to get a valid access token
func (t *Tenant) RefreshAccessToken() error {
	var data TenantAccesstoken
	err := t.Cache.Get(&data)
	if err == nil {
		t.AccessToken = data.TenantAccessToken
		return nil
	}

	tokenResp, err := GetTenantAccessTokenInternal(t.AppId, t.AppSecret)
	if err == nil {
		t.AccessToken = tokenResp.TenantAccessToken
		data = tokenResp.TenantAccesstoken
		data.Created = time.Now().Unix()
		err = t.Cache.Set(&data)
	}
	return err
}

func (t *Tenant) request(method httputils.THttpMethod, url string, data jsonutils.JSONObject, out CommonResponser) error {
	obj, err := Request(method, url, BuildTokenHeader(t.AccessToken), data)
	if err != nil {
		return err
	}
	err = unmarshal(obj, out)
	return err
}

func (t *Tenant) get(url string, query jsonutils.JSONObject, out CommonResponser) error {
	if query != nil {
		qs := query.QueryString()
		if len(qs) > 0 {
			url = fmt.Sprintf("%s?%s", url, qs)
		}
	}
	return t.request(httputils.GET, url, nil, out)
}

func (t *Tenant) post(url string, body jsonutils.JSONObject, out CommonResponser) error {
	return t.request(httputils.POST, url, body, out)
}

func (t *Tenant) ChatList(pageSize int, pageToken string) (*GroupListResp, error) {
	query := jsonutils.NewDict()
	if pageSize > 0 {
		query.Add(jsonutils.NewInt(int64(pageSize)), "page_size")
	}
	if pageToken != "" {
		query.Add(jsonutils.NewString(pageToken), "page_token")
	}
	resp := new(GroupListResp)
	err := t.get(ApiChatList, query, resp)
	return resp, err
}

func (t *Tenant) SendMessage(msg MsgReq) (*MsgResp, error) {
	body := jsonutils.Marshal(msg)
	resp := new(MsgResp)
	err := t.post(ApiRobotSendMessage, body, resp)
	return resp, err
}

// UserIdByMobile query the open id of user by mobile number. https://open.feishu.cn/document/ukTMukTMukTM/uUzMyUjL1MjM14SNzITN
func (t *Tenant) UserIdByMobile(mobile string) (string, error) {
	query := jsonutils.NewDict()
	query.Set("mobiles", jsonutils.NewString(mobile))
	resp := new(UserIDResp)
	err := t.get(ApiFetchUserID, query, resp)
	if err != nil {
		return "", err
	}
	if len(resp.Data.MobilesNotExist) != 0 {
		return "", errors.Wrapf(errors.ErrNotFound, "no such user whose mobile is %s", mobile)
	}
	list, err := resp.Data.MobileUsers.GetArray(mobile)
	if err != nil {
		return "", errors.Wrap(err, "jsonutils.JSONObject.GetArray")
	}
	// len(list) must be positive
	return list[0].GetString("open_id")
}

// SendWebhookRobotMessage will send to message to Webhook address.
// Webhook's format: https://open.feishu.cn/open-apis/bot/hook/xxxxxxxxxxxxxxxxxxxxxxxxxxx.
// The hook represents the last part of webhook: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'.
func SendWebhookRobotMessage(hook string, msg WebhookRobotMsgReq) (*WebhookRobotMsgResp, error) {
	url := ApiWebhookRobotSendMessage + hook
	obj, err := Request(httputils.POST, url, http.Header{}, jsonutils.Marshal(msg))
	if err != nil {
		return nil, err
	}
	resp := new(WebhookRobotMsgResp)
	err = obj.Unmarshal(resp)
	if err != nil {
		return nil, errors.Wrap(err, "unmarshal json")
	}
	if !resp.Ok {
		return resp, fmt.Errorf("response error, msg: %s", resp.Error)
	}
	return resp, err
}

// BatchSendMessage batch send messages. Doc: https://open.feishu.cn/document/ukTMukTMukTM/ucDO1EjL3gTNx4yN4UTM
func (t *Tenant) BatchSendMessage(msg BatchMsgReq) (*BatchMsgResp, error) {
	body := jsonutils.Marshal(msg)
	resp := new(BatchMsgResp)
	err := t.post(ApiRobotBatchSendMessage, body, resp)
	return resp, err
}
