package main

import (
	"errors"
	"fmt"
	"math/rand"
	"sync"
	"time"

	"github.com/BBAlliance/miaokeeper/memutils"
	jsoniter "github.com/json-iterator/go"
	tb "gopkg.in/telebot.v3"
	"gorm.io/gorm/clause"
)

// GORM:%NAME%_Lottery
type DBLottery struct {
	ID        string    `gorm:"column:id;type:string;size:128;primaryKey;not null"`
	Config    string    `gorm:"column:config;type:text;not null"`
	CreatedAt time.Time `gorm:"column:createdat;autoCreateTime"`
}

// GORM:%NAME%_Lottery_Participation
type DBLotteryParticipation struct {
	LotteryID   string    `gorm:"column:lotteryid;type:string;size:128;uniqueIndex:uniq_participant;index:lottery_id;not null"`
	Participant int64     `gorm:"column:participant;uniqueIndex:uniq_participant;not null"`
	Username    string    `gorm:"column:username;type:text;not null"`
	CreatedAt   time.Time `gorm:"column:createdat;autoCreateTime"`
}

var LotteryConfigCache map[string]*LotteryInstance
var LotteryConfigLock sync.Mutex

// status: -1 not start, 0 start, 1 stopped, 2 finished
type LotteryInstance struct {
	ID        string
	Status    int
	GroupID   int64
	MsgID     int
	CreatedAt int64
	StartedAt int64

	Payload     string
	Limit       int
	Consume     bool
	Num         int
	Duration    time.Duration
	Participant int

	Winners          []DBLotteryParticipation
	ParticipantCache int        `json:"-"`
	JoinLock         sync.Mutex `json:"-"`
}

func (li *LotteryInstance) UpdateTelegramMsg() *tb.Message {
	btns := []string{}
	if li.Status == 0 {
		btns = append(btns, fmt.Sprintf("ðŸ¤� æˆ‘è¦�æŠ½å¥–|lt?t=1&id=%s", li.ID))
	}
	if li.Status >= 0 && li.Status < 2 {
		btns = append(btns, fmt.Sprintf("ðŸ“¦ æ‰‹åŠ¨å¼€å¥–[ç®¡ç�†]|lt?t=3&id=%s", li.ID))
	}
	if li.Status == -1 {
		btns = append(btns, fmt.Sprintf("ðŸŽ¡ å¼€å�¯æ´»åŠ¨[ç®¡ç�†]|lt?t=2&id=%s", li.ID))
	}
	if li.MsgID > 0 && li.Status == 2 {
		Bot.Delete(&tb.Message{ID: li.MsgID, Chat: &tb.Chat{ID: li.GroupID}})
		li.MsgID = 0
	}
	if li.MsgID <= 0 {
		msg, _ := SendBtnsMarkdown(&tb.Chat{ID: li.GroupID}, li.GenText(), "", btns)
		if msg != nil {
			li.MsgID = msg.ID
			li.Update()
		}
		return msg
	} else {
		msg, _ := EditBtnsMarkdown(&tb.Message{ID: li.MsgID, Chat: &tb.Chat{ID: li.GroupID}}, li.GenText(), "", btns)
		if msg == nil {
			li.MsgID = 0
			return li.UpdateTelegramMsg()
		}
		return msg
	}
}

func (li *LotteryInstance) GenText() string {
	drawMsg := ""
	if li.Participant > 0 {
		drawMsg = fmt.Sprintf("å�‚ä¸Žäººæ•°è¾¾ %d äºº", li.Participant)
	}
	if li.Duration > 0 {
		if drawMsg != "" {
			drawMsg += " æˆ– "
		}
		durationStr := ""
		if li.Duration >= time.Hour {
			durationStr = fmt.Sprintf("%.1f å°�æ—¶", li.Duration.Hours())
		} else {
			durationStr = fmt.Sprintf("%d åˆ†é’Ÿ", int(li.Duration.Minutes()))
		}
		drawMsg += fmt.Sprintf("%så�Žè‡ªåŠ¨å¼€å¥–", durationStr)
	}
	if drawMsg == "" {
		drawMsg = "æ‰‹åŠ¨å¼€å¥–"
	}

	status := "`æœªçŸ¥`"
	if li.Status == -1 {
		status = "`å¾…éªŒè¯�`"
	} else if li.Status == 0 {
		status = "`è¿›è¡Œä¸­`"
	} else if li.Status == 1 {
		status = "`å¾…æ‰‹åŠ¨å¼€å¥–`"
	} else if li.Status == 2 {
		status = "`å·²å¼€å¥–`"
	}
	if li.Status >= 0 {
		status += fmt.Sprintf("\n*å�‚ä¸Žäººæ•°:* %d", li.Participants())
	}
	if len(li.Winners) > 0 {
		status += "\n\n*ðŸ�† èŽ·å¥–è€…:*"
		for i := range li.Winners {
			status += fmt.Sprintf("\n`%2d.` `%s` ([%d](%s))", i+1, GetQuotableStr(li.Winners[i].Username), li.Winners[i].Participant, fmt.Sprintf("tg://user?id=%d", li.Winners[i].Participant))
		}
	}

	return fmt.Sprintf(
		"ðŸ¤–ï¸� *æŠ½å¥–ä»»åŠ¡:* `%s`.\n\n*æŠ½å¥–é…�ç½®:*\nç§¯åˆ†è¦�æ±‚: `%d`\nç§¯åˆ†æ¶ˆè€—: `%v`\nå¥–å“�æ•°é‡�: `%d`\nå¼€å¥–æ–¹å¼�: `%s`\n\n*ä»»åŠ¡çŠ¶æ€�:* %s",
		GetQuotableStr(li.Payload), li.Limit, li.Consume, li.Num, drawMsg, status,
	)
}

