package client

import (
	"agent/client/events"
	"agent/client/events/opts"
	eopts "agent/client/events/opts"
	"agent/client/modules"
	"agent/commons/adata"
	ibytes "agent/commons/bytes"
	"agent/commons/crypto"
	"agent/commons/debug"
	"agent/commons/utils"
	"agent/commons/utime"
	"agent/commons/vars"
	"agent/defines/derrs"
	"bytes"
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"os"
	"strconv"
	"strings"

	"github.com/google/uuid"
)

func NewClient(methods ClientMethods, transport ITransport, config *AppConfig) (IClient, error) {
	cl := &client{
		methods:   methods,
		transport: transport,
		config:    config,
		nextLogin: 0,
		lastLogin: utime.Now(),
	}
	return cl, cl.loadPubKey(false)
}

type maskInfo struct {
	mask    []byte
	created int64
}

func (mi *maskInfo) Reset() bool {
	if len(mi.mask) == 0 {
		return true
	}
	if utime.NanoNow()-mi.created > int64(15*utime.Second) {
		mi.mask = nil
		mi.created = 0
		return true
	}
	return false
}

type client struct {
	config *AppConfig

	transport ITransport
	methods   ClientMethods

	nextLogin utime.Duration
	lastLogin utime.Time
	lastPing  utime.Time
	throttle  utime.Time
	srvTime   int64
	maskKey   maskInfo
}

func (c *client) Methods() ClientMethods {
	return c.methods
}

func (c *client) Config() *AppConfig {
	return c.config
}

func (c *client) Transport() ITransport {
	return c.transport
}

func (c *client) Schema() string {
	return c.transport.Schema()
}

func (c *client) Host() string {
	return c.transport.Host()
}

func (c *client) Port() int {
	return c.transport.Port()
}

func (c *client) Token() string {
	return c.transport.Token()
}

func (c *client) Reset() {
	c.maskKey.Reset()
	c.transport.Reset()
}

func (c *client) IsAuthorized() bool {
	return c.transport.IsAuthorized()
}

func (c *client) CanLogin() bool {
	return utime.Now().After(c.lastLogin.Add(c.NextLogin()))
}

func (c *client) Interval() ClientItenterval {
	return c.config.Client.Interval
}

func (c *client) SetInterval(interval ClientItenterval) {
	c.config.Client.Interval = interval
}

func (c *client) SetMaskKey(maskKey []byte) {
	c.maskKey = maskInfo{
		mask:    maskKey,
		created: utime.NanoNow(),
	}
	if len(maskKey) > 0 {
		vars.MaskKeySize = len(maskKey)
	}
}

func (c *client) NextLogin() utime.Duration {
	return c.nextLogin
}

func (c *client) SetNextLogin(interval utime.Duration) {
	c.nextLogin = interval
}

func (c *client) SetThrottle(to utime.Time) {
	c.throttle = to
}

func (c *client) LastLogin() utime.Time {
	return c.lastLogin
}

func (c *client) LastPing() utime.Time {
	return c.lastPing
}

func (c *client) NewStartupInfo(ctx context.Context) map[string]any {
	return c.methods.NewStartupInfo(ctx)
}

func (c *client) Login(ctx context.Context, startupInfo ...map[string]any) (err error) {
	_startupInfo := map[string]any{}
	if len(startupInfo) > 0 {
		_startupInfo = startupInfo[0]
	} else {
		_startupInfo = c.methods.NewStartupInfo(ctx)
	}

	defer func() {
		c.lastLogin = utime.Now()
		c.lastPing = utime.Now()

		if err != nil {
			if c.nextLogin == 0 {
				// Если интервал не установлен, устанавливаем интервал по умолчанию
				c.nextLogin = utime.Duration(vars.DefaultLoginInterval) * utime.Second
			} else {
				// Во время каждой ошибки, увеличиваем его в 2 раза
				c.nextLogin = c.nextLogin + c.nextLogin
			}
			// Если дошли до максимального значения, устанавливаем интервал по умолчанию
			if c.nextLogin > utime.Duration(vars.MaxLoginInterval)*utime.Second {
				c.nextLogin = utime.Duration(vars.DefaultLoginInterval) * utime.Second
			}
			debug.Logf(
				"Next login: %v, Seconds: %d\n",
				utime.Now().Add(c.nextLogin).Format("2006/01/02 15:04:05"),
				c.nextLogin/utime.Second,
			)
		}
	}()

	if debug := &c.config.Debug; debug.Active {
		debugOpts := map[string]any{}
		if debug.AgentUuid != uuid.Nil {
			debugOpts["agent_uuid"] = debug.AgentUuid
		}
		if debug.DeviceUuid != uuid.Nil {
			debugOpts["device_uuid"] = debug.DeviceUuid
		}
		_startupInfo["debug"] = debugOpts
	}

	ctx, cancel := context.WithTimeout(ctx, vars.LoginTimeout)
	defer cancel()

	c.maskKey.Reset()

	res, err := c.methods.Login(ctx, _startupInfo)
	if err != nil {
		return err
	}

	c.transport.SetServerTime(res.SrvTime)
	c.transport.SetToken(res.Token.Value)
	c.SetMaskKey(res.MaskKey)

	if err = c.SetConfig(ctx, res.Config); err != nil {
		return err
	}

	c.nextLogin = utime.Duration(c.config.Client.Interval.Login) * utime.Second

	for _, mod := range modules.List() {
		if err := mod.OnLogin(ctx, res.Config); err != nil {
			debug.LogErrf(`Module "%s" OnLogin error: %v`, mod.Name(), err)
		}
	}

	return nil
}

