// модуль для использования Телеграмм Клиента (или бота)
package telegram_client

import (
	"bufio"
	"context"
	"fmt"
	"github.com/ManyakRus/starter/logger"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/ManyakRus/starter/contextmain"
	"github.com/ManyakRus/starter/micro"
	"github.com/ManyakRus/starter/stopapp"

	"github.com/gotd/td/clock"
	"github.com/gotd/td/telegram/message"
	"github.com/gotd/td/tg"
	"github.com/gotd/td/tgerr"

	"golang.org/x/crypto/ssh/terminal"

	"github.com/go-faster/errors"
	"github.com/gotd/contrib/bg"
	"github.com/gotd/td/session"
	"github.com/gotd/td/telegram"
	"github.com/gotd/td/telegram/auth"
)

// filenameSession - имя файла сохранения сессии мессенджера Телеграм
var filenameSession string

// client - клиент соединения мессенджера Телеграм
var client *telegram.Client

// lastSendTime - время последней отправки сообщения и мьютекс
var lastSendTime = lastSendTimeMutex{}

// log - глобальный логгер приложения
var log = logger.GetLog()

// stopTelegramFunc - функция остановки соединения с мессенджером Телеграм
var stopTelegramFunc bg.StopFunc

// MAX_MESSAGE_LEN - максимальная длина сообщения
const MAX_MESSAGE_LEN = 4096

// MaxSendMessageCountIn1Second - максимальное количество сообщений в 1 секунду
var MaxSendMessageCountIn1Second float32 = 0.13 //0.13 =4 сообщения в секунду

// lastSendTimeMutex - структура хранения времени последней отправки и мьютекс
type lastSendTimeMutex struct {
	time time.Time
	sync.Mutex
}

// noSignUp can be embedded to prevent signing up.
type noSignUp struct{}

// Settings хранит все нужные переменные окружения
var Settings SettingsINI

// SettingsINI - структура для хранения всех нужных переменных окружения
// TELEGRAM_APP_ID, TELEGRAM_APP_HASH - первоначально получить по ссылке: https://my.telegram.org/apps
// TELEGRAM_PHONE_FROM - номер телефона с которого отправляются сообщения
type SettingsINI struct {
	TELEGRAM_APP_ID          int
	TELEGRAM_APP_HASH        string
	TELEGRAM_PHONE_FROM      string
	TELEGRAM_PHONE_SEND_TEST string
}

// SendMessage - отправка сообщения в мессенджер Телеграм
// возвращает:
// id = id отправленного сообщения в telegram
// err = error
func SendMessage(phone_send_to string, text string) (int, error) {
	var id int
	//var is_sent bool
	//

	if client == nil {
		CreateTelegramClient(nil)
		//if err != nil {
		//	log.Error("ConnectTelegram() error: ", err)
		//	return 0, false, err
		//}
	}

	if text == "" {
		text1 := "ConnectTelegram() text id empty ! "
		log.Error(text1)
		err := errors.New(text1)
		return 0, err
	}

	if phone_send_to == "" {
		text1 := "ConnectTelegram() phone_send_to id empty ! "
		log.Error(text1)
		err := errors.New(text1)
		return 0, err
	}

	TimeLimit()
	log.Debug("phone_send_to: ", phone_send_to, ", text: "+text)

	text = micro.SubstringLeft(text, MAX_MESSAGE_LEN)

	//
	api := client.API()

	//ctxMain := contextmain.GetContext()
	ctxMain := context.Background()
	ctx, cancel := context.WithTimeout(ctxMain, 60*time.Second)
	defer cancel()

	sender := message.NewSender(api)

	//peer := api.ContactsImportContacts()
	//target0 := sender.To(peer)

	target := sender.Resolve(phone_send_to)
	target.NoForwards()

	//отправка сообщения
	UpdatesClass, err := target.Text(ctx, text)

	//проверка на ошибки
	isFlood := false
	if err != nil {
		textFind := "peer: can't resolve phone"
		if micro.SubstringLeft(err.Error(), len(textFind)) == textFind {
			err2 := AddContact(ctx, phone_send_to)

			isFlood = FloodWait(ctx, err2) //ожидание при ошибке FloodWait
			if isFlood {
				return SendMessage(phone_send_to, text)
			}

			if err2 == nil {
				return SendMessage(phone_send_to, text)
			} else {
				log.Error("not send, text: " + err.Error())
				return 0, err
			}
		} else {
			isFlood = FloodWait(ctx, err) //ожидание при ошибке FloodWait
			if isFlood {
				return SendMessage(phone_send_to, text)
			}
		}

	}

	if UpdatesClass != nil {
		id = findIdFromUpdatesClass(UpdatesClass)
	}
	log.Debug("id: ", id, ", error: ", err, ", text: "+text)

	//log.Print("Success")

	// Return to close client connection and free up resources.
	// Client is closed.
	return id, err

}

