package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"io/ioutil"
	"strconv"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/golang-jwt/jwt"
)

var hmacSampleSecret []byte

// TEMP: hard coding, use config file in future
var knownApps = map[string]string{
	"cli_a18d2ef3aff8d00d": "6lVHGPaUe4gp9yGjikN5dhlzO76Lstr2",
}

var knownFields = map[string]string{
	"userHealthDoc": "Business Health Record - 业务健康档案（URL）",
}

type APPCred struct {
	Id     string `form:"appId" json:"appId"`
	Secret string `form:"appSecret" json:"appSecret"`
}
type FetchTenantTokenResp struct {
	Code   int64  `json:"code"`
	Msg    string `json:"msg"`
	Tat    string `json:"tenantAccessToken"`
	Expire int    `json:"expire"`
}
type CodeState struct {
	Code           string `form:"code" json:"code" binding:"required"`
	State          string `form:"state" json:"state"`
	AppId          string `form:"appId" json:"appId"`
	AppSecret      string `form:"appSecret" json:"appSecret"`
	AppAccessToken string `form:"appAccessToken" json:"appAccessToken"`
}
type UserAccessTokenInfoData struct {
	AccessToken      string `json:"accessToken"`
	TokenType        string `json:"tokenType"`
	ExpiresIn        int64  `json:"expiresIn"`
	Name             string `json:"name"`
	EnName           string `json:"enName"`
	AvatarUrl        string `json:"avatarUrl"`
	AvatarThumb      string `json:"avatarThumb"`
	AvatarMiddle     string `json:"avatarMiddle"`
	AvatarBig        string `json:"avatarBig"`
	OpenId           string `json:"openId"`
	UnionId          string `json:"unionId"`
	Email            string `json:"email"`
	UserId           string `json:"userId"`
	Mobile           string `json:"mobile"`
	TenantKey        string `json:"tenantKey"`
	RefreshExpiresIn int64  `json:"refreshExpiresIn"`
	RefreshToken     string `json:"refreshToken"`
}
type FetchUserAccessTokenInfoResp struct {
	Code int64                   `json:"code"`
	Msg  string                  `json:"msg"`
	Data UserAccessTokenInfoData `json:"data"`
}
type UserLoginResp struct {
	Code int64  `json:"code"`
	Msg  string `json:"msg"`
	Data string `json:"data"`
}
type LoginBody struct {
	Code  string `form:"code" json:"code" binding:"required"`
	State string `form:"state" json:"state"`
	AppId string `form:"appId" json:"appId" binding:"required"`
}
type TokenClaims struct {
	Uat    string `json:"uat"`
	UatExp int64  `json:"uat_exp"`
	Rt     string `json:"rt"`
	RtExp  int64  `json:"rt_exp"`
	AppId  string `json:"app_id"`
	jwt.StandardClaims
}
type UserInfoData struct {
	Name         string `json:"name"`
	EnName       string `json:"enName"`
	AvatarUrl    string `json:"avatarUrl"`
	AvatarThumb  string `json:"avatarThumb"`
	AvatarMiddle string `json:"avatarMiddle"`
	AvatarBig    string `json:"avatarBig"`
	OpenId       string `json:"openId"`
	UnionId      string `json:"unionId"`
	Email        string `json:"email"`
	UserId       string `json:"userId"`
	Mobile       string `json:"mobile"`
	TenantKey    string `json:"tenantKey"`
}
type UserAccessTokenInfoResp struct {
	Code int64        `json:"code"`
	Msg  string       `json:"msg"`
	Data UserInfoData `json:"data"`
}
type RefreshUserAccessTokenInfoResp struct {
	Code int64                   `json:"code"`
	Msg  string                  `json:"msg"`
	Data UserAccessTokenInfoData `json:"data"`
}
type FetchUserInfoResp struct {
	Code     int64        `json:"code"`
	Msg      string       `json:"msg"`
	Data     UserInfoData `json:"data"`
	NewToken string       `json:"newToken,omitempty"`
}
type GeneralLarkApiRes struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}
type FetchCSRecordsResItem struct {
	RecordId string                 `json:"recordId,omitempty"`
	Fields   map[string]interface{} `json:"fields"`
	SLFields BiTableRecordsItem     `json:"slFields"`
}
type FetchCSRecordsResData struct {
	HasMore   bool                    `json:"hasMore"`
	PageToken string                  `json:"pageToken"`
	Total     int                     `json:"total"`
	Items     []FetchCSRecordsResItem `json:"items"`
}
type FetchCSRecordsItem struct {
	RecordId string                 `json:"recordId,omitempty"`
	Fields   map[string]interface{} `json:"fields"`
}
type FetchCSRecordsData struct {
	HasMore   bool                 `json:"hasMore"`
	PageToken string               `json:"pageToken"`
	Total     int                  `json:"total"`
	Items     []FetchCSRecordsItem `json:"items"`
}
type FetchCSRecordsLarkApiRes struct {
	Code int64              `json:"code"`
	Msg  string             `json:"msg"`
	Data FetchCSRecordsData `json:"data"`
}
type AssDocHandlerBody struct {
	Link  string `json:"link" binding:"required"`
	Text  string `json:"text"`
	Field string `json:"field"`
}
type AssDocBody struct {
	Fields map[string]interface{} `json:"fields"`
}
type UpdateBiTableRecordLarkApiRes struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}
type FetchBiTableCollaboratorsLarkApiRes struct {
	Code int64       `json:"code"`
	Msg  string      `json:"msg"`
	Data interface{} `json:"data"`
}

