package rest

import (
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/gomodule/redigo/redis"
	"github.com/sasha-s/go-deadlock"
	uuid "github.com/satori/go.uuid"

	"quantex.com/qfixpt/src/app"
	"quantex.com/qfixpt/src/app/version"
	"quantex.com/qfixpt/src/client/store"
	"quantex.com/qfixpt/src/common/tracerr"
	"quantex.com/qfixpt/src/domain"
)

const (
	ProdEnv         = "prod"
	TokenExpireTime = 1200 * time.Second
)

const (
	responseKey     = "responseKey"
	sessionTokenKey = "sessionTokenKey"
)

type Controller struct {
	pool          *redis.Pool
	userData      app.UserDataProvider
	store         *store.Store
	tradeProvider TradeProvider
	config        Config
	notify        domain.Notifier
	authMutex     deadlock.Mutex
}

func newController(pool *redis.Pool, userData app.UserDataProvider,
	s *store.Store, tp TradeProvider, config Config, n domain.Notifier,
) *Controller {
	return &Controller{
		pool:          pool,
		userData:      userData,
		store:         s,
		tradeProvider: tp,
		config:        config,
		notify:        n,
	}
}

func (cont *Controller) TradeCaptureReports(ctx *gin.Context) {
	setHeaders(ctx, cont.config)
	trades := cont.tradeProvider.TradeCaptureReports()
	ctx.JSON(http.StatusOK, trades)
}

func (cont *Controller) GetAllTrades(ctx *gin.Context) {
	setHeaders(ctx, cont.config)
	trades := cont.tradeProvider.GetAllTrades()
	ctx.JSON(http.StatusOK, trades)
}

func (cont *Controller) GetTradeLogs(ctx *gin.Context) {
	setHeaders(ctx, cont.config)

	tradeID := ctx.Param("tradeID")

	logs, err := cont.store.GetLogsByTradeID(tradeID)
	if err != nil {
		slog.Error("error fetching trade logs", "tradeID", tradeID, "error", err)
		ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "error fetching logs"})

		return
	}

	ctx.JSON(http.StatusOK, logs)
}

func (cont *Controller) GetUser(ctx *gin.Context) app.User {
	// This is set on the AuthRequired middleware
	response, ok := ctx.Get(responseKey)
	if !ok {
		// TODO log this issue
		ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "internal server error"})
	}

	val, ok := response.([]uint8)
	if !ok {
		// TODO log this issue
		ctx.JSON(http.StatusInternalServerError, HTTPError{Error: "internal server error"})
	}

	return cont.userData.GetUserByEmail(string(val))
}

// Login godoc
// @Summary Login
// @Description Authenticate a User using credentials
// @Tags auth
// @Accept  json
// @Produce  json
// @Param credentials body Credentials true "Authentication"
// @Success 200 {object} Session
// @Failure 400 {object} HTTPError
// @Failure 401 {object} HTTPError
// @Failure 404 {object} HTTPError
// @Failure 500 {object} HTTPError
// @Router /auth/login [post]
func (cont *Controller) Login(ctx *gin.Context) {
	defer cont.authMutex.Unlock()
	cont.authMutex.Lock()

	setHeaders(ctx, cont.config)

	// Get the JSON body and decode into credentials
	var creds Credentials
	if err := ctx.ShouldBindJSON(&creds); err != nil {
		ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()})

		return
	}

	// Get the expected Password from our in memory map

	expectedUser := cont.userData.GetUserByEmail(creds.Email)

	// If a Password exists for the given User
	// AND, if it is the same as the Password we received, the we can move ahead
	// if NOT, then we return an "Unauthorized" status
	if expectedUser.Email == "" || expectedUser.Password != creds.Password {
		ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Invalid credentials"})

		return
	}

	// Create a new random session token
	sessionToken := uuid.NewV4().String()

	// Set the token in the cache, along with the User whom it represents
	// The token has an expiry time of 120 seconds
	conn := cont.pool.Get()
	defer func() {
		err := conn.Close()
		if err != nil {
			e := tracerr.Errorf("error closing connection: %w", err)
			slog.Error(e.Error())
		}
	}()

	_, err := conn.Do("SETEX", sessionToken, TokenExpireTime, creds.Email)
	if err != nil {
		slog.Error(tracerr.Errorf("Error setting token in redis cache: %w", err).Error())
		// If there is an error in setting the cache, return an internal server error
		ctx.JSON(http.StatusInternalServerError, HTTPError{Error: err.Error()})

		return
	}

	// Finally, we set the client cookie for "session_token" as the session token we just generated
	// we also set an expiry time of 120 seconds, the same as the cache

	// Finally, we set the client cookie for "session_token" as the session token we just generated
	// we also set an expiry time of TokenExpireTime, the same as the cache
	cookie := &http.Cookie{
		Name:     "session_token",
		Value:    sessionToken,
		HttpOnly: true,
		Path:     "/",
		Secure:   true,
		Expires:  time.Now().Add(TokenExpireTime),
	}

	if version.Environment() == version.EnvironmentTypeDev {
		cookie.SameSite = http.SameSiteNoneMode
	}

	http.SetCookie(ctx.Writer, cookie)

	ctx.JSON(http.StatusOK, Session{Email: creds.Email})
}

