package processor

import (
	"context"
	"errors"
	"fmt"
	"net/http"

	"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
	resolvedispatcher "git.optclblast.xyz/draincloud/draincloud-core/internal/resolve_dispatcher"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

type GinProcessor struct {
	rp                *common.RequestPool
	authStorage       storage.AuthStorage
	resolveDispatcher *resolvedispatcher.ResolveDispatcher
}

func NewGinProcessor(
	authStorage storage.AuthStorage,
	resolveDispatcher *resolvedispatcher.ResolveDispatcher,
) *GinProcessor {
	return &GinProcessor{
		rp:                common.NewRequestPool(),
		authStorage:       authStorage,
		resolveDispatcher: resolveDispatcher,
	}
}

func (p *GinProcessor) Process(handler handler.Handler) gin.HandlerFunc {
	return func(ctx *gin.Context) {
		req := common.NewRequestFromHttp(p.rp, ctx.Request)
		ctx.Request = ctx.Request.WithContext(context.WithValue(ctx.Request.Context(), "__request_id", req.ID))

		// 1. Resolve the resolvers, collect all data required
		// 2. Try process oprional resolvers
		err := p.resolve(ctx, handler, req)
		if err != nil {
			p.writeError(ctx, err)
			return
		}

		// 3. Call preprocessing fn's, middlewares etc.
		if preprocessFn := handler.GetPreprocessFn(); preprocessFn != nil {
			if err = preprocessFn(ctx, req, wrapGin(ctx)); err != nil {
				p.writeError(ctx, err)
				return
			}
		}

		// 4. Call handler.ProcessFn
		if err = handler.GetProcessFn()(ctx, req, wrapGin(ctx)); err != nil {
			p.writeError(ctx, err)
			return
		}
	}
}

func (p *GinProcessor) resolve(ctx *gin.Context, h handler.Handler, req *common.Request) error {
	eg, c := errgroup.WithContext(ctx)
	for _, r := range h.GetRequiredResolveParams() {
		resolver, err := p.resolveDispatcher.GetResolver(r)
		if err != nil {
			return fmt.Errorf("failed to resolve '%s' param: no resolver provided: %w", r, err)
		}

		resolveValueName := r
		eg.Go(func() error {
			if resolveErr := resolver.Resolve(c, req, ctx); resolveErr != nil {
				return fmt.Errorf("failed to resolve '%s' value: %w", resolveValueName, resolveErr)
			}
			return nil
		})
	}

	if err := eg.Wait(); err != nil {
		return err
	}

	return nil
}

func (p *GinProcessor) writeError(ctx *gin.Context, err error) {
	logger.Error(ctx, "error process request", logger.Err(err))

	// TODO do a custom error handling for resolvers / handlers / processors etc

	switch {
	case errors.Is(err, errs.ErrorAccessDenied):
		ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
			Code:    http.StatusForbidden,
			Message: err.Error(),
		})
	case errors.Is(err, errs.ErrorSessionExpired):
		ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
			Code:    http.StatusForbidden,
			Message: err.Error(),
		})
	default:
		ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
			Code:    http.StatusInternalServerError,
			Message: "Internal Error",
		})
	}
}
