package main

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/larksuite/oapi-sdk-go/api"
	"github.com/larksuite/oapi-sdk-go/api/core/request"
	"github.com/larksuite/oapi-sdk-go/core"
	larkConf "github.com/larksuite/oapi-sdk-go/core/config"
	v4 "github.com/larksuite/oapi-sdk-go/service/calendar/v4"
	"github.com/robfig/cron/v3"
	"gorm.io/gorm"

	"github.com/pingcap/lark2es/pkg"
	"github.com/pingcap/lark2es/pkg/config"
	"github.com/pingcap/lark2es/pkg/db"
	"github.com/pingcap/lark2es/pkg/job"
	"github.com/pingcap/lark2es/pkg/util"
)

var (
	ErrDataNotExist = errors.New("data not exist")
)

type ItalentTokenResp struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int    `json:"expires_in"`
	RefreshToken string `json:"refresh_token"`
}

func GetItalentToken() (ItalentTokenResp, error) {
	urlStr := "https://openapi.italent.cn/token"

	data := url.Values{}
	data.Set("grant_type", "client_credentials")
	data.Set("app_key", config.GetString("italentappkey"))
	data.Set("app_secret", config.GetString("italentappsecret"))
	payload := strings.NewReader(data.Encode())
	header := map[string]string{
		"Content-Type": "application/x-www-form-urlencoded",
	}

	req, err := pkg.PostRequest(urlStr, payload, header)
	token := ItalentTokenResp{}
	if err != nil {
		return token, err
	}

	err = pkg.Do(req, &token)
	if err != nil {
		return token, err
	}

	return token, err
}

type ItalentVacationDetailList []ItalentVacationDetail

func (l ItalentVacationDetailList) Len() int {
	return len(l)
}

func (l ItalentVacationDetailList) Less(i, j int) bool {
	return l[i].CreatedTime < l[j].CreatedTime
}

func (l ItalentVacationDetailList) Swap(i, j int) {
	l[i], l[j] = l[j], l[i]
}

type ItalentVacationDetail struct {
	VacationID    string `json:"VacationId"`
	AttendanceOrg int    `json:"AttendanceOrg"`
	StaffID       int    `json:"StaffId"`
	StaffEmail    string `json:"StaffEmail"`
	// remote、年假、短期休假
	VacationType                string  `json:"VacationType"`
	VacationStartDateTime       string  `json:"VacationStartDateTime"`
	VacationStopDateTime        string  `json:"VacationStopDateTime"`
	VacationDuration            float64 `json:"VacationDuration"`
	DayValueOfDuration          float64 `json:"DayValueOfDuration"`
	VacationDurationIncludeUnit string  `json:"VacationDurationIncludeUnit"`
	// 请假、销假
	DocumentType string `json:"DocumentType"`
	Reason       string `json:"Reason"`
	// 是否通过、也可能是草稿
	ApproveStatus  string      `json:"ApproveStatus"`
	Applicant      int         `json:"Applicant"`
	ApplicantEmail string      `json:"ApplicantEmail"`
	ApplyTime      string      `json:"ApplyTime"`
	JointApplyType interface{} `json:"JointApplyType"`
	Remark         interface{} `json:"Remark"`
	Properties     struct {
	} `json:"Properties"`
	CreatedTime        string `json:"CreatedTime"`
	ModifiedTime       string `json:"ModifiedTime"`
	HalfDayStartOption int    `json:"HalfDayStartOption"`
	HalfDayStopOption  int    `json:"HalfDayStopOption"`
}

type ItalentVacation struct {
	Data struct {
		VacationList ItalentVacationDetailList `json:"VacationList"`
		Total        int                       `json:"Total"`
	} `json:"Data"`
	Code    int         `json:"Code"`
	Message interface{} `json:"Message"`
}

type ItalentVacationRequest struct {
	Day       string `json:"Day"`
	PageIndex int    `json:"PageIndex"`
	PageSize  int    `json:"PageSize"`
}