func fTenantToken(appId string, appSecret string) (string, error) {
	postBody, _ := json.Marshal(map[string]string{
		"app_id":     appId,
		"app_secret": appSecret,
	})
	resp, err := fetchTenantAccessToken(bytes.NewBuffer(postBody))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	var jsonB FetchTenantTokenResp
	if err := json.Unmarshal(convertJSON2Camel(body), &jsonB); err != nil {
		return "", err
	}
	if jsonB.Code != 0 {
		return "", errors.New(strconv.FormatInt(jsonB.Code, 10) + ": " + jsonB.Msg)
	}
	return jsonB.Tat, nil
}

func fetchTenantToken(c *gin.Context, appId string) (string, error) {
	var ac APPCred
	if c.Request.Method == "POST" {
		if err := c.ShouldBindJSON(&ac); err != nil {
			return "", err
		}
	}
	aId := ac.Id
	if ac.Id == "" {
		if appId == "" {
			return "", errors.New("no app_id")
		}
		aId = appId
	}
	aSecret := ac.Secret
	if aSecret == "" {
		if knownApps[aId] == "" {
			return "", errors.New("no app_secret")
		}
		aSecret = knownApps[aId]
	}
	tat, err := fTenantToken(aId, aSecret)
	if err != nil {
		return "", err
	}
	return tat, nil
}

func fetchTenantTokenHandler(c *gin.Context) {
	tat, err := fetchTenantToken(c, "")
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	resBizJSON(c, genSuccBody(c, tat))
}

func fUserTokenInfoByTat(code string, tat string) (*UserAccessTokenInfoData, error) {
	lpostBody, _ := json.Marshal(map[string]string{
		"grant_type": "authorization_code",
		"code":       code,
	})
	lresp, lerr := fetchUserAccessTokenInfo(bytes.NewBuffer(lpostBody), tat)
	if lerr == nil {
		defer lresp.Body.Close()
		lbody, _ := ioutil.ReadAll(lresp.Body)
		var jsonLB FetchUserAccessTokenInfoResp
		if err := json.Unmarshal(convertJSON2Camel(lbody), &jsonLB); err != nil {
			return nil, err
		}
		if jsonLB.Code != 0 {
			return nil, errors.New(strconv.FormatInt(jsonLB.Code, 10) + ": " + jsonLB.Msg)
		}
		return &jsonLB.Data, nil
	}
	return nil, lerr
}

func fetchUserTokenInfo(c *gin.Context) (*UserAccessTokenInfoData, error) {
	var cs CodeState
	if err := c.ShouldBindBodyWith(&cs, binding.JSON); err != nil {
		return nil, err
	}
	if cs.AppAccessToken != "" {
		d, err := fUserTokenInfoByTat(cs.Code, cs.AppAccessToken)
		if err != nil {
			return nil, err
		}
		return d, nil
	} else if cs.AppId != "" && (cs.AppSecret != "" || knownApps[cs.AppId] != "") {
		appSec := cs.AppSecret
		if appSec == "" {
			appSec = knownApps[cs.AppId]
		}
		tatBody, _ := json.Marshal(map[string]string{
			"app_id":     cs.AppId,
			"app_secret": appSec,
		})
		tatReqBody := bytes.NewBuffer(tatBody)
		tatResp, err := fetchTenantAccessToken(tatReqBody)
		if err != nil {
			return nil, err
		}
		defer tatResp.Body.Close()
		tatRespBody, _ := ioutil.ReadAll(tatResp.Body)
		var tatJsonB struct {
			Code   int64  `json:"code"`
			Msg    string `json:"msg"`
			TAT    string `json:"tenantAccessToken"`
			Expire int64  `json:"expire"`
		}
		if err := json.Unmarshal(convertJSON2Camel(tatRespBody), &tatJsonB); err != nil {
			return nil, err
		}
		d, err := fUserTokenInfoByTat(cs.Code, tatJsonB.TAT)
		if err != nil {
			return nil, err
		}
		return d, nil
	}
	return nil, errors.New("need valid app_access_token or (app_id & app_secret)")
}