func (c *client) DownloadConfig(ctx context.Context) (err error) {
	res, err := c.methods.DownloadConfig(ctx)
	if err != nil {
		return err
	}
	if len(res) == 0 {
		return nil
	}
	return c.SetConfig(ctx, res)
}

func (c *client) SetConfig(ctx context.Context, config map[string]any) (err error) {
	if len(config) == 0 {
		return nil
	}

	data, err := json.Marshal(config)
	if err != nil {
		return err
	}
	var rcfg AgentRemoteConfig
	if err = json.Unmarshal(data, &rcfg); err != nil {
		return err
	}

	c.config.Version = rcfg.Version

	reconect := false
	if len(rcfg.Client.Key) > 0 {
		c.config.Client.Key = rcfg.Client.Key
		reconect = true
	}
	if rcfg.Client.CommandRound > 0 {
		c.config.Client.CommandRound = rcfg.Client.CommandRound
	}
	if rcfg.Client.CommandPerRound > 0 {
		c.config.Client.CommandPerRound = rcfg.Client.CommandPerRound
	}
	interval := rcfg.Client.Interval
	if interval.Ping > 0 {
		c.config.Client.Interval.Ping = interval.Ping
	}
	if interval.Next > 0 {
		c.config.Client.Interval.Next = interval.Next
	}
	if interval.Login > 0 {
		c.config.Client.Interval.Login = interval.Login
	}
	if interval.ConsoleInput > 0 {
		c.config.Client.Interval.ConsoleInput = interval.ConsoleInput
	}
	server := rcfg.Server
	if len(server.Schema) > 0 && c.config.Server.Schema != server.Schema {
		c.config.Server.Schema = server.Schema
		reconect = true
	}
	if len(server.Host) > 0 && c.config.Server.Host != server.Host {
		c.config.Server.Host = server.Host
		reconect = true
	}
	if server.Port > 0 && c.config.Server.Port != server.Port {
		c.config.Server.Port = server.Port
		reconect = true
	}
	if len(server.Handshake.KeyData) > 0 && !bytes.Equal(c.config.Server.Handshake.KeyData, server.Handshake.KeyData) {
		oldKey := c.config.Server.Handshake.KeyData
		c.config.Server.Handshake.KeyData = server.Handshake.KeyData
		if err = c.loadPubKey(true); err != nil {
			c.config.Server.Handshake.KeyData = oldKey
			return err
		}
		reconect = true
	}
	if len(server.Handshake.Value) > 0 && !bytes.Equal(c.config.Server.Handshake.Value, server.Handshake.Value) {
		c.config.Server.Handshake.Value = server.Handshake.Value
		reconect = true
	}

	if reconect {
		c.transport.Reset()
		return nil
	}

	return nil
}

func (c *client) Ping(ctx context.Context) (err error) {
	defer func() {
		c.lastPing = utime.Now()
	}()
	if _, err := c.methods.Ping(ctx); err != nil {
		return err
	}
	return nil
}