func GetDailyVacationApprovals(token, day string) (*ItalentVacation, error) {
	reqBody := ItalentVacationRequest{day, 1, 300}
	reqBodyBytes, err := json.Marshal(reqBody)
	if err != nil {
		return nil, err
	}

	req, err := pkg.PostRequest("https://openapi.italent.cn/AttendanceOpen/api/v1/Vacation/GetList",
		bytes.NewReader(reqBodyBytes),
		map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token),
			"Content-Type": "application/json"})
	if err != nil {
		return nil, err
	}

	vocations := ItalentVacation{}
	err = pkg.Do(req, &vocations)
	if err != nil {
		return nil, err
	}

	sort.Sort(vocations.Data.VacationList)
	return &vocations, nil
}

type EmployeeVacation struct {
	Email          string `gorm:"primaryKey"`
	FeishuOpenId   string `gorm:"type:varchar(255)"`
	TimeoffEventId string `gorm:"type:varchar(255)"`
	VacationTitle  string `gorm:"type:varchar(128)"`
	StartDate      string `gorm:"type:varchar(128)"`
	EndDate        string `gorm:"type:varchar(128)"`
	VacationId     string `gorm:"type:varchar(255)"`
	UpdateTime     string `gorm:"type:varchar(128)"`
	IsShortly      bool
	CreateTime     string           `gorm:"type:varchar(128)"` // vacation create time
	checker        *VacationChecker `gorm:"-"`
}

func (ev *EmployeeVacation) Clear() {
	ev.TimeoffEventId = ""
	ev.UpdateTime = time.Now().String()
	//ev.VacationTitle = ""
	//ev.StartDate = ""
	//ev.EndDate = ""
	//ev.VacationId = ""
}

func (ev *EmployeeVacation) DealShortly() error {
	startTime, _ := time.Parse("2006-01-02 15:04", ev.StartDate)
	endTime, _ := time.Parse("2006-01-02 15:04", ev.EndDate)
	now := time.Now().Format("2006-01-02 15:04")
	vc := ev.checker

	cancleFn := func() {
		ev.checker.CancleVacation(ev)
		err := Save(ev.checker.DB, ev)
		if err != nil {
			log.Println("[error] Save EmployeeVacation err", err, ev)
			return
		}
	}

	createFn := func() {
		err := ev.checker.CreateVacation(ev)
		if err != nil {
			log.Println("[error] StartVacationJob Submit failed ", err, ev)
			return
		}

		err = Save(ev.checker.DB, ev)
		if err != nil {
			log.Println("[error] Save EmployeeVacation err", err, ev)
			return
		}
	}

	if now < ev.StartDate {
		id, _ := job.AddJob(fmt.Sprintf("%d %d %d %d ?",
			startTime.Minute(), startTime.Hour(), startTime.Day(), startTime.Month()),
			VacationJobFn(createFn))
		vc.jobMap[ev.Email] = []cron.EntryID{id}

		id, _ = job.AddJob(fmt.Sprintf("%d %d %d %d ?",
			endTime.Minute(), endTime.Hour(), endTime.Day(), endTime.Month()),
			VacationJobFn(cancleFn))
		vc.jobMap[ev.Email] = append(vc.jobMap[ev.Email], id)
	} else {
		if now > ev.EndDate {
			vc.CancleVacation(ev)
		} else {
			//if ev.TimeoffEventId == "" {
			err := vc.CreateVacation(ev)
			if err != nil {
				log.Println("[error] create vacation err ", err, ev)
				return err
			}
			//}

			id, _ := job.AddJob(fmt.Sprintf("%d %d %d %d ?",
				endTime.Minute(), endTime.Hour(), endTime.Day(), endTime.Month()),
				VacationJobFn(cancleFn))
			vc.jobMap[ev.Email] = []cron.EntryID{id}
		}
	}

	return nil
}

func AutoMigrate(db *gorm.DB) {
	err := db.AutoMigrate(&EmployeeVacation{})
	if err != nil {
		log.Fatal(err.Error())
	}
}

