package http

import (
	"agent/client"
	"agent/client/events"
	"agent/client/events/opts"
	ibase64 "agent/commons/base64"
	"agent/commons/crypto"
	"agent/commons/debug"
	"agent/commons/response"
	"agent/commons/utime"
	"agent/defines/derrs"
	"agent/defines/dhandlers"
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	ebase64 "encoding/base64"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync"
	"time"
)

type ITransport = client.ITransport

func NewTransport(cfg *client.AppConfig) ITransport {
	if cfg.Server.Schema != "http" && cfg.Server.Schema != "https" {
		return nil
	}
	tr := http.DefaultTransport.(*http.Transport)
	tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	httpClient := &http.Client{Transport: tr}

	return &httpTransport{
		lock: &sync.Mutex{},

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

		httpClient: httpClient,
	}
}

type httpTransport struct {
	lock *sync.Mutex

	init  bool
	debug bool

	schema string
	host   string
	port   int

	httpClient *http.Client

	tokenValue string
}

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

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

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

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

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

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

func (t *httpTransport) 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 *httpTransport) Init(ctx context.Context) {
	t.lock.Lock()
	defer t.lock.Unlock()
	if t.init {
		return
	}
}

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

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

func (t *httpTransport) 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.lock.Lock()
		defer t.lock.Unlock()
	}

	var closer io.Closer
	var req *http.Request

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

	switch reqOpts.Handler {
	case dhandlers.Json:
		req, closer, err = t.prepareJsonRequest(ctx, eventIn)
	case dhandlers.BinaryJson:
		req, closer, err = t.prepareBinaryJson(ctx, eventIn)
	case dhandlers.OctetStream:
		req, closer, err = t.prepareOctetStream(ctx, eventIn)
	default:
		err = derrs.NewNotImplementedError(fmt.Sprintf(`unknown handler type: "%v"`, reqOpts.Handler))
	}
	if err != nil {
		return nil, err
	}

	defer func() {
		if closer != nil {
			closer.Close()
		}
	}()

	if err = t.prepareHeaders(req, reqOpts); err != nil {
		return nil, err
	}
	if err = t.prepareCookies(req, reqOpts); err != nil {
		return nil, err
	}

	resp, err := t.httpClient.Do(req)
	if err != nil {
		return nil, derrs.Join(derrs.NewConnectionLostError(), err)
	}

	defer resp.Body.Close()

	return t.extractEvent(ctx, resp, respOpts)
}

func (t *httpTransport) prepareHeaders(req *http.Request, reqOpts *events.EventRequest) (err error) {
	if len(reqOpts.Headers) > 0 {
		for key, val := range reqOpts.Headers {
			req.Header.Add(key, val)
		}
	}
	return nil
}

func (t *httpTransport) prepareCookies(req *http.Request, reqOpts *events.EventRequest) (err error) {
	if len(reqOpts.Cookies) > 0 {
		for key, val := range reqOpts.Cookies {
			req.AddCookie(&http.Cookie{Name: key, Value: val})
		}
	}
	return nil
}

func (t *httpTransport) extractEvent(ctx context.Context,
	resp *http.Response, opts *events.EventResponse,
) (event events.IEvent, err error) {
	defer func() {
		if err != nil {
			err = derrs.Join(
				err,
				// При возникновении любой ошибки, считаем что соединение потеряно
				derrs.NewConnectionLostError(),
			)
		}
	}()

	if t.debug {
		t.printHeader(resp.Header)
	}

	reader := io.Reader(resp.Body)
	if opts != nil && !opts.Exist {
		if resp.StatusCode == http.StatusOK {
			// Проверяем, вернулся ли ответ
			br := bufio.NewReader(reader)
			if _, err := br.Peek(16); err != nil {
				// Если нет ответа, выходим с успехом
				return nil, nil
			}
			// Если ответ есть, то сервер вернул ошибку виде ивента, т.к. мы не ожидали ответа
			reader = br
		} else {
			// Любой другой статус, кроме 200, это ошибка соединения
			return nil, derrs.NewReadError()
		}
	}

	if opts.Base64 {
		reader = ebase64.NewDecoder(ebase64.StdEncoding, reader)
	}
	var cipher crypto.ICipher
	if opts.Encrypt {
		cl := client.ExtractClient(ctx)
		if cipher, err = crypto.NewCipher(cl); err != nil {
			return nil, err
		}
		reader = crypto.NewDecoder(reader, cipher)
	}
	var readed int64
	if event, readed, err = events.EventFromStream(reader, false, opts); err != nil {
		return nil, derrs.Join(err, derrs.NewUnknownError(fmt.Sprintf("%v %v", resp.Status, err)))
	}
	return event, response.SkipPadding(reader, cipher, readed)
}