func fetchUserTokenInfoHandler(c *gin.Context) {
	uati, err := fetchUserTokenInfo(c)
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	resBizJSON(c, genSuccBody(c, uati))
}

func userLogin(c *gin.Context) {
	var lb LoginBody
	if err := c.ShouldBindBodyWith(&lb, binding.JSON); err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	uati, err := fetchUserTokenInfo(c)
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	_, tokenString, err := genBizJWT(uati, lb.AppId, hmacSampleSecret)
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	resBizJSON(c, genSuccBody(c, tokenString))
}

func fetchUserInfoHandler(c *gin.Context) {
	jwtToken := c.GetString("jwtToken")
	refreshUserInfo, exists := c.Get("refreshUserInfo")
	rui, ok := refreshUserInfo.(UserInfoData)
	if exists && ok {
		resBizJSON(c, genSuccWithToken(rui, jwtToken))
		return
	}
	jwtClaim, exists := c.Get("jwtClaim")
	jc, ok := jwtClaim.(*TokenClaims)
	if exists && ok {
		resp, err := fetchUserInfo(jc.Uat)
		if err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		defer resp.Body.Close()
		body, _ := ioutil.ReadAll(resp.Body)
		var jsonB UserAccessTokenInfoResp
		if err := json.Unmarshal(convertJSON2Camel(body), &jsonB); err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		isHandled := handleGeneralLarkApiRes(c, jsonB.Code, jsonB.Msg)
		if !isHandled {
			resBizJSON(c, genSuccBody(c, jsonB.Data))
			return
		}
	} else {
		resBizJSON(c, genCusErrBody(ResInternalErr, errors.New("auth middleware not mount jwtClaim").Error()))
		return
	}
}

func fetchBiTableRecordsHandler(c *gin.Context) {
	appToken := c.Query("appToken")
	tableId := c.Query("tableId")
	viewId := c.Query("viewId")
	pageSize := c.Query("pageSize")
	pageToken := c.Query("pageToken")
	if appToken == "" || tableId == "" {
		resBizJSON(c, genErrBody(ResBadRequest))
		return
	}
	jwtUat := c.GetString("jwtClaimUat")
	resp, err := fetchBiTableRecords(jwtUat, appToken, tableId, &LarkFetchBiTableRecordsParams{
		ViewId:    viewId,
		PageSize:  pageSize,
		PageToken: pageToken,
	})
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	defer resp.Body.Close()
	body, _ := ioutil.ReadAll(resp.Body)
	var jsonB FetchCSRecordsLarkApiRes
	if err := json.Unmarshal(convertJSON2Camel(body), &jsonB); err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	isHandled := handleGeneralLarkApiRes(c, jsonB.Code, jsonB.Msg)
	if !isHandled {
		var items []FetchCSRecordsResItem
		originItems := jsonB.Data.Items
		for _, originItem := range originItems {
			primaryCol, primaryColExists := (originItem.Fields[CSPrimaryColName]).([]interface{})
			customerCol, valueExists := (primaryCol[0]).(map[string]interface{})
			customerName, toStrOK := (customerCol["text"]).(string)
			if !primaryColExists || !valueExists || !toStrOK {
				resBizJSON(c, genCusErrBody(ResInternalErr, "Cannot Parse Customer Name from Lark Fetch BiTable Records API."))
				return
			}
			cachedRecord, hasCached := CSStageLevelRecords[customerName]
			if hasCached {
				items = append(items, FetchCSRecordsResItem{
					RecordId: originItem.RecordId,
					Fields:   originItem.Fields,
					SLFields: cachedRecord,
				})
			} else {
				items = append(items, FetchCSRecordsResItem{
					RecordId: originItem.RecordId,
					Fields:   originItem.Fields,
				})
			}
		}
		res := &FetchCSRecordsResData{
			HasMore:   jsonB.Data.HasMore,
			PageToken: jsonB.Data.PageToken,
			Total:     jsonB.Data.Total,
			Items:     items,
		}
		resBizJSON(c, genSuccBody(c, res))
		return
	}
}

