// SiYuan - Build Your Eternal Digital Garden
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package model

import (
	"encoding/hex"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/88250/gulu"
	"github.com/siyuan-note/siyuan/kernel/conf"
	"github.com/siyuan-note/siyuan/kernel/util"
)

var ErrFailedToConnectCloudServer = errors.New("failed to connect cloud server")

func DeactivateUser() (err error) {
	requestResult := gulu.Ret.NewResult()
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	resp, err := request.
		SetResult(requestResult).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/user/deactivate")
	if nil != err {
		util.LogErrorf("deactivate user failed: %s", err)
		return ErrFailedToConnectCloudServer
	}

	if 401 == resp.StatusCode {
		err = errors.New(Conf.Language(31))
		return
	}

	if 0 != requestResult.Code {
		util.LogErrorf("deactivate user failed: %s", requestResult.Msg)
		return errors.New(requestResult.Msg)
	}
	return
}

func SetCloudBlockReminder(id, data string, timed int64) (err error) {
	requestResult := gulu.Ret.NewResult()
	payload := map[string]interface{}{"dataId": id, "data": data, "timed": timed}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	resp, err := request.
		SetResult(requestResult).
		SetBody(payload).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/calendar/setBlockReminder")
	if nil != err {
		util.LogErrorf("set block reminder failed: %s", err)
		return ErrFailedToConnectCloudServer
	}

	if 401 == resp.StatusCode {
		err = errors.New(Conf.Language(31))
		return
	}

	if 0 != requestResult.Code {
		util.LogErrorf("set block reminder failed: %s", requestResult.Msg)
		return errors.New(requestResult.Msg)
	}
	return
}

var uploadToken = ""
var uploadTokenTime int64

func LoadUploadToken() (err error) {
	now := time.Now().Unix()
	if 3600 >= now-uploadTokenTime {
		return
	}

	requestResult := gulu.Ret.NewResult()
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	resp, err := request.
		SetResult(requestResult).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/upload/token")
	if nil != err {
		util.LogErrorf("get upload token failed: %s", err)
		return ErrFailedToConnectCloudServer
	}

	if 401 == resp.StatusCode {
		err = errors.New(Conf.Language(31))
		return
	}

	if 0 != requestResult.Code {
		util.LogErrorf("get upload token failed: %s", requestResult.Msg)
		return
	}

	resultData := requestResult.Data.(map[string]interface{})
	uploadToken = resultData["uploadToken"].(string)
	uploadTokenTime = now
	return
}

var (
	refreshUserTicker              = time.NewTicker(30 * time.Minute)
	subscriptionExpirationReminded bool
)

func AutoRefreshUser() {
	for {
		if !subscriptionExpirationReminded {
			subscriptionExpirationReminded = true
			go func() {
				if "ios" == util.Container {
					return
				}
				if IsSubscriber() && -1 != Conf.User.UserSiYuanProExpireTime {
					expired := int64(Conf.User.UserSiYuanProExpireTime)
					if time.Now().UnixMilli() >= expired { // 已经过期
						time.Sleep(time.Second * 30)
						util.PushErrMsg(Conf.Language(128), 0)
						return
					}
					remains := (expired - time.Now().Add(24*time.Hour*15).UnixMilli()) / 1000 / 60 / 60 / 24
					if 0 <= remains && 15 > remains { // 15 后过期
						time.Sleep(3 * time.Minute)
						util.PushErrMsg(fmt.Sprintf(Conf.Language(127), remains), 0)
						return
					}
				}
			}()
		}

		if nil != Conf.User {
			time.Sleep(3 * time.Minute)
			RefreshUser(Conf.User.UserToken)
			subscriptionExpirationReminded = false
		}
		<-refreshUserTicker.C
	}
}

func RefreshUser(token string) error {
	threeDaysAfter := util.CurrentTimeMillis() + 1000*60*60*24*3
	if "" == token {
		if "" != Conf.UserData {
			Conf.User = loadUserFromConf()
		}
		if nil == Conf.User {
			return errors.New(Conf.Language(19))
		}

		var tokenExpireTime int64
		tokenExpireTime, err := strconv.ParseInt(Conf.User.UserTokenExpireTime+"000", 10, 64)
		if nil != err {
			util.LogErrorf("convert token expire time [%s] failed: %s", Conf.User.UserTokenExpireTime, err)
			return errors.New(Conf.Language(19))
		}

		if threeDaysAfter > tokenExpireTime {
			token = Conf.User.UserToken
			goto Net
		}
		return nil
	}

Net:
	start := time.Now()
	user, err := getUser(token)
	if err != nil {
		if nil == Conf.User || errInvalidUser == err {
			return errors.New(Conf.Language(19))
		}

		var tokenExpireTime int64
		tokenExpireTime, err = strconv.ParseInt(Conf.User.UserTokenExpireTime+"000", 10, 64)
		if nil != err {
			util.LogErrorf("convert token expire time [%s] failed: %s", Conf.User.UserTokenExpireTime, err)
			return errors.New(Conf.Language(19))
		}

		if threeDaysAfter > tokenExpireTime {
			return errors.New(Conf.Language(19))
		}
		return nil
	}

	Conf.User = user
	data, _ := gulu.JSON.MarshalJSON(user)
	Conf.UserData = util.AESEncrypt(string(data))
	Conf.Save()

	if elapsed := time.Now().Sub(start).Milliseconds(); 3000 < elapsed {
		util.LogInfof("get cloud user elapsed [%dms]", elapsed)
	}
	return nil
}