// AddContact - добавляет новый контакт в список контактов Телеграм
func AddContact(ctx context.Context, phone_send_to string) error {
	var err error

	if phone_send_to == "" {
		text1 := "phone_send_to='' !"
		err := errors.New(text1)
		log.Error(text1)
		return err
	}

	TimeLimit()

	api := client.API()

	//var contacts []tg.InputPhoneContact
	contact := tg.InputPhoneContact{}
	contact.Phone = phone_send_to
	contact.FirstName = phone_send_to

	contacts := make([]tg.InputPhoneContact, 1)
	contacts = append(contacts, contact)

	ContactsImportedContacts, err := api.ContactsImportContacts(ctx, contacts)
	if ContactsImportedContacts == nil {
		text1 := "ContactsImportedContacts == nil. Не удалось добавить контакт !"
		err = errors.New(text1)
		log.Error(text1)
	} else if ContactsImportedContacts.Imported == nil {
		text1 := "ContactsImportedContacts.Imported =nil. Не удалось добавить контакт !"
		err = errors.New(text1)
		log.Error(text1)
	} else if len(ContactsImportedContacts.Imported) == 0 {
		text1 := "ContactsImportedContacts.Imported len=0. Не удалось добавить контакт !"
		err = errors.New(text1)
		log.Error(text1)
	}

	return err
}

// SignUp - обязательная функция клиента Телеграм
func (noSignUp) SignUp(context.Context) (auth.UserInfo, error) {
	return auth.UserInfo{}, errors.New("not implemented")
}

// AcceptTermsOfService - обязательная функция клиента Телеграм
func (noSignUp) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error {
	if ctx == nil {
		text1 := "telegramclient.AcceptTermsOfService() error: Context=nil"
		return errors.New(text1)
	}

	return &auth.SignUpRequired{TermsOfService: tos}
}

// termAuth implements authentication via terminal.
type termAuth struct {
	noSignUp

	phone string
}

// Phone - обязательная функция клиента Телеграм
func (a termAuth) Phone(_ context.Context) (string, error) {
	return a.phone, nil
}

// Password - обязательная функция клиента Телеграм
// ввод пароля с терминала
func (a termAuth) Password(ctx context.Context) (string, error) {
	if ctx == nil {
		return "", nil
	}

	fmt.Print("Enter 2FA password: ")
	bytePwd, err := terminal.ReadPassword(0)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(bytePwd)), nil
}

// Code - обязательная функция клиента Телеграм
// ввод кода CODE с терминала
func (a termAuth) Code(ctx context.Context, _ *tg.AuthSentCode) (string, error) {
	if ctx == nil {
		return "", nil
	}

	//Stdin, _ := io.Pipe() //нужен т.к. не работает в тест

	//r, _ := io.Pipe()
	//scanner := bufio.NewScanner(r)
	//msg := "Enter code from telegram: "
	//fmt.Fprintln(os.Stdout, msg)
	//
	//scanner.Scan()
	//if err := scanner.Err(); err != nil {
	//	log.Fatal(err)
	//}
	//code := scanner.Text()
	//if len(code) == 0 {
	//	log.Fatal("empty input")
	//}

	fmt.Print("Enter code: ")
	code, err := bufio.NewReader(os.Stdin).ReadString('\n')
	//code, err := bufio.NewReader(os.Stdin).ReadString('\n')
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(code), nil
}

// memorySession implements in-memory session storage.
// Goroutine-safe.
type memorySession struct {
	mux  sync.RWMutex
	data []byte
}

// LoadSession loads session from memory.
func (s *memorySession) LoadSession(context.Context) ([]byte, error) {
	if s == nil {
		return nil, session.ErrNotFound
	}

	s.mux.RLock()
	defer s.mux.RUnlock()

	// read the whole file at once
	cpy, err := ioutil.ReadFile(filenameSession)
	if err != nil {
		cpy = nil
		log.Error(err)
		return nil, nil
		//return nil, session.ErrNotFound
	}

	////было
	//if len(s.data) == 0 {
	//	return nil, session.ErrNotFound
	//}
	//cpy = append([]byte(nil), s.data...)

	return cpy, nil
}

// StoreSession stores session to memory.
func (s *memorySession) StoreSession(ctx context.Context, data []byte) error {
	if ctx == nil {
		text1 := "telegramclient.StoreSession() error: Context=nil"
		return errors.New(text1)
	}

	s.mux.Lock()
	//s.data = data

	// write the whole body at once
	err := ioutil.WriteFile(filenameSession, data, 0644)
	if err != nil {
		panic(err)
	}

	s.mux.Unlock()

	return nil
}