func Save(db *gorm.DB, obj interface{}) error {
	return db.Save(obj).Error
}

func Find(db *gorm.DB, obj interface{}) error {
	tx := db.Find(obj, obj)
	if tx.Error != nil {
		return tx.Error
	}

	if tx.RowsAffected == 0 {
		return ErrDataNotExist
	}

	return nil
}

type VacationChecker struct {
	LarkConf        *larkConf.Config
	CalendarService *v4.Service
	*gorm.DB
	jobMap         map[string][]cron.EntryID
	getvacationsfn func(token, day string) (*ItalentVacation, error)
}

func NewVacationChecker() *VacationChecker {
	var err error
	checker := VacationChecker{}
	checker.DB, err = db.InitDb(config.GetString("db"))
	if err != nil {
		log.Println("[error] db init failed ", err)
		return nil
	}

	appSettings := core.NewInternalAppSettings(
		core.SetAppCredentials(config.GetString("appid"),
			config.GetString("appsecret"))) // 必需
	var confiOptions []core.ConfigOpt
	if config.GetString("ginmode") == "debug" {
		confiOptions = append(confiOptions, core.SetLoggerLevel(core.LoggerLevelDebug))
	}

	checker.LarkConf = core.NewConfig(core.DomainFeiShu, appSettings, confiOptions...)
	checker.CalendarService = v4.NewService(checker.LarkConf)
	checker.jobMap = make(map[string][]cron.EntryID)
	checker.getvacationsfn = GetDailyVacationApprovals
	return &checker
}

func (vc *VacationChecker) OnStartup() {
	shortlyVacations := []EmployeeVacation{}
	if err := vc.DB.Find(&shortlyVacations, "is_shortly = 1").Error; err != nil {
		log.Println("[error] OnStartup ", err)
	}

	for i := range shortlyVacations {
		vacation := &shortlyVacations[i]
		vacation.checker = vc
		vacation.DealShortly()
	}

	if len(shortlyVacations) > 0 {
		vc.DB.Save(shortlyVacations)
	}
}

func (vc *VacationChecker) QueryLarkOpenIdByEmail(user *EmployeeVacation) error {
	params := map[string]interface{}{
		"emails": user.Email,
	}
	ret := make(map[string]interface{})
	req := request.NewRequestWithNative("/open-apis/user/v1/batch_get_id", "GET",
		request.AccessTokenTypeTenant, nil, &ret, request.SetQueryParams(params))
	err := api.Send(core.WrapContext(context.Background()), vc.LarkConf, req)
	if err != nil {
		return err
	}

	if users, ok := ret["email_users"]; ok {
		if userdata, ok := users.(map[string]interface{})[user.Email]; ok {
			if ids, ok := userdata.([]interface{})[0].(map[string]interface{}); ok {
				user.FeishuOpenId = ids["open_id"].(string)
				return nil
			}
		}
	}

	return ErrDataNotExist
}

func (vc *VacationChecker) CancleVacation(vacation *EmployeeVacation) {
	if vacation.TimeoffEventId == "" {
		return
	}
	req := vc.CalendarService.TimeoffEvents.Delete(core.WrapContext(context.Background()))
	req.SetTimeoffEventId(vacation.TimeoffEventId)

	resp, err := req.Do()
	if err != nil {
		log.Println("[error] cancle vacation err ", err, vacation)
	} else {
		vacation.Clear()
		log.Println("CancleVacation ", vacation, resp)
	}
}

func (vc *VacationChecker) CreateVacation(vacation *EmployeeVacation) error {
	event := v4.TimeoffEvent{UserId: vacation.FeishuOpenId,
		Title: vacation.VacationTitle, Timezone: "Asia/Shanghai",
		StartTime: vacation.StartDate[:10],
		EndTime:   vacation.EndDate[:10],
	}

	req := vc.CalendarService.TimeoffEvents.Create(core.WrapContext(context.Background()),
		&event)
	resp, err := req.Do()
	if err != nil {
		return err
	}

	vacation.TimeoffEventId = resp.TimeoffEventId
	vacation.UpdateTime = time.Now().String()
	log.Println("CreateVacation ", *vacation)
	return nil
}

