// Package external defines all external services access
package external

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/rs/zerolog/log"
	"github.com/sasha-s/go-deadlock"

	"quantex.com/qfixdpl/src/app"
	"quantex.com/qfixdpl/src/app/version"
	jwttoken "quantex.com/qfixdpl/src/common/jwttoken"
	"quantex.com/qfixdpl/src/domain"
)

const (
	sTimeout = 10
	lTimeout = 15
)

type Config struct {
	QApixPort     string
	QApixHost     string
	External      map[string]app.ExtAuth
	QApixToken    string
	EnableJWTAuth bool
}

type cacheItem struct {
	Time time.Time
	Resp []byte
}

type Manager struct {
	config   Config
	notifier domain.Notifier
	cache    map[string]cacheItem
	mutex    deadlock.RWMutex
}

type RequestOptions struct {
	Method        string
	Path          string
	URL           string
	Token         string
	Secured       bool
	Body          interface{}
	Retries       int
	Timeout       time.Duration
	CacheDuration time.Duration
	Auth          *app.ExtAuth
	SessionToken  string
}

// NewManager create new Manager struct
func NewManager(n domain.Notifier, cfg Config) *Manager {
	return &Manager{
		config:   cfg,
		notifier: n,
		cache:    make(map[string]cacheItem),
	}
}

//revive:disable:argument-limit we need this arguments
func (s *Manager) sendRequestToExternal(opts RequestOptions) ([]byte, error) {
	host := fmt.Sprintf("http://localhost:%v", opts.Auth.Port)
	if opts.Auth.Host != "" {
		host = opts.Auth.Host
	}

	token := s.config.QApixToken

	secured := false

	if s.config.EnableJWTAuth && opts.Auth != nil {
		t, err := jwttoken.Encrypt(*opts.Auth)
		if err != nil {
			e := fmt.Errorf("error encrypting quantexService: %w", err)
			log.Error().Msg(e.Error())

			return nil, e
		}

		token = t

		secured = true
	}

	opts.Secured = secured
	opts.Token = token
	opts.URL = urlFrom(host, opts.Path)

	return s.sendRequestWithCache(opts)
}

func (s *Manager) sendRequestWithCache(opts RequestOptions) ([]byte, error) {
	sha := optionSha(opts)

	if opts.CacheDuration > 0 && len(sha) > 0 {
		s.mutex.RLock()
		t, ok := s.cache[sha]
		s.mutex.RUnlock()

		if ok && time.Since(t.Time) < opts.CacheDuration {
			return t.Resp, nil
		}
	}

	resp, err := s.sendRequestWithRetries(opts)
	if err == nil && len(sha) > 0 {
		s.mutex.Lock()
		s.cache[sha] = cacheItem{
			time.Now(),
			resp,
		}
		s.mutex.Unlock()
	}

	return resp, err
}

//revive:disable:flag-parameter we need this flag
//nolint:funlen //it's long but easy to read
func (s *Manager) sendRequestWithRetries(opts RequestOptions) ([]byte, error) {
	client := &http.Client{
		Timeout: opts.Timeout,
	}

	bodyBytes, err := json.Marshal(&opts.Body)
	if err != nil {
		e := fmt.Errorf("error encoding the body: %v, %w", opts.Body, err)
		slog.Error(e.Error())

		return nil, e
	}

	slog.Debug("sending request to: " + opts.URL)

	request, err := http.NewRequest(opts.Method, opts.URL, bytes.NewBuffer(bodyBytes)) //nolint:noctx //no ctx needed
	if err != nil {
		e := fmt.Errorf("error creating new %s request to: %s, %w", opts.Method, opts.URL, err)
		slog.Error(e.Error())

		return nil, e
	}

	authorization := "Bearer " + opts.Token
	if opts.Secured {
		authorization = opts.Token
	}

	request.Header.Set("Authorization", authorization)

	request.Header.Set("Content-Type", "application/json")

	if opts.SessionToken != "" {
		// Create a cookie
		cookie := &http.Cookie{
			Name:  "session_token",
			Value: opts.SessionToken,
			Path:  "/",
		}

		// Add the cookie to the request
		request.AddCookie(cookie)
	}

	var resp *http.Response
	for i := 0; i <= opts.Retries; i++ { //nolint:wsl // It's ok in this case
		interval := time.Duration(i) * time.Second
		slog.Debug(fmt.Sprintf("request to '%s' try #%v in %v", request.URL, i, interval))
		time.Sleep(interval)

		resp, err = client.Do(request)
		if err != nil {
			e := fmt.Errorf("error making request to %s. error: %w", request.URL, err)
			slog.Error(e.Error())

			// send notification if notifier is available
			s.sendNotification(domain.MessageChannelError, e.Error(), domain.MessageStatusWarning, nil)

			continue
		}

		break
	}

	if resp == nil {
		err = fmt.Errorf("error: '%s' response is nil", request.URL)
		slog.Error(err.Error())

		return nil, err
	}

	defer func() {
		if err := resp.Body.Close(); err != nil {
			slog.Error("error closing response body at sendRequestWithRetries: " + err.Error())
		}
	}()

	if resp.StatusCode != http.StatusOK {
		msg := fmt.Sprintf("response code from %s is not 200. StatusCode: %d.", opts.URL, resp.StatusCode)
		// send notification if notifier is available
		s.sendNotification(domain.MessageChannelError, msg, domain.MessageStatusWarning, nil)

		return nil, fmt.Errorf("error %s", msg)
	}

	bodyBytes, resErr := io.ReadAll(resp.Body)
	if resErr != nil {
		msg := fmt.Sprintf("error reading response from %s. error: %s", opts.URL, resErr.Error())
		// send notification if notifier is available
		s.sendNotification(domain.MessageChannelError, msg, domain.MessageStatusWarning, nil)

		return nil, fmt.Errorf("error %s", msg)
	}

	return bodyBytes, nil
}

//revive:enable

// sendNotification is a nil-safe wrapper around the notifier's SendMsg method.
// Some call sites run in contexts where Manager.notifier may be nil, so guard the call
// to avoid runtime panics.
func (s *Manager) sendNotification(channel domain.MessageChannel, message string, status domain.MessageStatus, wg *sync.WaitGroup) {
	if s == nil || s.notifier == nil {
		slog.Debug("notifier is nil, skipping SendMsg")

		return
	}

	s.notifier.SendMsg(channel, message, status, wg)
}

func optionSha(opts RequestOptions) string {
	key := fmt.Sprintf("%s-%s-%s", opts.Method, opts.URL, opts.Body)

	return shaString(key)
}

func shaString(s string) (sha string) {
	hash := sha256.New()
	if _, err := hash.Write([]byte(s)); err != nil {
		err = fmt.Errorf("error generating hash at generateShaString, error: %w", err)
		slog.Error(err.Error())

		return ""
	}

	return hex.EncodeToString(hash.Sum(nil))
}

func urlFrom(host, path string) string {
	from := strings.ToLower(version.AppName)

	url := fmt.Sprintf("%s%s?from=%s", host, path, from)
	if strings.Contains(path, "?") {
		url = fmt.Sprintf("%s%s&from=%s", host, path, from)
	}

	return url
}
