package admin

import (
	"net/http"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"forge.cadoles.com/cadoles/bouncer/internal/store"
	"github.com/go-chi/chi/v5"
	"github.com/pkg/errors"
	"gitlab.com/wpetit/goweb/api"
)

type QueryProxyResponse struct {
	Proxies []*store.ProxyHeader `json:"proxies"`
}

func (s *Server) queryProxy(w http.ResponseWriter, r *http.Request) {
	options := []store.QueryProxyOptionFunc{}

	names, ok := getStringableSliceValues(w, r, "names", nil, validateProxyName)
	if !ok {
		return
	}

	if names != nil {
		options = append(options, store.WithProxyQueryNames(names...))
	}

	ctx := r.Context()

	proxies, err := s.proxyRepository.QueryProxy(
		ctx,
		options...,
	)
	if err != nil {
		logAndCaptureError(ctx, "could not list proxies", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)

		return
	}

	sort.Sort(store.ByProxyWeight(proxies))

	api.DataResponse(w, http.StatusOK, QueryProxyResponse{
		Proxies: proxies,
	})
}

func validateProxyName(v string) (store.ProxyName, error) {
	name, err := store.ValidateName(v)
	if err != nil {
		return "", errors.WithStack(err)
	}

	return store.ProxyName(name), nil
}

type GetProxyResponse struct {
	Proxy *store.Proxy `json:"proxy"`
}

func (s *Server) getProxy(w http.ResponseWriter, r *http.Request) {
	proxyName, ok := getProxyName(w, r)
	if !ok {
		return
	}

	ctx := r.Context()

	proxy, err := s.proxyRepository.GetProxy(ctx, proxyName)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)

			return
		}

		logAndCaptureError(ctx, "could not get proxy", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)

		return
	}

	api.DataResponse(w, http.StatusOK, GetProxyResponse{
		Proxy: proxy,
	})
}

type DeleteProxyResponse struct {
	ProxyName store.ProxyName `json:"proxyName"`
}

func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) {
	proxyName, ok := getProxyName(w, r)
	if !ok {
		return
	}

	ctx := r.Context()

	if err := s.deleteProxyAndLayers(ctx, proxyName); err != nil {
		if errors.Is(err, store.ErrNotFound) {
			api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)

			return
		}

		logAndCaptureError(ctx, "could not delete proxy", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)

		return
	}

	api.DataResponse(w, http.StatusOK, DeleteProxyResponse{
		ProxyName: proxyName,
	})
}

type CreateProxyRequest struct {
	Name string   `json:"name" validate:"required"`
	To   string   `json:"to"`
	From []string `json:"from" validate:"required"`
}

type CreateProxyResponse struct {
	Proxy *store.Proxy `json:"proxy"`
}

func (s *Server) createProxy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	createProxyReq := &CreateProxyRequest{}
	if ok := api.Bind(w, r, createProxyReq); !ok {
		return
	}

	name, err := store.ValidateName(createProxyReq.Name)
	if err != nil {
		logAndCaptureError(ctx, "could not parse 'name' parameter", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

		return
	}

	if _, err := url.Parse(createProxyReq.To); err != nil {
		logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

		return
	}

	proxy, err := s.proxyRepository.CreateProxy(ctx, store.ProxyName(name), createProxyReq.To, createProxyReq.From...)
	if err != nil {
		if errors.Is(err, store.ErrAlreadyExist) {
			api.ErrorResponse(w, http.StatusConflict, ErrCodeAlreadyExist, nil)

			return
		}

		logAndCaptureError(ctx, "could not create proxy", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)

		return
	}

	api.DataResponse(w, http.StatusOK, struct {
		Proxy *store.Proxy `json:"proxy"`
	}{
		Proxy: proxy,
	})
}

type UpdateProxyRequest struct {
	Enabled *bool    `json:"enabled"`
	Weight  *int     `json:"weight"`
	To      *string  `json:"to"`
	From    []string `json:"from"`
}

type UpdateProxyResponse struct {
	Proxy *store.Proxy `json:"proxy"`
}

func (s *Server) updateProxy(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	proxyName, ok := getProxyName(w, r)
	if !ok {
		return
	}

	updateProxyReq := &UpdateProxyRequest{}
	if ok := api.Bind(w, r, updateProxyReq); !ok {
		return
	}

	options := make([]store.UpdateProxyOptionFunc, 0)

	if updateProxyReq.Enabled != nil {
		options = append(options, store.WithProxyUpdateEnabled(*updateProxyReq.Enabled))
	}

	if updateProxyReq.To != nil {
		_, err := url.Parse(*updateProxyReq.To)
		if err != nil {
			logAndCaptureError(ctx, "could not parse 'to' parameter", errors.WithStack(err))
			api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

			return
		}

		options = append(options, store.WithProxyUpdateTo(*updateProxyReq.To))
	}

	if updateProxyReq.From != nil {
		options = append(options, store.WithProxyUpdateFrom(updateProxyReq.From...))
	}

	if updateProxyReq.Weight != nil {
		options = append(options, store.WithProxyUpdateWeight(*updateProxyReq.Weight))
	}

	proxy, err := s.proxyRepository.UpdateProxy(
		ctx, proxyName,
		options...,
	)
	if err != nil {
		if errors.Is(err, store.ErrNotFound) {
			api.ErrorResponse(w, http.StatusNotFound, api.ErrCodeNotFound, nil)

			return
		}

		logAndCaptureError(ctx, "could not update proxy", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusInternalServerError, api.ErrCodeUnknownError, nil)

		return
	}

	api.DataResponse(w, http.StatusOK, UpdateProxyResponse{Proxy: proxy})
}

func getProxyName(w http.ResponseWriter, r *http.Request) (store.ProxyName, bool) {
	rawProxyName := chi.URLParam(r, "proxyName")

	name, err := store.ValidateName(rawProxyName)
	if err != nil {
		logAndCaptureError(r.Context(), "could not parse proxy name", errors.WithStack(err))
		api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

		return "", false
	}

	return store.ProxyName(name), true
}

func getIntQueryParam(w http.ResponseWriter, r *http.Request, param string, defaultValue int64) (int64, bool) {
	rawValue := r.URL.Query().Get(param)
	if rawValue != "" {
		value, err := strconv.ParseInt(rawValue, 10, 64)
		if err != nil {
			logAndCaptureError(r.Context(), "could not parse int param", errors.WithStack(err))
			api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

			return 0, false
		}

		return value, true
	}

	return defaultValue, true
}

func getStringSliceValues(w http.ResponseWriter, r *http.Request, param string, defaultValue []string) ([]string, bool) {
	rawValue := r.URL.Query().Get(param)
	if rawValue != "" {
		values := strings.Split(rawValue, ",")

		return values, true
	}

	return defaultValue, true
}

func getStringableSliceValues[T ~string](w http.ResponseWriter, r *http.Request, param string, defaultValue []T, transform func(string) (T, error)) ([]T, bool) {
	rawValue := r.URL.Query().Get(param)

	if rawValue != "" {
		rawValues := strings.Split(rawValue, ",")
		values := make([]T, 0, len(rawValues))

		for _, rv := range rawValues {
			v, err := transform(rv)
			if err != nil {
				logAndCaptureError(r.Context(), "could not parse ids slice param", errors.WithStack(err))
				api.ErrorResponse(w, http.StatusBadRequest, api.ErrCodeMalformedRequest, nil)

				return nil, false
			}

			values = append(values, v)
		}

		return values, true
	}

	return defaultValue, true
}