func (t *httpTransport) prepareRequest(ctx context.Context,
	event events.IEvent, contentType string, binary bool, base64 bool,
) (req *http.Request, closer io.Closer, err error) {
	stream, size, closer, err := events.EventToStream(event, binary)
	if err != nil {
		return nil, closer, err
	}
	req, err = t.newRequestWithContext(ctx, http.MethodPost, t.url(), stream, size, base64)
	if err != nil {
		return nil, closer, err
	}
	req.Header.Add("Content-Type", contentType)
	return req, closer, err
}

func (t *httpTransport) prependAuthData(ctx context.Context,
	stream io.Reader, base64 bool,
) (io.Reader, int64, error) {
	authOpts := client.AuthDataOpts{
		PubKeyTag:     true,
		TokenValue:    t.tokenValue,
		RoundToBase64: base64,
	}
	cl := client.ExtractClient(ctx)
	authData, err := cl.AuthData(authOpts)
	if err != nil {
		return nil, 0, err
	}

	authStream := io.Reader(bytes.NewReader(authData))
	size := int64(len(authData))

	if base64 {
		authStream = ibase64.NewEncoder(ebase64.StdEncoding, authStream)
		if size > 0 {
			size = int64(ebase64.StdEncoding.EncodedLen(int(size)))
		}
	}

	return io.MultiReader(authStream, stream), size, nil
}

// prepareJsonRequest отправляет ивент в json-виде. Этот обработчик имеет большой минус. Данные кодируются
// в base64 два раза (первый раз, при кодировании в json, второй раз после шифрования), что сильно раздувает их размер.
// Поэтому он не рекомендуется к использованию, когда event.data содержит данные больше 16 КБ.
func (t *httpTransport) prepareJsonRequest(ctx context.Context,
	event events.IEvent,
) (req *http.Request, closer io.Closer, err error) {
	return t.prepareRequest(ctx, event, "application/json", false, true)
}

// prepareBinaryJson отправляет ивент в виде массива байтов обернутого в base64. Рекомендуется использовать этот метод
// всегда, когда нужно отправить ивент с event.data до 1 МБ.
func (t *httpTransport) prepareBinaryJson(ctx context.Context,
	event events.IEvent,
) (req *http.Request, closer io.Closer, err error) {
	return t.prepareRequest(ctx, event, "application/json", true, true)
}

// prepareOctetStream отправляет ивент полностью в бинарном виде, без оборачивания в base64. Рекомендуется использовать
// для передачи массивных файлов больше 1 МБ. Отличается от prepareFormDataRequest тем, что шлёт ивент прямым потоком
// байтов, в то время prepareFormDataRequest шлёт ивент в одном из полей формы.
func (t *httpTransport) prepareOctetStream(ctx context.Context,
	event events.IEvent,
) (req *http.Request, closer io.Closer, err error) {
	return t.prepareRequest(ctx, event, "application/octet-stream", true, false)
}

func (t *httpTransport) newRequestWithContext(
	ctx context.Context, method string, url string, body io.Reader, size int64, base64 bool,
) (req *http.Request, err error) {
	// Размер должен присуствовать, иначе возможны проблемы с кодированием данных.
	if size <= 0 {
		return nil, derrs.NewIncorrectParamsError("size <= 0")
	}

	cl := client.ExtractClient(ctx)
	cipher, err := crypto.NewCipher(cl)
	if err != nil {
		return nil, err
	}

	body = crypto.NewEncoder(body, cipher)
	size = int64(cipher.EncryptOutputSize(int(size)))

	if base64 {
		body = ibase64.NewEncoder(ebase64.StdEncoding, body)
		if size > 0 {
			size = int64(ebase64.StdEncoding.EncodedLen(int(size)))
		}
	}

	body, authSize, err := t.prependAuthData(ctx, body, base64)
	if err != nil {
		return nil, err
	}
	size += authSize

	if req, err = http.NewRequestWithContext(ctx, method, url, body); err != nil {
		return nil, err
	}
	req.ContentLength = size

	debug.Log("ContentLength:", req.ContentLength)

	return req, nil
}

func (t *httpTransport) printHeader(header http.Header) {
	for key, items := range header {
		for _, val := range items {
			// debug.Logf в релизной версии может быть удалено обфускатором, чтобы после этого не было ошибок с
			// неиспользуемыми переменными key и val, присваиваем их пустым операциям.
			_ = key
			_ = val
			debug.Logf("%s: %s\n", key, val)
		}
	}
}

func (t *httpTransport) url() string {
	return fmt.Sprintf("%s://%s:%d", t.schema, t.host, t.port)
}
