package respond

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

	"github.com/niels/banking-app/backend/internal/domain"
)

type errorBody struct {
	Error apiError `json:"error"`
}

type apiError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

func JSON(w http.ResponseWriter, status int, payload any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if payload == nil {
		return
	}
	_ = json.NewEncoder(w).Encode(payload)
}

func Created(w http.ResponseWriter, payload any) {
	JSON(w, http.StatusCreated, payload)
}

func NoContent(w http.ResponseWriter) {
	w.WriteHeader(http.StatusNoContent)
}

func DecodeJSON(r *http.Request, dst any) error {
	decoder := json.NewDecoder(r.Body)
	decoder.DisallowUnknownFields()

	if err := decoder.Decode(dst); err != nil {
		return fmt.Errorf("%w: invalid JSON body", domain.ErrValidation)
	}

	if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
		return fmt.Errorf("%w: request body must contain one JSON object", domain.ErrValidation)
	}

	return nil
}

func Error(w http.ResponseWriter, err error) {
	status, code, message := classify(err)
	JSON(w, status, errorBody{Error: apiError{Code: code, Message: message}})
}

func Problem(w http.ResponseWriter, status int, code, message string) {
	JSON(w, status, errorBody{Error: apiError{Code: code, Message: message}})
}

func classify(err error) (int, string, string) {
	switch {
	case errors.Is(err, domain.ErrValidation):
		return http.StatusBadRequest, "validation_failed", err.Error()
	case errors.Is(err, domain.ErrInvalidCredentials):
		return http.StatusUnauthorized, "invalid_credentials", "invalid email or password"
	case errors.Is(err, domain.ErrUnauthorized):
		return http.StatusUnauthorized, "unauthorized", "authentication required"
	case errors.Is(err, domain.ErrForbidden):
		return http.StatusForbidden, "forbidden", "you do not have permission to perform this action"
	case errors.Is(err, domain.ErrStepUpRequired):
		return http.StatusForbidden, "step_up_required", "step-up authentication is required for this action"
	case errors.Is(err, domain.ErrNotFound):
		return http.StatusNotFound, "not_found", "resource not found"
	case errors.Is(err, domain.ErrConflict):
		return http.StatusConflict, "conflict", "resource already exists"
	case errors.Is(err, domain.ErrInsufficientFunds):
		return http.StatusUnprocessableEntity, "insufficient_funds", "insufficient available balance"
	case errors.Is(err, domain.ErrLimitExceeded):
		return http.StatusUnprocessableEntity, "payment_limit_exceeded", err.Error()
	case errors.Is(err, domain.ErrProviderUnavailable):
		return http.StatusServiceUnavailable, "provider_unavailable", err.Error()
	case errors.Is(err, domain.ErrRateLimited):
		return http.StatusTooManyRequests, "rate_limited", err.Error()
	default:
		return http.StatusInternalServerError, "internal_error", "internal server error"
	}
}
