package apis

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

	"github.com/labstack/echo/v5"
	"github.com/pocketbase/pocketbase/core"
	"github.com/pocketbase/pocketbase/models"
	"github.com/pocketbase/pocketbase/tools/rest"
	"github.com/pocketbase/pocketbase/tools/routine"
	"github.com/pocketbase/pocketbase/tools/types"
	"github.com/spf13/cast"
)

// Common request context keys used by the middlewares and api handlers.
const (
	ContextUserKey       string = "user"
	ContextAdminKey      string = "admin"
	ContextCollectionKey string = "collection"
)

// RequireGuestOnly middleware requires a request to NOT have a valid
// Authorization header set.
//
// This middleware is the opposite of [apis.RequireAdminOrUserAuth()].
func RequireGuestOnly() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			err := rest.NewBadRequestError("The request can be accessed only by guests.", nil)

			user, _ := c.Get(ContextUserKey).(*models.User)
			if user != nil {
				return err
			}

			admin, _ := c.Get(ContextAdminKey).(*models.Admin)
			if admin != nil {
				return err
			}

			return next(c)
		}
	}
}

// RequireUserAuth middleware requires a request to have
// a valid user Authorization header set (aka. `Authorization: User ...`).
func RequireUserAuth() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			user, _ := c.Get(ContextUserKey).(*models.User)
			if user == nil {
				return rest.NewUnauthorizedError("The request requires valid user authorization token to be set.", nil)
			}

			return next(c)
		}
	}
}

// RequireAdminAuth middleware requires a request to have
// a valid admin Authorization header set (aka. `Authorization: Admin ...`).
func RequireAdminAuth() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			admin, _ := c.Get(ContextAdminKey).(*models.Admin)
			if admin == nil {
				return rest.NewUnauthorizedError("The request requires admin authorization token to be set.", nil)
			}

			return next(c)
		}
	}
}

// RequireAdminAuthOnlyIfAny middleware requires a request to have
// a valid admin Authorization header set (aka. `Authorization: Admin ...`)
// ONLY if the application has at least 1 existing Admin model.
func RequireAdminAuthOnlyIfAny(app core.App) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			totalAdmins, err := app.Dao().TotalAdmins()
			if err != nil {
				return rest.NewBadRequestError("Failed to fetch admins info.", err)
			}

			admin, _ := c.Get(ContextAdminKey).(*models.Admin)

			if admin != nil || totalAdmins == 0 {
				return next(c)
			}

			return rest.NewUnauthorizedError("The request requires admin authorization token to be set.", nil)
		}
	}
}

// RequireAdminOrUserAuth middleware requires a request to have
// a valid admin or user Authorization header set
// (aka. `Authorization: Admin ...` or `Authorization: User ...`).
//
// This middleware is the opposite of [apis.RequireGuestOnly()].
func RequireAdminOrUserAuth() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			admin, _ := c.Get(ContextAdminKey).(*models.Admin)
			user, _ := c.Get(ContextUserKey).(*models.User)

			if admin == nil && user == nil {
				return rest.NewUnauthorizedError("The request requires admin or user authorization token to be set.", nil)
			}

			return next(c)
		}
	}
}

// RequireAdminOrOwnerAuth middleware requires a request to have
// a valid admin or user owner Authorization header set
// (aka. `Authorization: Admin ...` or `Authorization: User ...`).
//
// This middleware is similar to [apis.RequireAdminOrUserAuth()] but
// for the user token expects to have the same id as the path parameter
// `ownerIdParam` (default to "id").
func RequireAdminOrOwnerAuth(ownerIdParam string) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if ownerIdParam == "" {
				ownerIdParam = "id"
			}

			ownerId := c.PathParam(ownerIdParam)
			admin, _ := c.Get(ContextAdminKey).(*models.Admin)
			loggedUser, _ := c.Get(ContextUserKey).(*models.User)

			if admin == nil && loggedUser == nil {
				return rest.NewUnauthorizedError("The request requires admin or user authorization token to be set.", nil)
			}

			if admin == nil && loggedUser.Id != ownerId {
				return rest.NewForbiddenError("You are not allowed to perform this request.", nil)
			}

			return next(c)
		}
	}
}