// CreateTelegramClient создание клиента Телеграм
func CreateTelegramClient(func_OnNewMessage func(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error) {
	// https://core.telegram.org/api/obtaining_api_id

	if Settings.TELEGRAM_APP_ID == 0 {
		FillSettings()
	}

	programDir := micro.ProgramDir()
	filenameSession = programDir + "session.txt"

	sessionStorage := &memorySession{}

	dispatcher := tg.NewUpdateDispatcher()
	client = telegram.NewClient(Settings.TELEGRAM_APP_ID, Settings.TELEGRAM_APP_HASH,
		telegram.Options{
			SessionStorage: sessionStorage,
			UpdateHandler:  dispatcher,
		})

	if func_OnNewMessage != nil {
		dispatcher.OnNewMessage(func_OnNewMessage)
	}

	return
}

// OnNewMessage_Test - пример функции для получения новых сообщений
func OnNewMessage_Test(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error {
	var err error

	m, ok := u.Message.(*tg.Message)
	if !ok || m.Out {
		// Outgoing message, not interesting.
		return nil
	}

	// тестовый пример эхо
	// Helper for sending messages.
	api := client.API()
	sender := message.NewSender(api)

	// Sending reply.
	_, err = sender.Reply(entities, u).Text(ctx, m.Message)

	return err
}

// TimeLimit пауза для ограничения количество сообщений в секунду
func TimeLimit() {
	//if MaxSendMessageCountIn1Second == 0 {
	//	return
	//}

	lastSendTime.Lock()
	defer lastSendTime.Unlock()

	if lastSendTime.time.IsZero() {
		lastSendTime.time = time.Now()
		return
	}

	t := time.Now()
	ms := int(t.Sub(lastSendTime.time).Milliseconds())
	msNeedWait := int(1000 / MaxSendMessageCountIn1Second)
	if ms < msNeedWait {
		micro.Sleep(msNeedWait - ms)
	}

	lastSendTime.time = time.Now()
}

// ConnectTelegram подключение к серверу Телеграм
func ConnectTelegram() error {

	ctxMain := context.Background()
	//ctxMain := contextmain.GetContext()
	ctx, cancel := context.WithTimeout(ctxMain, 60*time.Second) //60
	defer cancel()

	bg.WithContext(ctx)
	var err error
	//Option := bg.WithContext(ctx)
	stopTelegramFunc, err = bg.Connect(client)
	if err != nil {
		log.Fatalln("Can not connect to Telegram ! Error: ", err)
	}

	micro.Sleep(100) //не успевает
	//for i := 1; i <= 5; i++ {
	//	err = client.Ping(ctx)
	//	if err != nil {
	//		micro.Sleep(1000)
	//	}
	//}

	//fmt.Println("Client: ", client)

	//
	flow := auth.NewFlow(
		termAuth{phone: Settings.TELEGRAM_PHONE_FROM},
		auth.SendCodeOptions{},
	)

	if err := client.Auth().IfNecessary(ctx, flow); err != nil {
		return err
	}

	return nil
}

// findIdFromUpdatesClass - возвращает id сообщения из ответа Телеграм сервера
func findIdFromUpdatesClass(UpdatesClass tg.UpdatesClass) int {
	var id int

	switch v := UpdatesClass.(type) {
	case *tg.UpdatesTooLong: // updatesTooLong#e317af7e
	case *tg.UpdateShortMessage: // updateShortMessage#313bc7f8
	case *tg.UpdateShortChatMessage: // updateShortChatMessage#4d6deea5
	case *tg.UpdateShort: // updateShort#78d4dec1
	case *tg.UpdatesCombined: // updatesCombined#725b04c3
	case *tg.Updates: // updates#74ae4240
		UpdatesClass1 := UpdatesClass.(*tg.Updates)
		for _, row1 := range UpdatesClass1.Updates {
			switch row1.(type) {
			case *tg.UpdateMessageID:
				{
					rowV := row1.(*tg.UpdateMessageID)
					id = rowV.ID
				}
			case *tg.UpdateNewMessage:
				{
					rowV := row1.(*tg.UpdateNewMessage)
					MessageV := rowV.Message.(*tg.Message)
					//is_sent = MessageV.Out
					if id == 0 {
						id = MessageV.ID
					}
				}

			}
		}

	case *tg.UpdateShortSentMessage: // updateShortSentMessage#9015e101
		UpdatesClass1 := UpdatesClass.(*tg.UpdateShortSentMessage)
		id = UpdatesClass1.ID
	default:
		log.Fatalln("Wrong type: ", v)
	}

	return id
}

// FindMessageByID - находит сообщение на сервере Телеграм по id
func FindMessageByID(ctx context.Context, id int) (*tg.Message, error) {
	var Otvet *tg.Message

	if id == 0 {
		text1 := "telegramclient.FindMessageByID() id=0 !"
		err := errors.New(text1)
		return Otvet, err
	}

	api := client.API()

	var IMC []tg.InputMessageClass
	IMC = append(IMC, &tg.InputMessageID{ID: id})

	MMC, err := api.MessagesGetMessages(ctx, IMC)
	if err != nil {
		return Otvet, err
	}

	if MMC == nil {
		return Otvet, err
	}

	MMCV := MMC.(*tg.MessagesMessages)
	Messages := MMCV.Messages
	for _, v := range Messages {
		Otvet = v.(*tg.Message)
		//Otvet.MediaUnread
	}

	return Otvet, err
}

// WaitStop - ожидает отмену глобального контекста
func WaitStop() {
	//stopapp.GetWaitGroup_Main().Add(1)
	select {
	case <-contextmain.GetContext().Done():
		log.Warn("Context app is canceled.")
	}

	//
	stopapp.WaitTotalMessagesSendingNow("telegram")

	//
	StopTelegram()

	//
	stopapp.GetWaitGroup_Main().Done()
}

// StopTelegram - остановка работы клиента Телеграм
func StopTelegram() {
	if stopTelegramFunc != nil {
		err := stopTelegramFunc()
		if err != nil {
			log.Error("error: ", err)
		}
	}

}

// FloodWait sleeps required duration and returns true if err is FLOOD_WAIT
// or false and context or original error otherwise.
func FloodWait(ctx context.Context, err error) bool {
	otvet := false

	if err == nil {
		return false
	}

	sec, ok := AsFloodWait(err)
	if ok {
		otvet = true
		log.Debug("isFlood sec: ", sec)

		var duration time.Duration
		duration = time.Second * time.Duration(sec)
		timer := clock.System.Timer(duration)
		defer clock.StopTimer(timer)

		select {
		case <-timer.C():
			return otvet
		case <-ctx.Done():
			return otvet
		}
	} else {
		log.Warn("AsFloodWait() ok =false")
	}

	return otvet
}

// AsFloodWait returns wait duration and true boolean if err is
// the "FLOOD_WAIT" error.
//
// Client should wait for that duration before issuing new requests with
// same method.
func AsFloodWait(err error) (d int, ok bool) {
	rpcErr, ok := tgerr.AsType(err, tgerr.ErrFloodWait)
	//log.Debugf("error response: %v", rpcErr)
	if ok {
		return rpcErr.Argument, true
	}
	return 0, false
}

// StartTelegram - подключается к телеграмму, запускает остановку приложения.
// func_OnNewMessage - функция для приёма новых сообщений
func StartTelegram(func_OnNewMessage func(ctx context.Context, entities tg.Entities, u *tg.UpdateNewMessage) error) {
	CreateTelegramClient(func_OnNewMessage)

	err := ConnectTelegram()
	if err != nil {
		log.Fatalln("Can not login to telegram ! Error: ", err)
	}

	stopapp.GetWaitGroup_Main().Add(1)
	go WaitStop()

}

// FillSettings загружает переменные окружения в структуру из переменных окружения
func FillSettings() {
	Settings = SettingsINI{}
	Settings.TELEGRAM_APP_ID, _ = strconv.Atoi(os.Getenv("TELEGRAM_APP_ID"))
	Settings.TELEGRAM_APP_HASH = os.Getenv("TELEGRAM_APP_HASH")
	Settings.TELEGRAM_PHONE_FROM = os.Getenv("TELEGRAM_PHONE_FROM")
	Settings.TELEGRAM_PHONE_SEND_TEST = os.Getenv("TELEGRAM_PHONE_SEND_TEST")

	if Settings.TELEGRAM_APP_ID == 0 {
		log.Panicln("Need fill TELEGRAM_APP_ID ! in os.ENV ")
	}

	if Settings.TELEGRAM_APP_HASH == "" {
		log.Panicln("Need fill TELEGRAM_APP_HASH ! in os.ENV ")
	}

	if Settings.TELEGRAM_PHONE_FROM == "" {
		log.Panicln("Need fill TELEGRAM_PHONE_FROM ! in os.ENV ")
	}

	if Settings.TELEGRAM_PHONE_SEND_TEST == "" && micro.IsTestApp() == true {
		log.Info("Need fill TELEGRAM_PHONE_SEND_TEST ! in os.ENV ")
	}

}