type VacationJobFn func()

func (fn VacationJobFn) Run() {
	fn()
}

func (vc *VacationChecker) Run() {
	token, err := GetItalentToken()
	if err != nil {
		log.Println("[error] get italent token err ", err)
		return
	}

	today := time.Now().Format("2006-01-02")
	vacations, err := vc.getvacationsfn(token.AccessToken, today)
	if err != nil {
		log.Println("[error] get italent vacations err ", err)
		return
	}

	for i := 0; i < len(vacations.Data.VacationList); i++ {
		vacation := vacations.Data.VacationList[i]
		if vacation.VacationType == "remote" ||
			vacation.ApproveStatus != "通过" {
			continue
		}

		employee := EmployeeVacation{Email: vacation.StaffEmail}
		err := Find(vc.DB, &employee)
		if err != nil {
			if !errors.Is(err, ErrDataNotExist) {
				log.Println("[error] db query failed ", err, vacation)
				continue
			}

			err := vc.QueryLarkOpenIdByEmail(&employee)
			if err != nil {
				log.Println("[error] query larkopenid failed ", err, employee)
				continue
			}
		}

		if vacation.DocumentType == "请假" {
			if employee.VacationId != "" {
				if employee.VacationId == vacation.VacationID {
					employee.CreateTime = vacation.CreatedTime
					continue
				}

				// 比较vacation的创建时间, 处理一天有多个请假的情况
				if vacation.CreatedTime[:10] != today {
					log.Println("Vacation created before today: ", vacation)
					vacation.CreatedTime = today + " 00:01"
				}
				if vacation.CreatedTime < employee.CreateTime {
					continue
				}
			}

			employee.VacationTitle = "请假" //fmt.Sprintf("%s(%s)", vacation.VacationType, vacation.VacationDurationIncludeUnit)
			employee.StartDate = vacation.VacationStartDateTime
			employee.EndDate = vacation.VacationStopDateTime
			employee.VacationId = vacation.VacationID
			employee.IsShortly = false
			employee.checker = vc
			//employee.TimeoffEventId = ""
			employee.CreateTime = vacation.CreatedTime

			if vacation.VacationType == "短期休假" {
				employee.IsShortly = true
				err := employee.DealShortly()
				if err != nil {
					continue
				}
			} else {
				// half day
				if vacation.DayValueOfDuration == 0.5 {
					// morning
					if vacation.HalfDayStartOption == 1 {
						employee.EndDate = employee.EndDate[:10] + " 13:00"
					} else {
						// afternoon
						employee.StartDate = employee.StartDate[:10] + " 13:00"
						employee.EndDate = employee.StartDate[:10] + " 22:00"
					}

					// add IsShortly for server restart checking
					employee.IsShortly = true
					err := employee.DealShortly()
					if err != nil {
						continue
					}
				} else {
					err := vc.CreateVacation(&employee)
					if err != nil {
						log.Println("[error] create vacation err ", err, employee)
						continue
					}
				}
			}
		} else if vacation.DocumentType == "销假" {
			if vacation.CreatedTime < employee.CreateTime {
				continue
			}

			for _, id := range vc.jobMap[employee.Email] {
				job.RemoveJob(id)
			}

			vc.CancleVacation(&employee)
		}

		employee.UpdateTime = time.Now().String()
		log.Printf("New Vacation %+v\n", employee)
		Save(vc.DB, employee)
	}
}

func main() {
	config.InitConfig("config", "yaml", "./etc")

	checker := NewVacationChecker()
	if checker == nil {
		panic(nil)
	}
	AutoMigrate(checker.DB)

	job.Start()
	checker.OnStartup()
	job.AddJob("@every 15m", checker)

	util.GracefulShutdown(nil, nil)
	log.Println("Server exiting")
}