// LoadAuthContext middleware reads the Authorization request header
// and loads the token related user or admin instance into the
// request's context.
//
// This middleware is expected to be registered by default for all routes.
func LoadAuthContext(app core.App) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			token := c.Request().Header.Get("Authorization")

			if token != "" {
				if strings.HasPrefix(token, "User ") {
					user, err := app.Dao().FindUserByToken(
						token[5:],
						app.Settings().UserAuthToken.Secret,
					)
					if err == nil && user != nil {
						c.Set(ContextUserKey, user)
					}
				} else if strings.HasPrefix(token, "Admin ") {
					admin, err := app.Dao().FindAdminByToken(
						token[6:],
						app.Settings().AdminAuthToken.Secret,
					)
					if err == nil && admin != nil {
						c.Set(ContextAdminKey, admin)
					}
				}
			}

			return next(c)
		}
	}
}

// LoadCollectionContext middleware finds the collection with related
// path identifier and loads it into the request context.
func LoadCollectionContext(app core.App) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if param := c.PathParam("collection"); param != "" {
				collection, err := app.Dao().FindCollectionByNameOrId(param)
				if err != nil || collection == nil {
					return rest.NewNotFoundError("", err)
				}

				c.Set(ContextCollectionKey, collection)
			}

			return next(c)
		}
	}
}

// ActivityLogger middleware takes care to save the request information
// into the logs database.
//
// The middleware does nothing if the app logs retention period is zero
// (aka. app.Settings().Logs.MaxDays = 0).
func ActivityLogger(app core.App) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			err := next(c)

			// no logs retention
			if app.Settings().Logs.MaxDays == 0 {
				return err
			}

			httpRequest := c.Request()
			httpResponse := c.Response()
			status := httpResponse.Status
			meta := types.JsonMap{}

			if err != nil {
				switch v := err.(type) {
				case *echo.HTTPError:
					status = v.Code
					meta["errorMessage"] = v.Message
					meta["errorDetails"] = fmt.Sprint(v.Internal)
				case *rest.ApiError:
					status = v.Code
					meta["errorMessage"] = v.Message
					meta["errorDetails"] = fmt.Sprint(v.RawData())
				default:
					status = http.StatusBadRequest
					meta["errorMessage"] = v.Error()
				}
			}

			requestAuth := models.RequestAuthGuest
			if c.Get(ContextUserKey) != nil {
				requestAuth = models.RequestAuthUser
			} else if c.Get(ContextAdminKey) != nil {
				requestAuth = models.RequestAuthAdmin
			}

			model := &models.Request{
				Url:       httpRequest.URL.RequestURI(),
				Method:    strings.ToLower(httpRequest.Method),
				Status:    status,
				Auth:      requestAuth,
				Ip:        httpRequest.RemoteAddr,
				Referer:   httpRequest.Referer(),
				UserAgent: httpRequest.UserAgent(),
				Meta:      meta,
			}
			// set timestamp fields before firing a new go routine
			model.RefreshCreated()
			model.RefreshUpdated()

			routine.FireAndForget(func() {
				attempts := 1

			BeginSave:
				logErr := app.LogsDao().SaveRequest(model)
				if logErr != nil {
					// try one more time after 10s in case of SQLITE_BUSY or "database is locked" error
					if attempts <= 2 {
						attempts++
						time.Sleep(10 * time.Second)
						goto BeginSave
					} else if app.IsDebug() {
						log.Println("Log save failed:", logErr)
					}
				}

				// Delete old request logs
				// ---
				now := time.Now()
				lastLogsDeletedAt := cast.ToTime(app.Cache().Get("lastLogsDeletedAt"))
				daysDiff := now.Sub(lastLogsDeletedAt).Hours() * 24

				if daysDiff > float64(app.Settings().Logs.MaxDays) {
					deleteErr := app.LogsDao().DeleteOldRequests(now.AddDate(0, 0, -1*app.Settings().Logs.MaxDays))
					if deleteErr == nil {
						app.Cache().Set("lastLogsDeletedAt", now)
					} else if app.IsDebug() {
						log.Println("Logs delete failed:", deleteErr)
					}
				}
			})

			return err
		}
	}
}