func associateUserHealthDocHandler(c *gin.Context) {
	appToken := c.Query("appToken")
	tableId := c.Query("tableId")
	recordId := c.Query("recordId")
	if appToken == "" || tableId == "" || recordId == "" {
		resBizJSON(c, genErrBody(ResBadRequest))
		return
	}
	var b AssDocHandlerBody
	if err := c.ShouldBindBodyWith(&b, binding.JSON); err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}

	fieldName := knownFields["userHealthDoc"]
	if b.Field != "" {
		fieldName = b.Field
	}
	fieldVal := make(map[string]interface{})
	fieldVal[fieldName] = map[string]string{
		"text": b.Text,
		"link": b.Link,
	}
	body, _ := json.Marshal(&AssDocBody{
		Fields: fieldVal,
	})
	reqBody := bytes.NewBuffer(body)
	jwtClaim, exists := c.Get("jwtClaim")
	jc, ok := jwtClaim.(*TokenClaims)
	if exists && ok {
		var appSec string = knownApps[jc.AppId]
		if appSec == "" {
			resBizJSON(c, genCusErrBody(ResInternalErr, "Cannot find corresponding app secret"))
			return
		}
		tatBody, _ := json.Marshal(map[string]string{
			"app_id":     jc.AppId,
			"app_secret": appSec,
		})
		tatReqBody := bytes.NewBuffer(tatBody)
		tatResp, err := fetchTenantAccessToken(tatReqBody)
		if err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		defer tatResp.Body.Close()
		tbody, _ := ioutil.ReadAll(tatResp.Body)
		var jsonTB FetchTenantTokenResp
		if err := json.Unmarshal(convertJSON2Camel(tbody), &jsonTB); err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		if jsonTB.Code != 0 {
			resBizJSON(c, genCusErrBody(ResInternalErr, strconv.FormatInt(jsonTB.Code, 10)+": "+jsonTB.Msg))
			return
		}

		upRes, err := updateBiTableRecord(jsonTB.Tat, appToken, tableId, recordId, reqBody)
		if err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		defer upRes.Body.Close()
		ub, _ := ioutil.ReadAll(upRes.Body)
		var jsonB UpdateBiTableRecordLarkApiRes
		if err := json.Unmarshal(convertJSON2Camel(ub), &jsonB); err != nil {
			resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
			return
		}
		if jsonB.Code != 0 {
			resBizJSON(c, genCusErrBody(ResInternalErr, strconv.FormatInt(jsonB.Code, 10)+": "+jsonB.Msg))
			return
		}
		resBizJSON(c, genSuccBody(c, jsonB.Data))
	} else {
		resBizJSON(c, genCusErrBody(ResInternalErr, "No valid token"))
		return
	}
}

func fetchBiTableCollaboratorsHandler(c *gin.Context) {
	appToken := c.Query("appToken")
	if appToken == "" {
		resBizJSON(c, genErrBody(ResBadRequest))
		return
	}
	body, _ := json.Marshal(&struct {
		Token string `json:"token"`
		Type  string `json:"type"`
	}{
		Token: appToken,
		Type:  "bitable",
	})
	reqBody := bytes.NewBuffer(body)
	jwtUat := c.GetString("jwtClaimUat")
	coRes, err := fetchBiTableCollaborators(jwtUat, reqBody)
	if err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	defer coRes.Body.Close()
	cos, _ := ioutil.ReadAll(coRes.Body)
	var jsonB FetchBiTableCollaboratorsLarkApiRes
	if err := json.Unmarshal(convertJSON2Camel(cos), &jsonB); err != nil {
		resBizJSON(c, genCusErrBody(ResInternalErr, err.Error()))
		return
	}
	if jsonB.Code != 0 {
		resBizJSON(c, genCusErrBody(ResInternalErr, strconv.FormatInt(jsonB.Code, 10)+": "+jsonB.Msg))
		return
	}
	resBizJSON(c, genSuccBody(c, jsonB.Data))
}
