// Package api implements a common handler used by the api routes
package api

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"

	"github.com/FAU-CDI/wisski-distillery/internal/config"
	"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth"
	"github.com/FAU-CDI/wisski-distillery/internal/dis/component/auth/scopes"
	"github.com/rs/zerolog"
	"github.com/tkw1536/pkglib/httpx"
	"github.com/tkw1536/pkglib/lazy"
)

// Handler represents an API handler that returns a REST response.
// The response is automatically marshaled using T.
type Handler[T any] struct {
	Config *config.Config
	Auth   *auth.Auth // Handler to handle Auth

	Methods []string // HTTP methods to allow
	methods lazy.Lazy[map[string]struct{}]

	Scope      scopes.Scope
	ScopeParam func(*http.Request) string
	Handler    func(string, *http.Request) (T, error)
}

var apiNotEnabled = &Response{
	Status:  http.StatusNotImplemented,
	Message: "API is not implemented on this server",
}

var apiMethodNotAllowed = &Response{
	Status:  http.StatusMethodNotAllowed,
	Message: "method not allowed",
}

var apiInternalServerError = &Response{
	Status:  http.StatusInternalServerError,
	Message: "internal server error",
}

var apiBadRequest = &Response{
	Status:  http.StatusBadRequest,
	Message: "bad request",
}

var apiNotFound = &Response{
	Status:  http.StatusNotFound,
	Message: "not found",
}

var apiForbidden = &Response{
	Status:  http.StatusForbidden,
	Message: "forbidden",
}

// ServeHTTP servers an api call
func (handler *Handler[T]) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// check that the api is actually enabled!
	if !handler.Config.HTTP.API.Value {
		apiNotEnabled.ServeHTTP(w, r)
		return
	}

	// get the permitted methods
	methods := handler.methods.Get(func() map[string]struct{} {
		m := make(map[string]struct{}, len(handler.Methods)+1)
		for _, method := range handler.Methods {
			m[method] = struct{}{}
		}
		m["OPTIONS"] = struct{}{}
		return m
	})

	// check that the method is permitted
	if _, ok := methods[r.Method]; !ok {
		apiMethodNotAllowed.ServeHTTP(w, r)
		return
	}

	// we now delegate to user-level code;
	// so we now need to make sure that panic()s are caught.
	var stage string
	defer func() {
		// recover any error
		rec := recover()
		if rec == nil {
			return
		}

		// log the error, and serve the default internal server error
		zerolog.Ctx(r.Context()).Error().Str("panic", fmt.Sprint(rec)).Str("stage", stage).Str("route", r.URL.RequestURI()).Msg("api handler caused panic()")
		apiInternalServerError.ServeHTTP(w, r)
	}()

	// read the parameter
	stage = "param"
	var param string
	if handler.ScopeParam != nil {
		param = handler.ScopeParam(r)
	}

	// check that the scope is correct
	stage = "scope"
	if err := handler.Auth.CheckScope(param, handler.Scope, r); err != nil {
		(&Response{
			Status:  http.StatusForbidden,
			Message: err.Error(),
		}).ServeHTTP(w, r)
		return
	}

	stage = "handler"

	result, err := handler.Handler(param, r)
	switch true {
	case err == nil: /* keep going */

	// handle common httpx errors
	case errors.Is(err, httpx.ErrInternalServerError):
		apiInternalServerError.ServeHTTP(w, r)
		return
	case errors.Is(err, httpx.ErrBadRequest):
		apiBadRequest.ServeHTTP(w, r)
		return
	case errors.Is(err, httpx.ErrNotFound):
		apiNotFound.ServeHTTP(w, r)
		return
	case errors.Is(err, httpx.ErrForbidden):
		apiForbidden.ServeHTTP(w, r)
		return
	case errors.Is(err, httpx.ErrMethodNotAllowed):
		apiMethodNotAllowed.ServeHTTP(w, r)
		return

		// generic error
	default:
		(&Response{
			Status:  http.StatusInternalServerError,
			Message: err.Error(),
		}).ServeHTTP(w, r)
		return
	}

	stage = "marshal"

	// encode the result into json and send it as the response
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(result)
}

// Response represents a generic response to any request.
// Response objects cache response serialization
type Response struct {
	Status  int    `json:"status"`
	Message string `json:"message"`
	res     lazy.Lazy[httpx.Response]
}

func (g *Response) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	g.res.Get(func() httpx.Response {
		bytes, _ := json.Marshal(g)
		return httpx.Response{
			ContentType: "application/json",
			Body:        bytes,
			StatusCode:  g.Status,
		}
	}).ServeHTTP(w, r)
}
