package integram

import (
	"encoding/gob"
	"errors"
	"fmt"
	"math/rand"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"crypto/md5"
	"github.com/kennygrant/sanitize"

	"github.com/requilence/jobs"
	tg "github.com/requilence/telegram-bot-api"
	log "github.com/sirupsen/logrus"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

const inlineButtonStateKeyword = '`'
const antiFloodSameMessageTimeout = 60

var botPerID = make(map[int64]*Bot)
var botPerService = make(map[string]*Bot)

var botTokenRE = regexp.MustCompile("([0-9]*):([0-9a-zA-Z_-]*)")

// Bot represents parsed auth data & API reference
type Bot struct {
	// Bot Telegram user id
	ID int64

	// Bot Telegram username
	Username string

	// Bot Telegram token
	token string

	// Slice of services that using this bot (len=1 means that bot is dedicated for service – recommended case)
	services []*Service

	// Used to store long-pulling updates channel and survive panics
	updatesChan <-chan tg.Update
	API         *tg.BotAPI
}

type Location struct {
	Latitude  float64
	Longitude float64
}

// Message represent both outgoing and incoming message data
type Message struct {
	ID               bson.ObjectId `bson:"_id,omitempty"` // Internal unique BSON ID
	EventID          []string      `bson:",omitempty"`
	MsgID            int           `bson:",omitempty"`         // Telegram Message ID. BotID+MsgID is unique
	InlineMsgID      string        `bson:",omitempty"`         // Telegram InlineMessage ID. ChatID+InlineMsgID is unique
	BotID            int64         `bson:",minsize"`           // TG bot's ID on which behalf message is sending or receiving
	FromID           int64         `bson:",minsize"`           // TG User's ID of sender. Equal to BotID in case of outgoinf message from bot
	ChatID           int64         `bson:",omitempty,minsize"` // Telegram chat's ID, equal to FromID in case of private message
	BackupChatID     int64         `bson:",omitempty,minsize"` // This chat will be used if chatid failed (bot not started or stopped or group deactivated)
	ReplyToMsgID     int           `bson:",omitempty"`         // If this message is reply, contains Telegram's Message ID of original message
	Date             time.Time
	Text             string           `bson:"-"`			   // Exclude text field
	TextHash         string           `bson:",omitempty"`
	Location         *Location        `bson:",omitempty"`
	AntiFlood        bool             `bson:",omitempty"`
	Deleted          bool             `bson:",omitempty"` // f.e. admin can delete the message in supergroup and we can't longer edit or reply on it
	OnCallbackAction string           `bson:",omitempty"` // Func to call on inline button press
	OnCallbackData   []byte           `bson:",omitempty"` // Args to send to this func
	OnReplyAction    string           `bson:",omitempty"` // Func to call on message reply
	OnReplyData      []byte           `bson:",omitempty"` // Args to send to this func
	OnEditAction     string           `bson:",omitempty"` // Func to call on message edit
	OnEditData       []byte           `bson:",omitempty"` // Args to send to this func
	om               *OutgoingMessage // Cache when retreiving original replied message
}

// IncomingMessage specifies data that available for incoming message
type IncomingMessage struct {
	Message              `bson:",inline"`
	From                 User
	Chat                 Chat
	ForwardFrom          *User
	ForwardDate          time.Time
	ForwardFromMessageID int64

	ReplyToMessage        *Message            `bson:"-"`
	ForwardFromChat       *Chat               `json:"forward_from_chat"`       // optional
	EditDate              int                 `json:"edit_date"`               // optional
	Entities              *[]tg.MessageEntity `json:"entities"`                // optional
	Audio                 *tg.Audio           `json:"audio"`                   // optional
	Document              *tg.Document        `json:"document"`                // optional
	Photo                 *[]tg.PhotoSize     `json:"photo"`                   // optional
	Sticker               *tg.Sticker         `json:"sticker"`                 // optional
	Video                 *tg.Video           `json:"video"`                   // optional
	Voice                 *tg.Voice           `json:"voice"`                   // optional
	Caption               string              `json:"caption"`                 // optional
	Contact               *tg.Contact         `json:"contact"`                 // optional
	Location              *tg.Location        `json:"location"`                // optional
	Venue                 *tg.Venue           `json:"venue"`                   // optional
	NewChatMembers        []*User             `json:"new_chat_members"`        // optional
	LeftChatMember        *User               `json:"left_chat_member"`        // optional
	NewChatTitle          string              `json:"new_chat_title"`          // optional
	NewChatPhoto          *[]tg.PhotoSize     `json:"new_chat_photo"`          // optional
	DeleteChatPhoto       bool                `json:"delete_chat_photo"`       // optional
	GroupChatCreated      bool                `json:"group_chat_created"`      // optional
	SuperGroupChatCreated bool                `json:"supergroup_chat_created"` // optional
	ChannelChatCreated    bool                `json:"channel_chat_created"`    // optional
	MigrateToChatID       int64               `json:"migrate_to_chat_id"`      // optional
	MigrateFromChatID     int64               `json:"migrate_from_chat_id"`    // optional
	PinnedMessage         *Message            `json:"pinned_message"`          // optional

	// Need to update message in DB. Used f.e. when you set the eventID for outgoing message
	needToUpdateDB bool
}

// OutgoingMessage specispecifiesfy data of performing or performed outgoing message
type OutgoingMessage struct {
	Message              `bson:",inline"`
	KeyboardHide         bool           `bson:",omitempty"`
	ResizeKeyboard       bool           `bson:",omitempty"`
	KeyboardMarkup       Keyboard       `bson:"-"`
	InlineKeyboardMarkup InlineKeyboard `bson:",omitempty"`
	Keyboard             bool           `bson:",omitempty"`
	ParseMode            string         `bson:",omitempty"`
	OneTimeKeyboard      bool           `bson:",omitempty"`
	Selective            bool           `bson:",omitempty"`
	ForceReply           bool           `bson:",omitempty"` // in the private dialog assume user's message as the reply for the last message sent by the bot if bot's message has Reply handler and ForceReply set
	WebPreview           bool           `bson:",omitempty"`
	Silent               bool           `bson:",omitempty"`
	FilePath             string         `bson:",omitempty"`
	FileName             string         `bson:",omitempty"`
	FileType             string         `bson:",omitempty"`
	FileRemoveAfter      bool           `bson:",omitempty"`
	SendAfter            *time.Time     `bson:",omitempty"`
	processed            bool
	ctx                  *Context
}

// Keyboard is a Shorthand for [][]Button
type Keyboard []Buttons

// Buttons is a Shorthand for []Button
type Buttons []Button

// InlineKeyboard contains the data to create the Inline keyboard for Telegram and store it in DB
type InlineKeyboard struct {
	Buttons    []InlineButtons // You must specify at least 1 InlineButton in slice
	FixedWidth bool            `bson:",omitempty"` // will add right padding to match all buttons text width
	State      string          // determine the current keyboard's state. Useful to change the behavior for branch cases and make it little thread safe while it is using by several users
	MaxRows    int             `bson:",omitempty"` // Will automatically add next/prev buttons. Zero means no limit
	RowOffset  int             `bson:",omitempty"` // Current offset when using MaxRows
}

// InlineButtons is a Shorthand for []InlineButton
type InlineButtons []InlineButton

// Button contains the data to create Keyboard
type Button struct {
	Data string // data is stored in the DB. May be collisions if button text is not unique per keyboard
	Text string // should be unique per keyboard
}

// InlineButton contains the data to create InlineKeyboard
// One of URL, Data, SwitchInlineQuery must be specified
// If more than one specified the first in order of (URL, Data, SwitchInlineQuery) will be used
type InlineButton struct {
	Text                         string
	State                        int
	URL                          string `bson:",omitempty"`
	Data                         string `bson:",omitempty"` // maximum 64 bytes
	SwitchInlineQuery            string `bson:",omitempty"` //
	SwitchInlineQueryCurrentChat string `bson:",omitempty"`

	OutOfPagination bool `bson:",omitempty" json:"-"` // Only for the single button in first or last row. Use together with InlineKeyboard.MaxRows – for buttons outside of pagination list
}

type ChatConfig struct {
	tg.ChatConfig
}

type ChatConfigWithUser struct {
	tg.ChatConfigWithUser
}

// InlineKeyboardMarkup allow to generate TG and DB data from different states - (InlineButtons, []InlineButtons and InlineKeyboard)
type InlineKeyboardMarkup interface {
	tg() [][]tg.InlineKeyboardButton
	Keyboard() InlineKeyboard
}

// KeyboardMarkup allow to generate TG and DB data from different states - (Buttons and Keyboard)
type KeyboardMarkup interface {
	tg() [][]tg.KeyboardButton
	Keyboard() Keyboard
	db() map[string]string
}

func (c *Bot) tgToken() string {
	return fmt.Sprintf("%d:%s", c.ID, c.token)
}

// PMURL return URL to private messaging with the bot like https://telegram.me/trello_bot?start=param
func (c *Bot) PMURL(param string) string {
	if param == "" {
		return fmt.Sprintf("https://telegram.me/%v", c.Username)
	}

	return fmt.Sprintf("https://telegram.me/%v?start=%v", c.Username, param)
}

func (c *Bot) webhookURL() *url.URL {
	url, _ := url.Parse(fmt.Sprintf("%s/tg/%d/%s", Config.BaseURL, c.ID, compactHash(c.token)))
	return url
}

func (service *Service) registerBot(fullTokenWithID string) error {

	s := botTokenRE.FindStringSubmatch(fullTokenWithID)

	if len(s) < 3 {
		return errors.New("can't parse token")
	}
	id, err := strconv.ParseInt(s[1], 10, 64)
	if err != nil {
		return err
	}

	if b, exists := botPerID[id]; !exists || b.token != s[2] {
		// bot with this ID not exists or the bot's token changed
		bot := Bot{ID: id, token: s[2], services: []*Service{service}}
		botPerID[id] = &bot

		token := bot.tgToken()
		bot.API, err = tg.NewBotAPI(token)
		if err != nil {
			log.WithError(err).WithField("token", token).Error("NewBotAPI returned error")
			return err
		}

		bot.Username = bot.API.Self.UserName

	} else {
		b := botPerID[id]

		serviceAlreadyExists := false
		for _, s := range b.services {
			if s.Name == service.Name {
				serviceAlreadyExists = true
				break
			}
		}
		if !serviceAlreadyExists {
			b.services = append(b.services, service)
		}
		botPerID[id] = b
	}
	botPerService[service.Name] = botPerID[id]
	return nil
}

// Compare if InlineKeyboard.tg() of 2 keyboards are equal
func whetherTGInlineKeyboardsAreEqual(tg1, tg2 [][]tg.InlineKeyboardButton) bool {

	if len(tg1) != len(tg2) {
		return false
	}

	for i := 0; i < len(tg1); i++ {
		if len(tg1[i]) != len(tg2[i]) {
			return false
		}
		for j := 0; j < len(tg1[i]); j++ {
			b1 := tg1[i][j]
			b2 := tg2[i][j]
			if b1.Text != b2.Text ||
				b1.CallbackData == nil && b2.CallbackData != nil ||
				b1.CallbackData != nil && b2.CallbackData == nil ||
				b1.CallbackData != nil && b2.CallbackData != nil && *b1.CallbackData != *b2.CallbackData ||
				b1.URL == nil && b2.URL != nil ||
				b1.URL != nil && b2.URL == nil ||
				b1.URL != nil && b2.URL != nil && *b1.URL != *b2.URL ||
				b1.SwitchInlineQuery == nil && b2.SwitchInlineQuery != nil ||
				b1.SwitchInlineQuery != nil && b2.SwitchInlineQuery == nil ||
				b1.SwitchInlineQuery != nil && b2.SwitchInlineQuery != nil && *b1.SwitchInlineQuery != *b2.SwitchInlineQuery ||
				b1.SwitchInlineQueryCurrentChat == nil && b2.SwitchInlineQueryCurrentChat != nil ||
				b1.SwitchInlineQueryCurrentChat != nil && b2.SwitchInlineQueryCurrentChat == nil ||
				b1.SwitchInlineQueryCurrentChat != nil && b2.SwitchInlineQueryCurrentChat != nil && *b1.SwitchInlineQueryCurrentChat != *b2.SwitchInlineQueryCurrentChat {
				return false
			}
		}
	}
	return true
}

// Find the InlineButton in Keyboard by the Data
func (keyboard *InlineKeyboard) Find(buttonData string) (i, j int, but *InlineButton) {
	for i, buttonsRow := range keyboard.Buttons {
		for j, button := range buttonsRow {
			if button.Data == buttonData {
				return i, j, &button
			}
		}
	}
	return -1, -1, nil
}

// EditText find the InlineButton in Keyboard by the Data and change the text of that button
func (keyboard *InlineKeyboard) EditText(buttonData string, newText string) {
	for i, buttonsRow := range keyboard.Buttons {
		for j, button := range buttonsRow {
			if button.Data == buttonData {
				keyboard.Buttons[i][j].Text = newText
				return
			}
		}
	}
}

// AddPMSwitchButton add the button to switch to PM as a first row in the InlineKeyboard
func (keyboard *InlineKeyboard) AddPMSwitchButton(b *Bot, text string, param string) {
	if len(keyboard.Buttons) > 0 && len(keyboard.Buttons[0]) > 0 && keyboard.Buttons[0][0].Text == text {
		return
	}
	keyboard.PrependRows(InlineButtons{InlineButton{Text: text, URL: b.PMURL(param)}})
}

// AppendRows adds 1 or more InlineButtons (rows) to the end of InlineKeyboard
func (keyboard *InlineKeyboard) AppendRows(buttons ...InlineButtons) {
	keyboard.Buttons = append(keyboard.Buttons, buttons...)
}

// PrependRows adds 1 or more InlineButtons (rows) to the begin of InlineKeyboard
func (keyboard *InlineKeyboard) PrependRows(buttons ...InlineButtons) {
	keyboard.Buttons = append(buttons, keyboard.Buttons...)
}

// Append adds 1 or more InlineButton (column) to the end of InlineButtons(row)
func (buttons *InlineButtons) Append(data string, text string) {
	if len(data) > 64 {
		log.WithField("text", text).Errorf("InlineButton data '%s' extends 64 bytes limit", data)
	}
	*buttons = append(*buttons, InlineButton{Data: data, Text: text})
}

// Prepend adds 1 or more InlineButton (column) to the begin of InlineButtons(row)
func (buttons *InlineButtons) Prepend(data string, text string) {
	if len(data) > 64 {
		log.WithField("text", text).Errorf("InlineButton data '%s' extends 64 bytes limit", data)
	}
	*buttons = append([]InlineButton{{Data: data, Text: text}}, *buttons...)
}

// AppendWithState add the InlineButton with state to the end of InlineButtons(row)
// Useful for checkbox or to revert the action
func (buttons *InlineButtons) AppendWithState(state int, data string, text string) {
	if len(data) > 64 {
		log.WithField("text", text).Errorf("InlineButton data '%s' extends 64 bytes limit", data)
	}
	if state > 9 || state < 0 {
		log.WithField("data", data).WithField("text", text).Errorf("AppendWithState – state must be [0-9], %d received", state)
	}
	*buttons = append(*buttons, InlineButton{Data: data, Text: text, State: state})
}

// PrependWithState add the InlineButton with state to the begin of InlineButtons(row)
// Useful for checkbox or to revert the action
func (buttons *InlineButtons) PrependWithState(state int, data string, text string) {
	if len(data) > 64 {
		log.WithField("text", text).Errorf("InlineButton data '%s' extends 64 bytes limit", data)
	}
	if state > 9 || state < 0 {
		log.WithField("data", data).WithField("text", text).Errorf("PrependWithState – state must be [0-9], %d received", state)
	}
	*buttons = append([]InlineButton{{Data: data, Text: text, State: state}}, *buttons...)
}

// AddURL adds InlineButton with URL to the end of InlineButtons(row)
func (buttons *InlineButtons) AddURL(url string, text string) {
	*buttons = append(*buttons, InlineButton{URL: url, Text: text})
}

// Markup generate InlineKeyboard from InlineButtons ([]Button), chunking buttons by columns number, and specifying current keyboard state
// Keyboard state useful for nested levels to determine current position
func (buttons *InlineButtons) Markup(columns int, state string) InlineKeyboard {
	keyboard := InlineKeyboard{}

	col := 0

	row := InlineButtons{}
	len := len(*buttons)
	for i, button := range *buttons {
		row = append(row, button)

		col++
		if col == columns || i == (len-1) {
			col = 0
			keyboard.AppendRows(row)
			row = InlineButtons{}
		}
	}
	keyboard.State = state
	return keyboard
}

// Keyboard generates inline keyboard from inline keyboard  :-D
func (keyboard InlineKeyboard) Keyboard() InlineKeyboard {
	return keyboard
}

// Keyboard generates inline keyboard with 1 button
func (button InlineButton) Keyboard() InlineKeyboard {
	return InlineKeyboard{Buttons: []InlineButtons{{button}}}
}

// Keyboard generates inline keyboard with 1 column
func (buttons InlineButtons) Keyboard() InlineKeyboard {
	return buttons.Markup(1, "")
}

func (button InlineButton) tg() [][]tg.InlineKeyboardButton {
	return button.Keyboard().tg()
}

func (buttons InlineButtons) tg() [][]tg.InlineKeyboardButton {
	return buttons.Keyboard().tg()
}

func stringPointer(s string) *string {
	return &s
}

func (keyboard InlineKeyboard) tg() [][]tg.InlineKeyboardButton {
	res := make([][]tg.InlineKeyboardButton, len(keyboard.Buttons))

	maxWidth := 0
	if keyboard.FixedWidth {

		for _, columns := range keyboard.Buttons {
			for _, button := range columns {
				if len(button.Text) > maxWidth {
					maxWidth = len(button.Text)
				}
			}
		}
	}
	for r, columns := range keyboard.Buttons {
		res[r] = make([]tg.InlineKeyboardButton, len(keyboard.Buttons[r]))
		c := 0
		for _, button := range columns {
			if keyboard.FixedWidth {
				button.Text = button.Text + strings.Repeat(" ", maxWidth-len(button.Text))
			}

			if button.State != 0 {
				button.Data = fmt.Sprintf("%c%d%s", inlineButtonStateKeyword, button.State, button.Data)
			}

			if button.URL != "" {
				res[r][c] = tg.InlineKeyboardButton{Text: button.Text, URL: stringPointer(button.URL)}
			} else if button.Data != "" {
				res[r][c] = tg.InlineKeyboardButton{Text: button.Text, CallbackData: stringPointer(button.Data)}
			} else if button.SwitchInlineQueryCurrentChat != "" {
				res[r][c] = tg.InlineKeyboardButton{Text: button.Text, SwitchInlineQueryCurrentChat: stringPointer(button.SwitchInlineQueryCurrentChat)}
			} else {
				res[r][c] = tg.InlineKeyboardButton{Text: button.Text, SwitchInlineQuery: stringPointer(button.SwitchInlineQuery)}
			}
			c++
		}
	}
	return res
}

// AddRows adds 1 or more Buttons (rows) to the end of InlineKeyboard
func (keyboard *Keyboard) AddRows(buttons ...Buttons) {
	*keyboard = append(*keyboard, buttons...)
}

// Prepend adds InlineButton with URL to the begin of InlineButtons(row)
func (buttons *Buttons) Prepend(data string, text string) {
	*buttons = append([]Button{{Data: data, Text: text}}, *buttons...)
}

// Append adds Button with URL to the end of Buttons(row)
func (buttons *Buttons) Append(data string, text string) {
	*buttons = append(*buttons, Button{Data: data, Text: text})
}

// InlineButtons converts Buttons to InlineButtons
// useful with universal methods that create keyboard (f.e. settigns) for both usual and inline keyboard
func (buttons *Buttons) InlineButtons() InlineButtons {
	row := InlineButtons{}

	for _, button := range *buttons {
		row.Append(button.Data, button.Text)

	}
	return row
}

// Markup generate Keyboard from Buttons ([]Button), chunking buttons by columns number
func (buttons *Buttons) Markup(columns int) Keyboard {
	keyboard := Keyboard{}

	col := 0

	row := Buttons{}
	len := len(*buttons)
	for i, button := range *buttons {
		row.Append(button.Data, button.Text)
		col++
		if col == columns || i == (len-1) {
			col = 0
			keyboard.AddRows(row)
			row = Buttons{}
		}
	}

	return keyboard
}

// Keyboard is generating Keyboard with 1 column
func (buttons Buttons) Keyboard() Keyboard {
	return buttons.Markup(1)
}

func (buttons Buttons) tg() [][]tg.KeyboardButton {
	return buttons.Keyboard().tg()
}

func (buttons Buttons) db() map[string]string {
	res := make(map[string]string)
	for _, button := range buttons {
		res[checksumString(button.Text)] = button.Data
	}
	return res
}

// Keyboard generates keyboard from 1 button
func (button Button) Keyboard() Keyboard {
	btns := Buttons{button}
	return btns.Keyboard()
}

func (button Button) tg() [][]tg.KeyboardButton {
	btns := Buttons{button}
	return btns.Keyboard().tg()
}

func (button Button) db() map[string]string {
	res := make(map[string]string)
	res[checksumString(button.Text)] = button.Data

	return res
}

func (keyboard Keyboard) db() map[string]string {
	res := make(map[string]string)
	for _, columns := range keyboard {
		for _, button := range columns {
			res[checksumString(button.Text)] = button.Data
		}
	}
	return res
}

// Keyboard generate keyboard for keyboard – just to match the KeyboardMarkup interface
func (keyboard Keyboard) Keyboard() Keyboard {
	return keyboard
}

func (keyboard Keyboard) tg() [][]tg.KeyboardButton {
	res := make([][]tg.KeyboardButton, len(keyboard))

	for r, columns := range keyboard {
		res[r] = make([]tg.KeyboardButton, len(keyboard[r]))
		c := 0
		for _, button := range columns {
			res[r][c] = tg.KeyboardButton{Text: button.Text}
			c++
		}
	}
	return res
}

// FindMessageByEventID find message by event id
func (c *Context) FindMessageByEventID(id string) (*Message, error) {
	if c.Bot() == nil {
		return nil, errors.New("Bot not set for the service")
	}
	return findMessageByEventID(c.db, c.Chat.ID, c.Bot().ID, id)
}

func findMessageByEventID(db *mgo.Database, chatID int64, botID int64, eventID string) (*Message, error) {
	msg := OutgoingMessage{}

	err := db.C("messages").Find(bson.M{"chatid": chatID, "botid": botID, "eventid": eventID}).Sort("-_id").One(&msg)
	if err != nil || msg.BotID == 0 {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

func findMessageByBsonID(db *mgo.Database, id bson.ObjectId) (*Message, error) {
	if !id.Valid() {
		return nil, errors.New("BSON ObjectId is not valid")
	}
	msg := OutgoingMessage{}
	err := db.C("messages").Find(bson.M{"_id": id}).One(&msg)
	if err != nil {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

func findMessage(db *mgo.Database, chatID int64, botID int64, msgID int) (*Message, error) {
	msg := OutgoingMessage{}
	err := db.C("messages").Find(bson.M{"chatid": chatID, "botid": botID, "msgid": msgID}).One(&msg)
	if err != nil {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

func findInlineMessage(db *mgo.Database, botID int64, inlineMsgID string) (*Message, error) {
	msg := OutgoingMessage{}
	err := db.C("messages").Find(bson.M{"botid": botID, "inlinemsgid": inlineMsgID}).One(&msg)
	if err != nil {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

func findLastOutgoingMessageInChat(db *mgo.Database, botID int64, chatID int64) (*Message, error) {

	msg := OutgoingMessage{}
	err := db.C("messages").Find(bson.M{"chatid": chatID, "botid": botID, "fromid": botID}).Sort("-msgid").One(&msg)
	if err != nil {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

func findLastMessageInChat(db *mgo.Database, botID int64, chatID int64) (*Message, error) {

	msg := OutgoingMessage{}
	err := db.C("messages").Find(bson.M{"chatid": chatID, "botid": botID}).Sort("-msgid").One(&msg)
	if err != nil {
		return nil, err
	}
	msg.Message.om = &msg
	return &msg.Message, nil
}

// SetChat sets the target chat to send the message
func (m *OutgoingMessage) SetChat(id int64) *OutgoingMessage {
	m.ChatID = id
	return m
}

// SetBackupChat set backup chat id that will be used in case message failed to sent to private chat (f.e. bot stopped or not initialized)
func (m *OutgoingMessage) SetBackupChat(id int64) *OutgoingMessage {
	m.BackupChatID = id
	return m
}

// SetDocument adds the file located at localPath with name fileName to the message
func (m *OutgoingMessage) SetDocument(localPath string, fileName string) *OutgoingMessage {
	m.FilePath = localPath
	m.FileName = fileName
	m.FileType = "document"
	return m
}

// SetImage adds the image file located at localPath with name fileName to the message
func (m *OutgoingMessage) SetImage(localPath string, fileName string) *OutgoingMessage {
	m.FilePath = localPath
	m.FileName = fileName
	m.FileType = "image"
	return m
}

// EnableFileRemoveAfter adds the flag to remove the file after message will be sent
func (m *OutgoingMessage) EnableFileRemoveAfter() *OutgoingMessage {
	m.FileRemoveAfter = true
	return m
}

// SetKeyboard sets the keyboard markup and Selective bool. If Selective is true keyboard will sent only for target users that you must @mention people in text or specify with SetReplyToMsgID
func (m *OutgoingMessage) SetKeyboard(k KeyboardMarkup, selective bool) *OutgoingMessage {
	m.Keyboard = true
	m.KeyboardMarkup = k.Keyboard()
	m.Selective = selective
	//todo: here is workaround for QT version. Keyboard with selective is not working
	return m
}

// SetInlineKeyboard sets the inline keyboard markup
func (m *OutgoingMessage) SetInlineKeyboard(k InlineKeyboardMarkup) *OutgoingMessage {
	m.InlineKeyboardMarkup = k.Keyboard()
	return m
}

// SetSelective sets the Selective mode for the keyboard. If Selective is true keyboard make sure to @mention people in text or specify message to reply with SetReplyToMsgID
func (m *OutgoingMessage) SetSelective(b bool) *OutgoingMessage {
	m.Selective = b
	return m
}

// SetSilent turns off notifications on iOS and make it silent on Android
func (m *OutgoingMessage) SetSilent(b bool) *OutgoingMessage {
	m.Silent = b
	return m
}

// SetOneTimeKeyboard sets the Onetime mode for keyboard. Keyboard will be hided after 1st use
func (m *OutgoingMessage) SetOneTimeKeyboard(b bool) *OutgoingMessage {
	m.OneTimeKeyboard = b
	return m
}

// SetResizeKeyboard sets the ResizeKeyboard to collapse keyboard wrapper to match the actual underneath keyboard
func (m *OutgoingMessage) SetResizeKeyboard(b bool) *OutgoingMessage {
	m.ResizeKeyboard = b
	return m
}

// SetCallbackAction sets the callback func that will be called when user press inline button with Data field
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *IncomingMessage) SetCallbackAction(handlerFunc interface{}, args ...interface{}) *IncomingMessage {
	m.Message.SetCallbackAction(handlerFunc, args...)
	//TODO: save reply action

	return m
}

// SetCallbackAction sets the callback func that will be called when user press inline button with Data field
func (m *OutgoingMessage) SetCallbackAction(handlerFunc interface{}, args ...interface{}) *OutgoingMessage {
	m.Message.SetCallbackAction(handlerFunc, args...)
	return m
}

// SetEditAction sets the edited func that will be called when user edit the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *IncomingMessage) SetEditAction(handlerFunc interface{}, args ...interface{}) *IncomingMessage {
	m.Message.SetEditAction(handlerFunc, args...)

	return m
}

// SetReplyAction sets the reply func that will be called when user reply the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *IncomingMessage) SetReplyAction(handlerFunc interface{}, args ...interface{}) *IncomingMessage {
	m.Message.SetReplyAction(handlerFunc, args...)
	//TODO: save reply action

	return m
}

// SetReplyAction sets the reply func that will be called when user reply the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *OutgoingMessage) SetReplyAction(handlerFunc interface{}, args ...interface{}) *OutgoingMessage {
	m.Message.SetReplyAction(handlerFunc, args...)
	return m
}

// SetCallbackAction sets the reply func that will be called when user reply the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *Message) SetCallbackAction(handlerFunc interface{}, args ...interface{}) *Message {
	service, err := detectServiceByBot(m.BotID)

	if err != nil {
		log.WithError(err).Errorf("SetCallbackAction detectServiceByBot")
	}
	funcName := service.getShortFuncPath(handlerFunc)

	err = verifyTypeMatching(handlerFunc, args...)

	if err != nil {
		log.WithError(err).Error("Can't verify onCallback args for " + funcName + ". Be sure to omit first arg of type '*integram.Context'")
		return m
	}

	bytes, err := encode(args)

	if err != nil {
		log.WithError(err).Error("Can't encode onCallback args")
		return m
	}

	m.OnCallbackData = bytes
	m.OnCallbackAction = funcName

	return m
}

// SetReplyAction sets the reply func that will be called when user reply the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *Message) SetReplyAction(handlerFunc interface{}, args ...interface{}) *Message {
	service, err := detectServiceByBot(m.BotID)

	if err != nil {
		log.WithError(err).Errorf("SetReplyAction detectServiceByBot")
	}

	funcName := service.getShortFuncPath(handlerFunc)

	if _, ok := actionFuncs[funcName]; !ok {
		log.Panic(errors.New("Action for '" + funcName + "' not registred in service's configuration!"))
		return m
	}

	err = verifyTypeMatching(handlerFunc, args...)

	if err != nil {
		log.WithError(err).Error("Can't verify onReply args for " + funcName + ". Be sure to omit first arg of type '*integram.Context'")
		return m
	}

	bytes, err := encode(args)

	if err != nil {
		log.WithError(err).Error("Can't encode onReply args")
		return m
	}

	m.OnReplyData = bytes
	m.OnReplyAction = funcName

	return m
}

// SetEditAction sets the edited func that will be called when user edit the message
// !!! Please note that you must omit first arg *integram.Context, because it will be automatically prepended as message reply received and will contain actual context
func (m *Message) SetEditAction(handlerFunc interface{}, args ...interface{}) *Message {
	service, err := detectServiceByBot(m.BotID)

	if err != nil {
		log.WithError(err).Errorf("SetEditAction detectServiceByBot")
	}

	funcName := service.getShortFuncPath(handlerFunc)

	if _, ok := actionFuncs[funcName]; !ok {
		log.Panic(errors.New("Action for '" + funcName + "' not registred in service's configuration!"))
		return m
	}

	err = verifyTypeMatching(handlerFunc, args...)

	if err != nil {
		log.WithError(err).Error("Can't verify onEdit args for " + funcName + ". Be sure to omit first arg of type '*integram.Context'")
		return m
	}

	bytes, err := encode(args)

	if err != nil {
		log.WithError(err).Error("Can't encode onEdit args")
		return m
	}

	m.OnEditData = bytes
	m.OnEditAction = funcName

	return m
}

// HideKeyboard will hide existing keyboard in the chat where message will be sent
func (m *OutgoingMessage) HideKeyboard() *OutgoingMessage {
	m.KeyboardHide = true
	return m
}

// EnableForceReply will automatically set the reply to this message and focus on the input field
func (m *OutgoingMessage) EnableForceReply() *OutgoingMessage {
	m.ForceReply = true
	return m
}

type messageSender interface {
	Send(m *OutgoingMessage) error
}

type scheduleMessageSender struct{}

var activeMessageSender = messageSender(scheduleMessageSender{})

var ErrorFlood = fmt.Errorf("Too many messages. You could not send the same message more than once per %d sec", antiFloodSameMessageTimeout)
var ErrorBadRequstPrefix = "Can't process your request: "

func (t scheduleMessageSender) Send(m *OutgoingMessage) error {
	if m.processed {
		return nil
	}

	if m.AntiFlood {
		db := mongoSession.Clone().DB(mongo.Database)
		defer db.Session.Close()
		msg, _ := findLastOutgoingMessageInChat(db, m.BotID, m.ChatID)
		if msg != nil && msg.om.TextHash == m.GetTextHash() && time.Now().Sub(msg.Date).Seconds() < antiFloodSameMessageTimeout {
			//log.Errorf("flood. mins %v", time.Now().Sub(msg.Date).Minutes())
			return ErrorFlood
		}
	}

	if m.Selective && m.ChatID > 0 {
		m.Selective = false
	}
	m.ID = bson.NewObjectId()

	if m.Selective && len(m.findUsernames()) == 0 && m.ReplyToMsgID == 0 {
		err := errors.New("Inconsistence. Selective is true but there are no @mention or ReplyToMsgID specified")
		log.WithField("chat", m.ChatID).Error(err)
		return err
	}

	if m.ParseMode == "HTML" {
		text := ""
		var err error
		if m.FilePath == "" {
			text, err = sanitize.HTMLAllowing(m.Text, []string{"a", "b", "strong", "i", "em", "a", "code", "pre"}, []string{"href"})
		} else {
			// formatiing is not supported for file captions
			text = sanitize.HTML(m.Text)
		}

		if err == nil && text != "" {
			m.Text = text
		}
	} else {
		text := sanitize.HTML(m.Text)
		if text != "" {
			m.Text = text
		}
	}
	var sendAfter time.Time
	if m.SendAfter != nil {
		sendAfter = *m.SendAfter
	} else {
		sendAfter = time.Now()
	}

	_, err := sendMessageJob.Schedule(0, sendAfter, &m)
	if err != nil {
		log.WithField("chat", m.ChatID).WithError(err).Error("Can't schedule sendMessageJob")
	} else {
		m.processed = true
	}
	return err
}

// Send put the message to the jobs queue
func (m *OutgoingMessage) Send() error {
	if m.ChatID == 0 {
		return errors.New("ChatID is empty")
	}

	if m.BotID == 0 {
		return errors.New("BotID is empty")
	}

	if m.Text == "" && m.FilePath == "" && m.Location == nil {
		return errors.New("Text, FilePath and Location are empty")
	}

	if m.ctx != nil && m.ctx.messageAnsweredAt == nil {
		n := time.Now()
		m.ctx.messageAnsweredAt = &n
	}

	return activeMessageSender.Send(m)
}

// SetSendAfter set the time to send the message
func (m *OutgoingMessage) SetSendAfter(after time.Time) *OutgoingMessage {
	m.SendAfter = &after
	return m
}

// AddEventID attach one or more event ID. You can use eventid to edit the message in case of additional webhook received or to ignore in case of duplicate
func (m *OutgoingMessage) AddEventID(id ...string) *OutgoingMessage {
	m.EventID = append(m.EventID, id...)
	return m
}

// EnableAntiFlood will check if the message wasn't already sent within last antiFloodSameMessageTimeout seconds
func (m *OutgoingMessage) EnableAntiFlood() *OutgoingMessage {
	m.AntiFlood = true

	return m
}

// SetTextFmt is a shorthand for SetText(fmt.Sprintf("%s %s %s", a, b, c))
func (m *OutgoingMessage) SetTextFmt(text string, a ...interface{}) *OutgoingMessage {
	m.Text = fmt.Sprintf(text, a...)
	return m
}

// SetText set the text of message to sent
// In case of documents and photo messages this text will be used in the caption
func (m *OutgoingMessage) SetText(text string) *OutgoingMessage {
	m.Text = text
	return m
}

// SetLocation set the location
func (m *OutgoingMessage) SetLocation(latitude, longitude float64) *OutgoingMessage {
	m.Location = &Location{Latitude: latitude, Longitude: longitude}
	return m
}

// DisableWebPreview indicates TG clients to not trying to resolve the URL's in the message
func (m *OutgoingMessage) DisableWebPreview() *OutgoingMessage {
	m.WebPreview = false
	return m
}

// EnableMarkdown sets parseMode to Markdown
func (m *OutgoingMessage) EnableMarkdown() *OutgoingMessage {
	m.ParseMode = "Markdown"
	return m
}

// EnableHTML sets parseMode to HTML
func (m *OutgoingMessage) EnableHTML() *OutgoingMessage {
	m.ParseMode = "HTML"
	return m
}

// SetParseMode sets parseMode: 'HTML' and 'markdown' supporting for now
func (m *OutgoingMessage) SetParseMode(s string) *OutgoingMessage {
	m.ParseMode = s
	return m
}

// SetReplyToMsgID sets parseMode: 'HTML' and 'markdown' supporting for now
func (m *OutgoingMessage) SetReplyToMsgID(id int) *OutgoingMessage {
	m.ReplyToMsgID = id
	return m
}

// GetTextHash generate MD5 hash of message's text
func (m *Message) GetTextHash() string {
	if m.Text != "" {
		return fmt.Sprintf("%x", md5.Sum([]byte(m.Text)))
	}
	return ""
}

// UpdateEventsID sets the event id and update it in DB
func (m *Message) UpdateEventsID(db *mgo.Database, eventID ...string) error {
	m.EventID = append(m.EventID, eventID...)
	f := bson.M{"botid": m.BotID}
	if m.InlineMsgID != "" {
		f["inlinemsgid"] = m.InlineMsgID
	} else {
		f["chatid"] = m.ChatID
		f["msgid"] = m.MsgID
	}
	return db.C("messages").Update(f, bson.M{"$addToSet": bson.M{"eventid": bson.M{"$each": eventID}}})
}

// Update will update existing message in DB
func (m *Message) Update(db *mgo.Database) error {

	if m.ID.Valid() {
		return db.C("messages").UpdateId(m.ID, bson.M{"$set": m})
	}
	return errors.New("Can't update message: ID is not set")
}

func initBots() error {
	var err error

	if err != nil {
		return err
	}

	gob.Register(&OutgoingMessage{})

	var tgPool *jobs.Pool
	if Config.IsMainInstance() || Config.IsSingleProcessInstance() {

		tgPool, err = jobs.NewPool(&jobs.PoolConfig{
			Key:        "_telegram",
			NumWorkers: Config.TGPool,
			BatchSize:  Config.TGPoolBatchSize,
		})

		if err != nil {
			return err
		}

		tgPool.SetMiddleware(beforeJob)
		tgPool.SetAfterFunc(afterJob)

		log.Infof("Job pool %v[%d] is ready", "_telegram", Config.TGPool)
	}
	// 23 retries mean maximum of 8 hours deferment (fibonacci sequence)
	sendMessageJob, err = jobs.RegisterTypeWithPoolKey("sendMessage", "_telegram", 23, sendMessage)
	if err != nil {
		log.WithError(err).Panic("RegisterTypeWithPoolKey sendMessage failed")
	}

	ensureStandAloneServiceJob, err = jobs.RegisterTypeWithPoolKey("ensureStandAloneService", "_telegram", 1, ensureStandAloneService)

	if err != nil {
		log.WithError(err).Panic("RegisterTypeWithPoolKey ensureService failed")
	}

	if Config.IsStandAloneServiceInstance() || Config.IsSingleProcessInstance() {
		for _, service := range services {

			bot := service.Bot()
			if bot == nil {
				continue
			}
			if !service.UseWebhookInsteadOfLongPolling {
				bot.listen()
			} else {
				_, err := bot.API.SetWebhook(tg.WebhookConfig{URL: bot.webhookURL()})
				if err != nil {
					log.WithError(err).WithField("botID", bot.ID).Error("Error on initial SetWebhook")
				}
			}
			log.Infof("%v is performing on behalf of @%v", service.Name, bot.Username)
		}
	}

	if tgPool != nil {
		err = tgPool.Start()
		log.Info("Telegram main pool started")

		if err != nil {
			return err
		}
	}
	return nil
}

var sendMessageJob, ensureStandAloneServiceJob *jobs.Type

func (m *Message) findUsernames() []string {
	r, _ := regexp.Compile("@([a-zA-Z0-9_]{5,})") // according to TG docs minimum username length is 5
	usernames := r.FindAllString(m.Text, -1)

	for index, username := range usernames {
		usernames[index] = username[1:]
	}
	return usernames

}

func GetRemoteFilePath(c *Context, fileID string) (string, error) {

	var fileRemotePath string
	c.ServiceCache("file_remote_"+fileID, &fileRemotePath)

	if fileRemotePath != "" {
		return fileRemotePath, nil
	}

	url, err := c.Bot().API.GetFileDirectURL(fileID)
	if err != nil {
		return "", err
	}

	c.SetServiceCache("file_remote_"+fileID, fileRemotePath, time.Hour*1)

	return url, nil
}

func GetLocalFilePath(c *Context, fileID string) (string, error) {

	var fileLocalPath string
	c.ServiceCache("file_"+fileID, &fileLocalPath)

	if fileLocalPath != "" {
		if _, err := os.Stat(fileLocalPath); os.IsNotExist(err) {
			fileLocalPath = ""
		}
	}

	if fileLocalPath == "" {
		url, err := GetRemoteFilePath(c, fileID)
		if err != nil {
			return "", err
		}
		fileLocalPath, err = c.DownloadURL(url)
		if err != nil {
			return "", err
		}
		c.SetServiceCache("file_"+fileID, fileLocalPath, time.Hour*24)
	}

	return fileLocalPath, nil
}

var GetFileMaxSizeExceedError = errors.New("Maximum allowed file size exceed")

type FileType string

const (
	FileTypeDocument FileType = "document"
	FileTypePhoto    FileType = "photo"
	FileTypeAudio    FileType = "audio"
	FileTypeSticker  FileType = "sticker"
	FileTypeVideo    FileType = "video"
	FileTypeVoice    FileType = "voice"
)

func fileTypeAllowed(allowedTypes []FileType, fileType FileType) bool {
	if len(allowedTypes) == 0 {
		return true
	}

	for _, t := range allowedTypes {
		if t == fileType {
			return true
		}
	}
	return false
}

type FileInfo struct {
	ID   string
	Name string
	Type FileType
	Mime string
	Size int64
}

func (info *FileInfo) Emoji() string {
	switch info.Type {
	case FileTypePhoto, FileTypeSticker:
		return "🖼"
	case FileTypeAudio:
		return "🎵"
	case FileTypeVideo:
		return "🎬"
	case FileTypeVoice:
		return "🗣"
	default:
		return "📎"
	}
}

func (m *IncomingMessage) GetFileInfo(c *Context, allowedTypes []FileType) (info FileInfo, err error) {
	if m.Sticker != nil && fileTypeAllowed(allowedTypes, FileTypeSticker) {
		info.Type = FileTypeSticker
		var remotePath string
		remotePath, err = GetRemoteFilePath(c, m.Sticker.FileID)
		if err != nil {
			return
		}

		info.Name = filepath.Base(remotePath)
		info.Size = int64(m.Sticker.FileSize)
		info.ID = m.Sticker.FileID

		return
	}

	if m.Audio != nil && fileTypeAllowed(allowedTypes, FileTypeAudio) {
		info.Type = FileTypeAudio

		if m.Audio.Performer == "" && m.Audio.Title == "" {
			if c.User.UserName != "" {
				info.Name += c.User.UserName
			} else if c.User.FirstName != "" {
				info.Name += filepath.Clean(c.User.FirstName)
			}
			if m.Caption != "" {
				info.Name += "_" + filepath.Clean(m.Caption)
			} else {
				info.Name += fmt.Sprintf("_%d", m.MsgID)
			}
		} else {
			info.Name = filepath.Clean(m.Audio.Performer + "-" + m.Audio.Title)
		}
		var remotePath string
		remotePath, err = GetRemoteFilePath(c, m.Audio.FileID)
		if err != nil {
			return
		}
		info.Name += filepath.Ext(remotePath)
		info.ID = m.Audio.FileID
		info.Size = int64(m.Audio.FileSize)

		return
	}

	if m.Document != nil && fileTypeAllowed(allowedTypes, FileTypeDocument) {
		info = FileInfo{
			Type: FileTypeDocument,
			Size: int64(m.Document.FileSize),
			Name: m.Document.FileName,
			ID:   m.Document.FileID,
			Mime: m.Document.MimeType,
		}

		return
	}

	if m.Video != nil && fileTypeAllowed(allowedTypes, FileTypeVideo) {
		info.Type = FileTypeVideo

		if c.User.UserName != "" {
			info.Name += c.User.UserName
		} else if c.User.FirstName != "" {
			info.Name += filepath.Clean(c.User.FirstName)
		}
		if m.Caption != "" {
			info.Name += "_" + filepath.Clean(m.Caption)
		} else {
			info.Name += fmt.Sprintf("_%d", m.MsgID)
		}

		var remotePath string

		remotePath, err = GetRemoteFilePath(c, m.Video.FileID)
		if err != nil {
			return
		}
		info.Name += filepath.Ext(remotePath)
		info.ID = m.Video.FileID
		info.Size = int64(m.Video.FileSize)

		return
	}

	if m.Voice != nil && fileTypeAllowed(allowedTypes, FileTypeVoice) {
		info.Type = FileTypeVoice

		if c.User.UserName != "" {
			info.Name += c.User.UserName
		} else if c.User.FirstName != "" {
			info.Name += filepath.Clean(c.User.FirstName)
		}
		if m.Caption != "" {
			info.Name += "_" + filepath.Clean(m.Caption)
		} else {
			info.Name += fmt.Sprintf("_%d", m.MsgID)
		}

		var remotePath string
		remotePath, err = GetRemoteFilePath(c, m.Voice.FileID)
		if err != nil {
			return
		}
		info.Name += filepath.Ext(remotePath)
		info.ID = m.Voice.FileID
		info.Size = int64(m.Voice.FileSize)

		return
	}

	if m.Photo != nil && len(*m.Photo) > 0 && fileTypeAllowed(allowedTypes, FileTypePhoto) {
		info.Type = FileTypePhoto

		if c.User.UserName != "" {
			info.Name += c.User.UserName
		} else if c.User.FirstName != "" {
			info.Name += filepath.Clean(c.User.FirstName)
		}
		if m.Caption != "" {
			info.Name += "_" + filepath.Clean(m.Caption)
		} else {
			info.Name += fmt.Sprintf("_%d", m.MsgID)
		}
		info.Name += ".jpg"

		largestPhoto := (*m.Photo)[len(*m.Photo)-1]
		info.ID = largestPhoto.FileID
		info.Size = int64(largestPhoto.FileSize)
		info.Mime = "image/jpeg"

		if err != nil {
			return
		}

		return
	}
	return
}

func (m *IncomingMessage) GetFile(c *Context, allowedTypes []FileType, maxSize int) (localPath string, fileInfo FileInfo, err error) {

	fileInfo, err = m.GetFileInfo(c, allowedTypes)
	if err != nil {
		return
	}

	if maxSize > 0 && fileInfo.Size > int64(maxSize) {
		err = GetFileMaxSizeExceedError
		return
	}

	localPath, err = GetLocalFilePath(c, fileInfo.ID)

	return
}

func detectTargetUsersID(db *mgo.Database, m *Message) []int64 {
	if m.ChatID > 0 {
		return []int64{m.ChatID}
	}

	var usersID []int64

	// 1) If message is reply to message - add original message's sender
	if m.ReplyToMsgID > 0 {
		msg, err := findMessage(db, m.ChatID, m.BotID, m.ReplyToMsgID)
		if err == nil && msg.FromID > 0 {
			usersID = append(usersID, msg.FromID)
		}
	}

	// 2) Trying to find mentions in the message's text
	usernames := m.findUsernames()

	var users []struct {
		ID int64 `bson:"_id"`
	}
	db.C("users").Find(bson.M{"username": bson.M{"$in": usernames}}).Select(bson.M{"_id": 1}).All(&users)

	for _, user := range users {
		if len(usersID) == 0 || usersID[0] != user.ID {
			usersID = append(usersID, user.ID)
		}
	}
	return usersID
}

func botByID(ID int64) *Bot {
	if bot, exists := botPerID[ID]; exists {
		return bot
	}

	return nil
}

func sendMessage(m *OutgoingMessage) error {
	//log.Infof("sendMessage chat=%d ts=%d text=%s",m.ChatID, m.ID.Time().UnixNano(), m.Text)
	msg := tg.MessageConfig{Text: m.Text, BaseChat: tg.BaseChat{ChatID: m.ChatID}}

	db := mongoSession.Clone().DB(mongo.Database)
	defer db.Session.Close()
	if blacklisted, _ := db.C("chats").Find(bson.M{"_id": m.ChatID, "blacklisted": true}).Count(); blacklisted > 0 {
		log.Errorf("TG MSG not sent: chat %d blacklisted", m.ChatID)
		return nil
	}

	if m.ChatID == 0 {
		return errors.New("ChatID empty")
	}

	bot := botByID(m.BotID)

	if bot == nil {
		return fmt.Errorf("Can't send TG message: Unknown bot id=%d", m.BotID)
	}
	var err error
	var tgMsg tg.Message
	var rescheduled bool

	startedAt := time.Now()
	if m.FilePath != "" {
		if _, err := os.Stat(m.FilePath); os.IsNotExist(err) {
			log.Errorf("Can't send message with attachment, file not exists: %s", m.FilePath)
			return nil
		}

		if m.FileType == "image" {

			msg := tg.NewPhotoUpload(m.ChatID, m.FilePath)
			msg.FileName = m.FileName
			msg.Caption = m.Text
			if m.ReplyToMsgID != 0 {
				msg.BaseChat.ReplyToMessageID = m.ReplyToMsgID
			}
			tgMsg, err = bot.API.Send(msg)

		} else {
			msg := tg.NewDocumentUpload(m.ChatID, m.FilePath)
			msg.FileName = m.FileName
			msg.Caption = m.Text
			if m.ReplyToMsgID != 0 {
				msg.BaseChat.ReplyToMessageID = m.ReplyToMsgID
			}
			tgMsg, err = bot.API.Send(msg)

		}

		if m.FileRemoveAfter {
			defer func() {
				// message not rescheduled
				if err == nil && !rescheduled {
					err2 := os.Remove(m.FilePath)
					if err2 != nil {
						log.WithError(err).WithField("path", m.FilePath).Error("Error removing message's file")
					}
				}
			}()
		}

	} else if m.Location != nil {
		tgMsg, err = bot.API.Send(tg.LocationConfig{BaseChat: msg.BaseChat, Latitude: m.Location.Latitude, Longitude: m.Location.Longitude})
	} else {

		if m.KeyboardHide {
			msg.ReplyMarkup = tg.ReplyKeyboardRemove{RemoveKeyboard: true, Selective: m.Selective}
		}

		if m.ForceReply {
			msg.ReplyMarkup = tg.ForceReply{ForceReply: true, Selective: m.Selective}
		}
		// Keyboard will overridde HideKeyboard
		if m.KeyboardMarkup != nil && len(m.KeyboardMarkup) > 0 {
			msg.ReplyMarkup = tg.ReplyKeyboardMarkup{Keyboard: m.KeyboardMarkup.tg(), OneTimeKeyboard: m.OneTimeKeyboard, Selective: m.Selective, ResizeKeyboard: m.ResizeKeyboard}
		}

		if len(m.InlineKeyboardMarkup.Buttons) > 0 {
			msg.ReplyMarkup = tg.InlineKeyboardMarkup{InlineKeyboard: m.InlineKeyboardMarkup.tg()}
		}

		msg.DisableWebPagePreview = !m.WebPreview

		msg.DisableNotification = m.Silent

		if m.ReplyToMsgID != 0 {
			msg.BaseChat.ReplyToMessageID = m.ReplyToMsgID
		}

		if m.ParseMode != "" {
			msg.ParseMode = m.ParseMode
		}

		tgMsg, err = bot.API.Send(msg)
	}

	if err == nil {

		log.Debugf("TG MSG sent, id = %v %.2f secs spent", tgMsg.MessageID, time.Now().Sub(startedAt).Seconds())
		m.MsgID = tgMsg.MessageID
		m.Date = time.Now()

		err = saveKeyboard(m, db)
		if err != nil {
			log.WithError(err).Error("Error processing keyboard")
		}

		m.TextHash = m.GetTextHash()
		m.Text = ""

		err = db.C("messages").Insert(&m)
		if err != nil {
			log.WithError(err).Error("Error outgoing inserting message in db")
		}

		return nil
	}

	if tgErr, ok := err.(tg.Error); ok {
		//  Todo: Bad workaround to catch network errors
		if tgErr.Code == 0 {
			log.WithError(err).Warn("Network error while sending a message")
			// pass through the error so the job will be rescheduled
			return err
		} else if tgErr.Code == 500 {
			log.WithError(err).Warn("TG dc is down while sending a message")
			// pass through the error so the job will be rescheduled
			return err
		} else if tgErr.IsMessageNotFound() {

			log.WithError(err).WithFields(log.Fields{"msgid": m.ReplyToMsgID, "chat": m.ChatID, "bot": m.BotID}).Warn("TG message we are replying on is no longer exists")
			// looks like the message we replying on is no longer exists...
			m.ReplyToMsgID = 0
			rescheduled = true
			_, err := sendMessageJob.Schedule(0, time.Now(), &m)
			if err != nil {
				log.WithField("chat", m.ChatID).WithError(err).Error("Can't reschedule sendMessageJob")
			}
			return nil
		} else if tgErr.ChatMigrated() {
			// looks like the the chat we trying to send the message is migrated to supergroup
			log.Warnf("sendMessage error: Migrated to %v", tgErr.Parameters.MigrateToChatID)

			db := mongoSession.Clone().DB(mongo.Database)
			defer db.Session.Close()
			migrateToSuperGroup(db, m.ChatID, tgErr.Parameters.MigrateToChatID)

			// todo: in rare case this can produce duplicate messages for incoming webhooks
			if err != nil {
				log.WithField("chat", m.ChatID).WithError(err).Error("Can't reschedule sendMessageJob")
			}

			m.ChatID = tgErr.Parameters.MigrateToChatID

			return nil
		} else if tgErr.BotStoppedForUser() {

			// Todo: Problems can appear when we rely on this user message (e.g. not webhook msg)
			db := mongoSession.Clone().DB(mongo.Database)
			defer db.Session.Close()

			serviceName := bot.services[0].Name

			key := "protected." + serviceName + ".botstoppedorkickedat"
			db.C("chats").Update(bson.M{"_id": m.ChatID, key: bson.M{"$exists": false}}, bson.M{"$set": bson.M{key: time.Now()}})

			log.WithField("chat", m.ChatID).WithField("bot", m.BotID).Warn("sendMessage error: Bot stopped by user")
			if m.BackupChatID != 0 {
				if m.BackupChatID != m.ChatID {
					// if this fall from private messages - add the mention and selective to grace notifications and protect the keyboard
					if m.ChatID > 0 && m.BackupChatID < 0 {
						db := mongoSession.Clone().DB(mongo.Database)
						defer db.Session.Close()
						username := findUsernameByID(db, m.ChatID)
						if username != "" {
							m.Text = "@" + username + " " + m.Text
							m.Selective = true
						}
					}
					m.ChatID = m.BackupChatID
					rescheduled = true
					_, err := sendMessageJob.Schedule(0, time.Now(), &m)
					return err
				}

				return errors.New("BackupChatID failed")

			}
			return nil
		} else if tgErr.ChatNotFound() {
			// usually this means that user not initialized the private chat with the bot
			log.WithField("chat", m.ChatID).WithField("bot", m.BotID).Warn("sendMessage error: Chat not found")
			if m.BackupChatID != 0 {
				if m.BackupChatID != m.ChatID {
					// if this fall from private messages - add the mention and selective to grace notifications and protect the keyboard
					if m.ChatID > 0 && m.BackupChatID < 0 {
						db := mongoSession.Clone().DB(mongo.Database)
						defer db.Session.Close()
						username := findUsernameByID(db, m.ChatID)
						if username != "" {
							m.Text = "@" + username + " " + m.Text
							m.Selective = true
						}
					}
					rescheduled = true
					m.ChatID = m.BackupChatID
					_, err := sendMessageJob.Schedule(0, time.Now(), &m)
					return err
				}

				return errors.New("BackupChatID failed")
			}
			return nil
		} else if tgErr.BotKicked() {

			db := mongoSession.Clone().DB(mongo.Database)
			defer db.Session.Close()
			serviceName := bot.services[0].Name
			if len(bot.services) == 1 {
				removeHooksForChat(db, serviceName, m.ChatID)
			}

			key := "protected." + serviceName + ".botstoppedorkickedat"
			db.C("chats").Update(bson.M{"_id": m.ChatID, key: bson.M{"$exists": false}}, bson.M{"$set": bson.M{key: time.Now()}})

			log.WithField("chat", m.ChatID).WithField("bot", m.BotID).Warn("sendMessage error: Bot kicked")

			return nil
		} else if tgErr.ChatDiactivated() {
			db := mongoSession.Clone().DB(mongo.Database)
			defer db.Session.Close()
			bot := botByID(m.BotID)

			if len(bot.services) == 1 {
				removeHooksForChat(db, bot.services[0].Name, m.ChatID)
			}

			db.C("chats").UpdateId(m.ChatID, bson.M{"$set": bson.M{"deactivated": true}})
			log.WithField("chat", m.ChatID).WithField("bot", m.BotID).Warn("sendMessage error: Chat deactivated")
			return nil
		} else if tgErr.TooManyRequests() {
			log.WithField("chat", m.ChatID).WithField("bot", m.BotID).Warn("sendMessage error: TooManyRequests")

			delay := 60

			if tgErr.Parameters != nil && tgErr.Parameters.RetryAfter > 0 {
				delay = tgErr.Parameters.RetryAfter
			}

			rescheduled = true
			_, err := sendMessageJob.Schedule(0, time.Now().Add(time.Duration(delay+rand.Intn(10))*time.Second), &m)
			return err
		} else if tgErr.IsParseError() {

			if offset := tgErr.ParseErrorOffset(); offset > -1 {

				var escapedSymbol = ""
				if m.ParseMode == "Markdown" {
					log.WithError(tgErr.Err).WithField("chat", m.ChatID).WithField("bot", m.BotID).Error("Bad Markdown in the text")

					mrk := MarkdownRichText{}
					escapedSymbol = mrk.Esc(m.Text[offset:offset+1])
				} else {
					log.WithError(tgErr.Err).WithField("chat", m.ChatID).WithField("bot", m.BotID).Error("Bad HTML in the text")

					mrk := HTMLRichText{}
					escapedSymbol = mrk.EncodeEntities(m.Text[offset:offset+1])
				}

				m.SetText(m.Text[0:offset] + escapedSymbol + m.Text[offset+1:])

				rescheduled = true
				_, err := sendMessageJob.Schedule(0, time.Now(), &m)
				return err
			}
		}

		log.WithError(err).WithField("chat", m.ChatID).WithField("bot", m.BotID).Error("TG error while sending a message")
		return nil
	}
	log.WithError(err).WithField("chat", m.ChatID).Error("Error while sending a message")
	return err
}