package tbot

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"reflect"
	"strconv"
	"sync"
	"time"

	"github.com/go-resty/resty"
)

const apiUrl = "https://api.telegram.org/bot%s/"
const (
	sendCooldownPerUser = int64(time.Second / 3)
	sendInterval        = time.Second
)

const (
	getMe = "getMe"
)

var deferredMessages = make(map[int]chan deferredMessage)
var lastMessageTimes = make(map[int]int64)

type Client struct {
	sync.RWMutex
	Token  string
	ApiUrl string
	*resty.Client
	Bot    *User
}

func NewClient(token string) *Client {
	c := &Client{
		Token:  token,
		ApiUrl: fmt.Sprintf(apiUrl, token),
		Client: resty.New(),
		Bot:    &User{},
	}

	go c.sendDeferredMessages()

	return c
}

// https://core.telegram.org/bots/api#getme
func (c *Client) GetMe() (*User, error) {
	raw, err := c.MakeRequest(getMe, nil)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(raw, c.Bot)
	if err != nil {
		return nil, err
	}
	return c.Bot, err
}

func (c *Client) SendMessage(chatId int, text, parseMode string, replyMarkup ReplyMarkup) (Message, error) {
	return c.SendMessageExt(chatId, text, parseMode, replyMarkup, false)
}

func (c *Client) SendMessageExt(chatId int, text, parseMode string, replyMarkup ReplyMarkup, disablePreview bool) (Message, error) {
	params, err := prepareParams(chatId, text, "", parseMode, replyMarkup, disablePreview)
	if err != nil {
		return Message{}, err
	}

	raw, err := c.MakeRequest("sendMessage", params)
	if err != nil {
		return Message{}, err
	}

	var m Message
	err = json.Unmarshal(raw, &m)
	if err != nil {
		return Message{}, err
	}

	return m, nil
}

func (c *Client) SendMessageDeferred(chatId int, text, parseMode string, replyMarkup ReplyMarkup, callback func(SendError)) error {
	params, err := prepareParams(chatId, text, "", parseMode, replyMarkup, false)
	if err != nil {
		return err
	}

	c.MakeRequestDeferred(chatId, "sendMessage", params, "", callback)

	return nil
}

func (c *Client) EditMessageText(chatId, messageId int, text, parseMode string) (Message, error) {
	params := make(map[string]string)
	params["chat_id"] = strconv.Itoa(chatId)
	params["message_id"] = strconv.Itoa(messageId)
	params["text"] = text

	if parseMode != "" {
		params["parse_mode"] = parseMode
	}

	raw, err := c.MakeRequest("editMessageText", params)
	if err != nil {
		return Message{}, err
	}

	var m Message
	err = json.Unmarshal(raw, &m)
	if err != nil {
		return Message{}, err
	}

	return m, nil
}

func (c *Client) EditMessageTextAndReplyMarkup(chatId, messageId int, text, parseMode string, replyMarkup ReplyMarkup) (Message, error) {
	params := make(map[string]string)
	params["chat_id"] = strconv.Itoa(chatId)
	params["message_id"] = strconv.Itoa(messageId)
	params["text"] = text

	if parseMode != "" {
		params["parse_mode"] = parseMode
	}

	if replyMarkup != nil {
		raw, err := replyMarkup.MarshalJSON()
		if err != nil {
			return Message{}, err
		}
		params["reply_markup"] = string(raw)
	}

	raw, err := c.MakeRequest("editMessageText", params)
	if err != nil {
		return Message{}, err
	}

	var m Message
	err = json.Unmarshal(raw, &m)
	if err != nil {
		return Message{}, err
	}

	return m, nil
}

func (c *Client) EditMessageReplyMarkup(chatId, messageId int, replyMarkup ReplyMarkup) (Message, error) {
	params := make(map[string]string)
	params["chat_id"] = strconv.Itoa(chatId)
	params["message_id"] = strconv.Itoa(messageId)

	if replyMarkup != nil {
		raw, err := replyMarkup.MarshalJSON()
		if err != nil {
			return Message{}, err
		}
		params["reply_markup"] = string(raw)
	}

	raw, err := c.MakeRequest("editMessageReplyMarkup", params)
	if err != nil {
		return Message{}, err
	}

	var m Message
	err = json.Unmarshal(raw, &m)
	if err != nil {
		return Message{}, err
	}

	return m, nil
}

func (c *Client) SendPhoto(chatId int, photo, text string, replyMarkup ReplyMarkup) (Message, error) {
	params, err := prepareParams(chatId, "", text, "", replyMarkup, false)
	if err != nil {
		return Message{}, err
	}

	raw, err := c.MakeRequestWithPhoto("sendPhoto", params, photo)
	if err != nil {
		return Message{}, err
	}

	var m Message
	err = json.Unmarshal(raw, &m)
	if err != nil {
		return Message{}, err
	}

	return m, nil
}