// Refresh godoc
// @Summary Refresh the authorization token
// @Description This endpoint must be called periodically to get a new token before the current one expires
// @Tags auth
// @Accept  json
// @Produce  json
// @Success 200 {object} Msg
// @Failure 400 {object} HTTPError
// @Failure 401 {object} HTTPError
// @Failure 404 {object} HTTPError
// @Failure 500 {object} HTTPError
// @Router /auth/refresh [get]
func (cont *Controller) Refresh(ctx *gin.Context) {
	defer cont.authMutex.Unlock()
	cont.authMutex.Lock()

	setHeaders(ctx, cont.config)

	// (BEGIN) The code until this point is the same as the first part of the `Welcome` route
	response, ok1 := ctx.Get(responseKey)
	if !ok1 {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
	}
	// (END) The code uptil this point is the same as the first part of the `Welcome` route

	// Now, create a new session token for the current User
	newSessionToken := uuid.NewV4().String()

	conn := cont.pool.Get()
	defer func() {
		err := conn.Close()
		if err != nil {
			e := tracerr.Errorf("error closing connection: %w", err)
			slog.Error(e.Error())
		}
	}()

	_, err := conn.Do("SETEX", newSessionToken, "120", fmt.Sprintf("%s", response))
	if err != nil {
		ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

		return
	}

	// Set the new token as the Users `session_token` cookie
	cookie := &http.Cookie{
		Name:     "session_token",
		Value:    newSessionToken,
		HttpOnly: true,
		Path:     "/",
		Secure:   true,
		Expires:  time.Now().Add(TokenExpireTime),
	}

	if version.Environment() == version.EnvironmentTypeDev {
		cookie.SameSite = http.SameSiteNoneMode
	}

	http.SetCookie(ctx.Writer, cookie)

	ctx.JSON(http.StatusOK, Msg{Text: "Token updated"})
}

// HealthCheck godoc
// @Summary Health check
// @Description Return service health
// @Tags health
// @Produce  json
// @Success 200 {object} map[string]string
// @Router /health [get]
func (cont *Controller) HealthCheck(ctx *gin.Context) {
	// ensure CORS and other headers are set consistently
	setHeaders(ctx, cont.config)

	status := struct {
		Status             string `json:"status"`
		Build              string `json:"build"`
		Sha                string `json:"sha"`
		JwtAuthentications string `json:"jwtAuthentications,omitempty"`
	}{
		Status: "ok",
		Build:  version.BuildBranch(),
		Sha:    version.BuildHash(),
	}

	// Only check JWT authentication if enabled
	if cont.config.EnableJWTAuth {
		status.JwtAuthentications = "ok"

		user, err := cont.store.UserByEmail("fede")
		if err != nil || user == nil {

			status.JwtAuthentications = "error"
			status.Status = "degraded"

			err = tracerr.Errorf("error fetching user: %w", err)

			slog.Error(err.Error())

			ctx.JSON(http.StatusInternalServerError, status)

			return
		}
	}

	// return a minimal JSON health response
	ctx.JSON(http.StatusOK, status)
}

// revive:disable:cyclomatic // We need this complexity
func setHeaders(ctx *gin.Context, config Config) {
	origin := ctx.Request.Header.Get("Origin")

	if allowed(origin, config) {
		ctx.Writer.Header().Set("Access-Control-Allow-Origin", origin)
		ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
		ctx.Writer.Header().Set("Access-Control-Allow-Headers",
			"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin,"+
				"UserCache-Control, X-Requested-With")
		ctx.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
	}
}

func allowed(origin string, config Config) bool {
	if version.Environment() == version.EnvironmentTypeProd {
		return origin == "https://monitor.quantex.com.ar"
	}

	for _, o := range config.AllowedOrigins {
		if o == origin {
			return true
		}
	}

	return false
}