func (c *client) Execute(ctx context.Context, eventIn events.IEvent) (eventOut events.IEvent, err error) {
	defer func() {
		// Если ивент помечен очищаемым, производим удаление привязанного к нему файла
		if !opts.IsCleanup(eventIn) {
			return
		}
		if data := eventIn.Data(); data != nil {
			if fileData, ok := data.(adata.IDataFromFile); ok {
				if utils.FileIsExist(fileData.Path()) {
					os.Remove(fileData.Path())
				}
			}
		}
	}()

	for {
		if utime.Until(c.throttle) > 0 {
			utime.Sleep(1 * utime.Second)
			continue
		}

		if waitSec := utils.NextWorkTime(&c.config.Schedule, utime.Now(), 365); waitSec > 0 {
			// Время не рабочее
			utime.Sleep(1 * utime.Second)
			continue
		} else if waitSec < 0 {
			// Если не найдено рабочего времени, выходим иначе клиент будет висеть в ожидании бесконечно
			os.Exit(0)
		}

		if !c.IsAuthorized() {
			// Возвращаем ошибку для Ping, пока соединение не будет авторизовано
			if events.IsPingEvent(eventIn) {
				return nil, derrs.NewConnectionLostError()
			}
			// Удерживаем любой ивент, кроме Login/CheckHealth, пока соединение не будет авторизовано
			if !events.IsLoginEvent(eventIn) && !events.IsCheckHealthEvent(eventIn) {
				utime.Sleep(1 * utime.Second)
				continue
			}
		}

		if eventOut, err = c.execute(ctx, eventIn); err != nil {
			// При ошибке соединения, можно повторить выполнение ивента
			if derrs.Contains(err, derrs.CodeConnectionLost) {
				if events.IsCheckHealthEvent(eventIn) {
					// Проблема с соединением, при выполнении CheckHealt возникла ошибка
					return nil, derrs.NewEventExecutionError()
				}
				// Следуется убедиться, что проблема действительно с соединением, а не вызвано выполнением ивента
				if !c.isServiceHealthy(ctx) {
					// Если возникла ошибка, соединение не валидно, считаем его разорванным
					c.Reset()
					// Отказываемся повторять Login/Ping
					if events.IsLoginEvent(eventIn) || events.IsPingEvent(eventIn) {
						return nil, err
					}
					// Удерживаем ивент, пока соединение не будет восстановлено
					if eopts.HoldOnError(eventIn) {
						continue
					}
				}
				if !events.IsLoginEvent(eventIn) && !events.IsPingEvent(eventIn) {
					debug.LogErrf(`Event error: %v`, err)
				}
				// Если соединение активно, проблема с ивентом
				return nil, derrs.Join(err, derrs.NewEventExecutionError())
			}
			return nil, err
		}
		if eventOut != nil {
			// Ошибка выполнения ивента, повторение не возможно
			if events.IsResultErrorEvent(eventOut) || events.IsErrEvent(eventOut) {
				if errData, ok := opts.ErrData(eventOut); ok {
					return nil, derrs.Join(errors.New(errData), derrs.NewEventExecutionError(errData))
				}
				return nil, derrs.NewEventExecutionError()
			}
		}
		break
	}

	return eventOut, nil
}

func (c *client) isServiceHealthy(ctx context.Context) bool {
	if !c.IsAuthorized() {
		return false
	}
	if err := c.methods.CheckServiceHealth(ctx); err != nil {
		return false
	}
	return c.IsAuthorized()
}

func (c *client) AuthData(opts ...AuthDataOpts) ([]byte, error) {
	pubKeyTag := []byte(nil)
	trashSize := 0
	minTrashSize := 8
	maxTrashSize := 64
	serverCfg := &c.config.Server
	authDataSize := serverCfg.Handshake.KeyObject.Size()

	if len(opts) > 0 {
		o := opts[0]
		if o.PubKeyTag {
			// Ключ будет добавлен перед зашифрованной публичным ключом AuthData
			pubKeyTag = c.pubKeyTag()
			// Нужно учеcть размер этих данных для правильного округления до блока base64
			o.ExtraSize += len(pubKeyTag)
		}
		// ExtraSize может содержат размер данных, которые будут добавлены к AuthData
		authDataSize += o.ExtraSize
		if o.MaxTrashSize > 0 {
			minTrashSize = o.MinTrashSize
		}
		if o.MaxTrashSize > 0 {
			maxTrashSize = o.MaxTrashSize
		} else {
			maxTrashSize = minTrashSize
		}
		if minTrashSize > maxTrashSize {
			minTrashSize = maxTrashSize
		}
	}
	if !(maxTrashSize == 0 && minTrashSize == 0) {
		if maxTrashSize == minTrashSize {
			trashSize = maxTrashSize
		} else {
			trashSize = utils.IntN(maxTrashSize-minTrashSize) + maxTrashSize
		}
	}
	authData, err := c.authData(opts...)
	if err != nil {
		return nil, err
	}

	// Всегда округляем данные до 3 байт (минимального блока для plaintext в base64). AuthData может оборачиваться
	// в base64 и совмещаться с другими данными, при не соблюдении требования с округлением, будут видны разрывы между
	// разными блоками данных объедененных в один бинарный поток. Требование обязательно и контролируется на стороне
	// сервера, при не соблюдении будет ошибка.
	//
	// Мусорные данные будут размещены после AuthData, которая будет зашифрована с применением публичного ключа
	// сервера, поэтому округление делаем с учётом размера публичного ключа.
	trashSize = utils.RoundTo(trashSize+authDataSize, 3) - authDataSize
	trashData := ibytes.Random(trashSize)

	// Настраиваем заголовок пакета
	data := []byte{byte(len(trashData)), byte(len(authData))}
	// Тело пакета
	data = append(data, authData...)
	data, err = crypto.EncryptWithPubKey(data, serverCfg.Handshake.KeyObject)
	if err != nil {
		return nil, err
	}
	// Мусрные данные добавляем в конец зашифрованнрого пакета
	data = append(data, trashData...)
	// Если необходимо, перед authData добавляется тег публичного ключа
	if len(pubKeyTag) > 0 {
		data = append(pubKeyTag, data...)
	}
	return data, nil
}