func (c *Client) SendPhotoDeferred(chatId int, photo, text string, replyMarkup ReplyMarkup, callback func(SendError)) error {
	params, err := prepareParams(chatId, "", text, "", replyMarkup, false)
	if err != nil {
		return err
	}

	c.MakeRequestDeferred(chatId, "sendPhoto", params, photo, callback)

	return nil
}

func (c *Client) AnswerCallbackQuery(callbackQueryId string, text string, showAlert bool) (bool, error) {
	params := make(map[string]string)
	params["callback_query_id"] = callbackQueryId
	if text != "" {
		params["text"] = text
	}

	if showAlert {
		params["show_alert"] = "true"
	} else {
		params["show_alert"] = "false"
	}

	raw, err := c.MakeRequest("answerCallbackQuery", params)
	if err != nil {
		return false, err
	}

	var success bool
	err = json.Unmarshal(raw, &success)
	if err != nil {
		return false, err
	}

	return success, nil
}

func (c *Client) MakeRequest(method string, params map[string]string) (json.RawMessage, error) {
	return c.makeRequest(method, params, "")
}

func (c *Client) MakeRequestWithPhoto(method string, params map[string]string, photo string) (json.RawMessage, error) {
	return c.makeRequest(method, params, photo)
}

func (c *Client) MakeRequestDeferred(chatId int, method string, params map[string]string, photo string, callback func(SendError)) {
	dm := deferredMessage{
		chatId:   chatId,
		method:   method,
		params:   params,
		photo:    photo,
		callback: callback,
	}

	c.Lock()
	defer c.Unlock()

	if _, ok := deferredMessages[chatId]; !ok {
		deferredMessages[chatId] = make(chan deferredMessage, 100)
	}

	deferredMessages[chatId] <- dm
}

func (c *Client) makeRequest(method string, params map[string]string, photo string) (json.RawMessage, error) {
	request := c.Client.R()
	if params != nil {
		request.SetFormData(params)
	}

	if photo != "" {
		request.SetFile("photo", photo)
	}

	r, err := request.Post(c.ApiUrl + method)
	if err != nil {
		return json.RawMessage{}, err
	}
	defer r.RawResponse.Body.Close()

	resp := &Response{}
	bodyBytes, readErr := io.ReadAll(r.Body)
	if readErr != nil {
		return json.RawMessage{}, readErr
	}
	err = json.Unmarshal(bodyBytes, resp)
	// err = json.Unmarshal(r.Body(), resp)
	if err != nil {
		return json.RawMessage{}, err
	}

	if !resp.Ok {
		return json.RawMessage{}, errors.New(string(resp.ErrorCode) + ": " + resp.Description)
	}

	return resp.Result, nil
}

func (c *Client) sendDeferredMessages() {
	timer := time.NewTicker(sendInterval)

	for range timer.C {
		var cases []reflect.SelectCase

		for userId, ch := range deferredMessages {
			if userCanReceiveMessage(userId) && len(ch) > 0 {
				sc := reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
				cases = append(cases, sc)
			}
		}

		if len(cases) > 0 {
			_, value, ok := reflect.Select(cases)

			if ok {
				dm := value.Interface().(deferredMessage)
				_, err := c.makeRequest(dm.method, dm.params, dm.photo)
				if err != nil {
					dm.callback(SendError{ChatId: dm.chatId, Msg: err.Error()})
				}

				lastMessageTimes[dm.chatId] = time.Now().UnixNano()
			}
		}
	}
}

type deferredMessage struct {
	chatId   int
	method   string
	params   map[string]string
	photo    string
	callback func(SendError)
}

func prepareParams(chatId int, text, photoCaption, parseMode string, replyMarkup ReplyMarkup, disablePreview bool) (map[string]string, error) {
	params := make(map[string]string)
	params["chat_id"] = strconv.Itoa(chatId)
	params["text"] = text
	params["caption"] = photoCaption

	if disablePreview {
		params["disable_web_page_preview"] = "true"
	} else {
		params["disable_web_page_preview"] = "false"
	}

	if parseMode != "" {
		params["parse_mode"] = parseMode
	}

	if replyMarkup != nil {
		raw, err := replyMarkup.MarshalJSON()
		if err != nil {
			return nil, err
		}

		params["reply_markup"] = string(raw)
	}

	return params, nil
}

func userCanReceiveMessage(userId int) bool {
	t, ok := lastMessageTimes[userId]

	return !ok || t+sendCooldownPerUser <= time.Now().UnixNano()
}