func (li *LotteryInstance) Update() bool {
	cfg, _ := jsoniter.Marshal(li)
	err := DB.Table(DBTName("Lottery")).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "id"}},
		DoUpdates: clause.AssignmentColumns([]string{"config"}),
	}).Create(&DBLottery{
		ID:     li.ID,
		Config: string(cfg),
	}).Error

	if err != nil {
		DErrorf("Update Lottery Error | id=%s value=%s error=%v", li.ID, string(cfg), err.Error())
		return false
	}

	return true
}

func (li *LotteryInstance) Join(userId int64, username string) error {
	li.JoinLock.Lock()
	defer li.JoinLock.Unlock()

	if li.Status != 0 {
		return errors.New("â�Œ æŠ½å¥–æ´»åŠ¨ä¸�åœ¨æœ‰æ•ˆæ—¶é—´èŒƒå›´å†…ï¼Œè¯·æ£€æŸ¥å�Žå†�è¯• ~")
	}

	err := DB.Table(DBTName("Lottery_Participation")).Create(&DBLotteryParticipation{
		LotteryID:   li.ID,
		Participant: userId,
		Username:    username,
	}).Error

	if err != nil {
		DLogf("Join Lottery Error | id=%s user=%d error=%v", li.ID, userId, err.Error())
		return errors.New("â�Œ æ‚¨å·²ç»�å�‚åŠ è¿‡è¿™ä¸ªæ´»åŠ¨äº†ï¼Œè¯·ä¸�è¦�é‡�å¤�å�‚åŠ å“¦ ~")
	}

	if li.ParticipantCache > 0 {
		li.ParticipantCache += 1
	}

	return nil
}

func (li *LotteryInstance) Participants() int {
	if li.Status >= 0 {
		if li.ParticipantCache > 0 {
			return li.ParticipantCache
		}

		ret := int64(0)
		err := DB.Table(DBTName("Lottery_Participation")).Where("lotteryid = ?", li.ID).Count(&ret).Error
		if err != nil {
			DLogf("Fetch Lottery Participants Number Error | id=%s error=%v", li.ID, err.Error())
			return -1
		}

		li.ParticipantCache = int(ret)
		return li.ParticipantCache
	}
	return -1
}

func (li *LotteryInstance) StartLottery() {
	li.JoinLock.Lock()
	defer li.JoinLock.Unlock()

	if li.Status == -1 {
		li.Status = 0
		li.StartedAt = time.Now().Unix()
		li.Update()
		li.UpdateTelegramMsg()

		if li.Duration > 0 {
			lazyScheduler.After(li.Duration+time.Second, memutils.LSC("checkDraw", &CheckDrawArgs{
				LotteryId: li.ID,
			}))
		}
	}
}

func (li *LotteryInstance) CheckDraw(force bool) bool {
	li.JoinLock.Lock()
	defer li.JoinLock.Unlock()

	if li.Status == 0 {
		if force {
			// manual draw
			li.Status = 2
		} else if li.Duration > 0 && li.StartedAt > 0 && li.StartedAt+int64(li.Duration/time.Second) < time.Now().Unix() {
			// timeout draw
			li.Status = 2
		} else if li.Participant >= 0 && li.Participants() >= li.Participant {
			// participant exceeding draw
			li.Status = 2
		}

		// draw
		if li.Status == 2 {
			winners := []DBLotteryParticipation{}
			DB.Table(DBTName("Lottery_Participation")).Clauses(clause.OrderBy{
				Expression: clause.Expr{SQL: GetRandClause()},
			}).Where("lotteryid = ?", li.ID).Limit(li.Num).Find(&winners)

			li.Winners = winners
			li.Update()
			li.UpdateTelegramMsg()
			return true
		}
	}

	return false
}

func GetLottery(lotteryId string) *LotteryInstance {
	LotteryConfigLock.Lock()
	defer LotteryConfigLock.Unlock()
	if li, ok := LotteryConfigCache[lotteryId]; ok && li != nil {
		return li
	}

	ret := DBLottery{}
	err := DB.Table(DBTName("Lottery")).First(&ret, "id = ?", lotteryId).Error
	if err != nil {
		DErrorf("Fetch Lottery Error | id=%s error=%v", lotteryId, err.Error())
		return nil
	}

	li := LotteryInstance{}
	err = jsoniter.Unmarshal([]byte(ret.Config), &li)
	if err != nil {
		DErrorf("Unmarshal Lottery Error | id=%s error=%v", lotteryId, err.Error())
		return nil
	}

	LotteryConfigCache[li.ID] = &li
	return &li
}

func CreateLottery(groupId int64, payload string, limit int, consume bool, num int, duration int, participant int) *LotteryInstance {
	li := LotteryInstance{
		ID:        fmt.Sprintf("%d:%d:%d", Abs(groupId), time.Now().Unix(), rand.Intn(9999)),
		Status:    -1,
		GroupID:   groupId,
		CreatedAt: time.Now().Unix(),
		StartedAt: 0,

		Payload:     payload,
		Limit:       limit,
		Consume:     consume,
		Num:         num,
		Duration:    time.Minute * time.Duration(duration),
		Participant: participant,
	}

	if li.Update() {
		LotteryConfigLock.Lock()
		LotteryConfigCache[li.ID] = &li
		LotteryConfigLock.Unlock()
		return &li
	}
	return nil
}

func init() {
	LotteryConfigCache = make(map[string]*LotteryInstance)
}
