package tcp

import (
	"agent/client"
	"agent/client/events"
	"agent/client/events/opts"
	"agent/commons/crypto"
	"agent/commons/debug"
	"agent/commons/response"
	"agent/commons/utime"
	"agent/commons/vars"
	"agent/defines/derrs"
	"context"
	"fmt"
	"io"
	"net"
	"strings"
	"sync"
	"time"
)

func NewTransport(cfg *client.AppConfig) client.ITransport {
	if cfg.Server.Schema != "tcp" {
		return nil
	}

	tr := &tcpTransport{
		mu: &sync.Mutex{},

		schema: cfg.Server.Schema,
		host:   cfg.Server.Host,
		port:   cfg.Server.Port,
	}

	pool, err := NewPool(Config{
		Size: 2,
		Factory: func(ctx context.Context) (*tcpConn, error) {
			return tr.connect(ctx)
		},
		MaxLifetime:    0,
		MaxIdle:        180 * time.Second,
		AcquireTimeout: 15 * time.Second,
	})

	if err != nil {
		debug.LogErrf("tcp.NewTransport: NewPool error: %v", err)
		return nil
	}

	tr.pool = pool

	return tr
}

type tcpTransport struct {
	mu *sync.Mutex

	init  bool
	debug bool

	schema string
	host   string
	port   int

	tokenValue string

	pool *Pool
}

func (t *tcpTransport) Name() string {
	return strings.ToUpper(t.schema)
}

func (t *tcpTransport) Schema() string {
	return t.schema
}

func (t *tcpTransport) Host() string {
	return t.host
}

func (t *tcpTransport) Port() int {
	return t.port
}

func (t *tcpTransport) IsAuthorized() bool {
	return len(t.tokenValue) > 0
}

func (t *tcpTransport) Reset() {
	t.tokenValue = ""
}

func (t *tcpTransport) Token() string {
	return t.tokenValue
}

func (t *tcpTransport) SetToken(token string) {
	t.tokenValue = token
	debug.Logf(`LoginSuccess(TokenValue: "%s")`, t.tokenValue)
}

func (t *tcpTransport) SetServerTime(srvTime int64) {
	debug.Logf(
		`SetServerTime(SrvTime: "%s", LocalTime: "%s")`,
		time.Unix(0, srvTime).UTC().Format("2006/01/02 15:04:05"), utime.Now().Format("2006/01/02 15:04:05"),
	)
	utime.SyncTime(srvTime)
}

func (t *tcpTransport) Init(ctx context.Context) {
	t.mu.Lock()
	defer t.mu.Unlock()
	if t.init {
		return
	}
}

func (t *tcpTransport) Execute(ctx context.Context,
	eventIn events.IEvent,
) (eventOut events.IEvent, err error) {
	// Содержит настройки для отправки ивента сервису
	reqOpts := opts.Request(eventIn)
	// Содержит настройки для получения ивента от сервиса
	respOpts := opts.Response(eventIn)

	if reqOpts == nil || respOpts == nil {
		return nil, derrs.NewIncorrectParamsError("reqOpts == nil || respOpts == nil")
	}

	if !reqOpts.Parallel {
		t.mu.Lock()
		defer t.mu.Unlock()
	}

	conn, err := t.pool.Acquire(ctx)
	if err != nil {
		return nil, derrs.Join(err, derrs.NewConnectionLostError())
	}

	defer func() {
		bad := err != nil || eventOut == nil
		_ = t.pool.Release(conn, bad)
	}()

	if err = t.sendEvent(ctx, conn, eventIn, reqOpts, respOpts); err != nil {
		return nil, err
	}

	eventOut, err = t.receiveEvent(ctx, conn, respOpts)
	if err != nil {
		return nil, err
	}

	return eventOut, nil
}

func (t *tcpTransport) sendEvent(ctx context.Context,
	conn *tcpConn,
	eventIn events.IEvent,
	reqOpts *events.EventRequest,
	respOpts *events.EventResponse,
) (err error) {
	// TCP транспорт всегда ожидает полный ивент в ответ
	respOpts.DataOnly = false

	stream, _, closer, err := events.EventToStream(eventIn, true)
	if err != nil {
		return err
	}
	if closer != nil {
		defer closer.Close()
	}
	stream = crypto.NewEncoder(stream, conn.streamCipher)
	if _, err = io.Copy(conn.raw, stream); err != nil {
		return derrs.Join(err, derrs.NewConnectionLostError())
	}
	return nil
}

func (t *tcpTransport) receiveEvent(ctx context.Context,
	conn *tcpConn,
	respOpts *events.EventResponse,
) (eventOut events.IEvent, err error) {
	// TCP транспорт всегда ожидает не пустой ответ
	opts := *respOpts
	opts.Exist = true
	opts.CanEmpty = false

	var readed int64
	if eventOut, readed, err = events.EventFromStream(conn.reader, true, &opts); err != nil {
		return nil, derrs.Join(err, derrs.NewConnectionLostError())
	}
	if err = response.SkipPadding(conn.reader, conn.streamCipher, readed); err != nil {
		return nil, derrs.Join(err, derrs.NewConnectionLostError())
	}
	return eventOut, nil
}

func (t *tcpTransport) addr() string {
	return fmt.Sprintf("%s:%d", t.host, t.port)
}

func (t *tcpTransport) connect(ctx context.Context) (conn *tcpConn, err error) {
	conn = &tcpConn{
		created: utime.NanoNow(),
	}

	defer func() {
		if err != nil && conn != nil && conn.raw != nil {
			conn.raw.Close()
		}
	}()

	d := net.Dialer{
		Timeout: vars.LoginTimeout,
	}

	if conn.raw, err = d.DialContext(ctx, "tcp", t.addr()); err != nil {
		return nil, err
	}

	opts := client.AuthDataOpts{
		PubKeyTag:     true,
		RoundToBase64: false,
		TokenValue:    t.Token(),
	}

	cl := client.ExtractClient(ctx)

	authData, err := cl.AuthData(opts)
	if err != nil {
		return conn, err
	}

	if _, err = conn.raw.Write(authData); err != nil {
		return conn, err
	}

	if conn.streamCipher, err = crypto.NewCipher(cl); err != nil {
		return conn, err
	}
	conn.reader = crypto.NewDecoder(conn.raw, conn.streamCipher)

	return conn, nil
}