func (c *client) pubKeyTag() []byte {
	// Делаем копию для дальнейших манипуляций
	pubKeyValue := append([]byte(nil), c.config.Server.Handshake.Value...)

	// Генерируем рандомный ключ для маскировки реального значения pubKeyValue
	var maskKey []byte
	if len(c.maskKey.mask) > 0 {
		maskKey = ibytes.Random(len(c.maskKey.mask))
	} else {
		maskKey = ibytes.Random(vars.MaskKeySize)
	}

	// Маскируем реальное значение pubKeyValue
	for i := 0; i < len(pubKeyValue); i++ {
		pubKeyValue[i] ^= maskKey[i%len(maskKey)]
	}
	// Маскируем использованный клиентский MaskKey, если у нас есть MaskKey от сервера
	for i := 0; i < len(c.maskKey.mask); i++ {
		maskKey[i] ^= c.maskKey.mask[i]
	}

	// Собираем тег, предваряя его MaskKey
	tag := make([]byte, len(maskKey)+len(pubKeyValue))
	copy(tag, maskKey)
	copy(tag[len(maskKey):], pubKeyValue)

	return tag
}

func (c *client) authData(opts ...AuthDataOpts) ([]byte, error) {
	clientCfg := &c.config.Client
	version := clientCfg.Version
	schema := clientCfg.Schema
	key := clientCfg.Key
	typ := clientCfg.Type

	if len(typ) == 0 {
		typ = "agent"
	}

	if len(version) == 0 || len(schema) == 0 || len(key) == 0 {
		return nil, derrs.NewIncorrectParamsError()
	}

	cipherName := clientCfg.Cipher.Name
	cipherKey := hex.EncodeToString(clientCfg.Cipher.Key)
	rawToken := ""
	clientType := typ
	expired := "0"

	if len(opts) > 0 {
		o := opts[0]
		if o.Expired > 0 {
			expired = strconv.FormatInt(o.Expired, 10)
		}
		if o.TokenValue != "" {
			rawToken = o.TokenValue
		}
	}

	authData := strings.Join([]string{version, schema, key, cipherName, cipherKey, rawToken, clientType, expired}, ",")
	//debug.Logfln("AuthDataSize:", len([]byte(authData)))

	return []byte(authData), nil
}

func (c *client) loadPubKey(force bool) (err error) {
	if serverCfg := &c.config.Server; force || serverCfg.Handshake.KeyObject == nil {
		pubKey, err := crypto.LoadPubKey(serverCfg.Handshake.KeyData, serverCfg.Handshake.KeyFile)
		if err != nil {
			return err
		}
		serverCfg.Handshake.KeyObject = pubKey
		return nil
	}
	return derrs.NewIncorrectParamsError()
}

func (c *client) execute(ctx context.Context,
	eventIn events.IEvent,
) (eventOut events.IEvent, err error) {
	if eventOut, err = c.transport.Execute(ctx, eventIn); err != nil {
		// Внутренняя ошибка клиента
		return nil, err
	}
	if eventOut == nil {
		return nil, nil
	}
	// Сервис прервал соединение
	if events.IsThrottleEvent(eventOut) || events.IsReloginEvent(eventOut) {
		if events.IsThrottleEvent(eventOut) {
			interval, err := events.ThrottleInterval(eventOut)
			if err != nil {
				debug.Logf(`Error while getting throttle interval: %v`, err)
			}
			// Сервер просит сделать паузу перед следующей отправкой ивентов
			c.SetThrottle(utime.Now().Add(utime.Second * utime.Duration(interval+1)))
		}
		if events.IsReloginEvent(eventOut) {
			// Сервер просит переподключиться (например, истёк токен)
			c.Reset()
		}
		return nil, derrs.NewConnectionLostError()
	}
	if respOpts := opts.Response(eventIn); respOpts != nil && !respOpts.Exist {
		// Ошибка выполнения ивента
		if events.IsResultErrorEvent(eventOut) || events.IsErrEvent(eventOut) {
			return eventOut, nil
		}
		// Ивент успешно выполнился, результат не требуется
		return nil, nil
	}

	return eventOut, nil
}