func loadUserFromConf() *conf.User {
	if "" == Conf.UserData {
		return nil
	}

	data := util.AESDecrypt(Conf.UserData)
	data, _ = hex.DecodeString(string(data))
	user := &conf.User{}
	if err := gulu.JSON.UnmarshalJSON(data, &user); nil == err {
		return user
	}
	return nil
}

func RemoveCloudShorthands(ids []string) (err error) {
	result := map[string]interface{}{}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	body := map[string]interface{}{
		"ids": ids,
	}
	resp, err := request.
		SetResult(&result).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		SetBody(body).
		Post(util.AliyunServer + "/apis/siyuan/inbox/removeCloudShorthands")
	if nil != err {
		util.LogErrorf("remove cloud shorthands failed: %s", err)
		err = ErrFailedToConnectCloudServer
		return
	}

	if 401 == resp.StatusCode {
		err = errors.New(Conf.Language(31))
		return
	}

	code := result["code"].(float64)
	if 0 != code {
		util.LogErrorf("remove cloud shorthands failed: %s", result["msg"])
		err = errors.New(result["msg"].(string))
		return
	}
	return
}

func GetCloudShorthands(page int) (result map[string]interface{}, err error) {
	result = map[string]interface{}{}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	resp, err := request.
		SetResult(&result).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/inbox/getCloudShorthands?p=" + strconv.Itoa(page))
	if nil != err {
		util.LogErrorf("get cloud shorthands failed: %s", err)
		err = ErrFailedToConnectCloudServer
		return
	}

	if 401 == resp.StatusCode {
		err = errors.New(Conf.Language(31))
		return
	}

	code := result["code"].(float64)
	if 0 != code {
		util.LogErrorf("get cloud shorthands failed: %s", result["msg"])
		err = errors.New(result["msg"].(string))
		return
	}
	shorthands := result["data"].(map[string]interface{})["shorthands"].([]interface{})
	for _, item := range shorthands {
		shorthand := item.(map[string]interface{})
		id := shorthand["oId"].(string)
		t, _ := strconv.ParseInt(id, 10, 64)
		hCreated := util.Millisecond2Time(t)
		shorthand["hCreated"] = hCreated.Format("2006-01-02 15:04")
	}
	return
}

var errInvalidUser = errors.New("invalid user")

func getUser(token string) (*conf.User, error) {
	result := map[string]interface{}{}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	_, err := request.
		SetResult(&result).
		SetBody(map[string]string{"token": token}).
		Post(util.AliyunServer + "/apis/siyuan/user")
	if nil != err {
		util.LogErrorf("get community user failed: %s", err)
		return nil, errors.New(Conf.Language(18))
	}

	code := result["code"].(float64)
	if 0 != code {
		if 255 == code {
			return nil, errInvalidUser
		}
		util.LogErrorf("get community user failed: %s", result["msg"])
		return nil, errors.New(Conf.Language(18))
	}

	dataStr := result["data"].(string)
	data := util.AESDecrypt(dataStr)
	user := &conf.User{}
	if err = gulu.JSON.UnmarshalJSON(data, &user); nil != err {
		util.LogErrorf("get community user failed: %s", err)
		return nil, errors.New(Conf.Language(18))
	}
	return user, nil
}

func UseActivationcode(code string) (err error) {
	requestResult := gulu.Ret.NewResult()
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	_, err = request.
		SetResult(requestResult).
		SetBody(map[string]string{"data": code}).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/useActivationcode")
	if nil != err {
		util.LogErrorf("check activation code failed: %s", err)
		return ErrFailedToConnectCloudServer
	}
	if 0 != requestResult.Code {
		return errors.New(requestResult.Msg)
	}
	return
}

func CheckActivationcode(code string) (retCode int, msg string) {
	retCode = 1
	requestResult := gulu.Ret.NewResult()
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	_, err := request.
		SetResult(requestResult).
		SetBody(map[string]string{"data": code}).
		SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
		Post(util.AliyunServer + "/apis/siyuan/checkActivationcode")
	if nil != err {
		util.LogErrorf("check activation code failed: %s", err)
		msg = ErrFailedToConnectCloudServer.Error()
		return
	}
	if 0 == requestResult.Code {
		retCode = 0
	}
	msg = requestResult.Msg
	return
}

func Login(userName, password, captcha string) (ret *gulu.Result, err error) {
	result := map[string]interface{}{}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	_, err = request.
		SetResult(&result).
		SetBody(map[string]string{"userName": userName, "userPassword": password, "captcha": captcha}).
		Post(util.AliyunServer + "/apis/siyuan/login")
	if nil != err {
		util.LogErrorf("login failed: %s", err)
		return nil, errors.New(Conf.Language(18))
	}
	ret = &gulu.Result{
		Code: int(result["code"].(float64)),
		Msg:  result["msg"].(string),
		Data: map[string]interface{}{
			"userName":    result["userName"],
			"token":       result["token"],
			"needCaptcha": result["needCaptcha"],
		},
	}
	if -1 == ret.Code {
		ret.Code = 1
	}
	return
}

func Login2fa(token, code string) (map[string]interface{}, error) {
	result := map[string]interface{}{}
	request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
	_, err := request.
		SetResult(&result).
		SetBody(map[string]string{"twofactorAuthCode": code}).
		SetHeader("token", token).
		Post(util.AliyunServer + "/apis/siyuan/login/2fa")
	if nil != err {
		util.LogErrorf("login 2fa failed: %s", err)
		return nil, errors.New(Conf.Language(18))
	}
	return result, nil
}

func LogoutUser() {
	Conf.UserData = ""
	Conf.User = nil
	Conf.Save()
}
