package server

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

	"codeberg.org/pronounscc/pronouns.cc/backend/log"
	"emperror.dev/errors"
	"github.com/getsentry/sentry-go"
	"github.com/go-chi/chi/v5"
	"github.com/go-chi/render"
)

// WrapHandler wraps a modified http.HandlerFunc into a stdlib-compatible one.
// The inner HandlerFunc additionally returns an error.
func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		hub := sentry.GetHubFromContext(r.Context())
		if hub == nil {
			hub = sentry.CurrentHub().Clone()
		}

		err := hn(w, r)
		if err != nil {
			// if the function returned an API error, just render that verbatim
			// we can assume that it also logged the error (if that was needed)
			if apiErr, ok := err.(APIError); ok {
				apiErr.prepare()

				render.Status(r, apiErr.Status)
				render.JSON(w, r, apiErr)
				return
			}

			rctx := chi.RouteContext(r.Context())
			hub.ConfigureScope(func(scope *sentry.Scope) {
				scope.SetTag("method", rctx.RouteMethod)
				scope.SetTag("path", rctx.RoutePattern())
			})

			var eventID *sentry.EventID = nil
			if isExpectedError(err) {
				log.Infof("expected error in handler for %v %v, ignoring", rctx.RouteMethod, rctx.RoutePattern())
			} else {
				log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err)
				eventID = hub.CaptureException(err)
			}
			apiErr := APIError{ID: eventID, Code: ErrInternalServerError}
			apiErr.prepare()

			render.Status(r, apiErr.Status)
			render.JSON(w, r, apiErr)
		}
	}
}

func isExpectedError(err error) bool {
	return errors.Is(err, context.Canceled)
}

// APIError is an object returned by the API when an error occurs.
// It implements the error interface and can be returned by handlers.
type APIError struct {
	Code    int             `json:"code"`
	ID      *sentry.EventID `json:"id,omitempty"`
	Message string          `json:"message,omitempty"`
	Details string          `json:"details,omitempty"`

	RatelimitReset *int `json:"ratelimit_reset,omitempty"`

	// Status is set as the HTTP status code.
	Status int `json:"-"`
}

func (e APIError) Error() string {
	if e.Message == "" {
		e.Message = errCodeMessages[e.Code]
	}

	if e.Details != "" {
		return fmt.Sprintf("%s (code: %d) (%s)", e.Message, e.Code, e.Details)
	}

	return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}

func (e *APIError) prepare() {
	if e.Status == 0 {
		e.Status = errCodeStatuses[e.Code]
	}

	if e.Message == "" {
		e.Message = errCodeMessages[e.Code]
	}
}

// Error code constants
const (
	ErrBadRequest          = 400
	ErrForbidden           = 403
	ErrNotFound            = 404
	ErrMethodNotAllowed    = 405
	ErrTooManyRequests     = 429
	ErrInternalServerError = 500 // catch-all code for unknown errors

	// Login/authorize error codes
	ErrInvalidState        = 1001
	ErrInvalidOAuthCode    = 1002
	ErrInvalidToken        = 1003 // a token was supplied, but it is invalid
	ErrInviteRequired      = 1004
	ErrInvalidTicket       = 1005 // invalid signup ticket
	ErrInvalidUsername     = 1006 // invalid username (when signing up)
	ErrUsernameTaken       = 1007 // username taken (when signing up)
	ErrInvitesDisabled     = 1008 // invites are disabled (unneeded)
	ErrInviteLimitReached  = 1009 // invite limit reached (when creating invites)
	ErrInviteAlreadyUsed   = 1010 // invite already used (when signing up)
	ErrDeletionPending     = 1011 // own user deletion pending, returned with undo code
	ErrRecentExport        = 1012 // latest export is too recent
	ErrUnsupportedInstance = 1013 // unsupported fediverse software
	ErrAlreadyLinked       = 1014 // user already has linked account of the same type
	ErrNotLinked           = 1015 // user already doesn't have a linked account
	ErrLastProvider        = 1016 // unlinking provider would leave account with no authentication method
	ErrInvalidCaptcha      = 1017 // invalid or missing captcha response

	// User-related error codes
	ErrUserNotFound        = 2001
	ErrMemberListPrivate   = 2002
	ErrFlagLimitReached    = 2003
	ErrRerollingTooQuickly = 2004

	// Member-related error codes
	ErrMemberNotFound     = 3001
	ErrMemberLimitReached = 3002
	ErrMemberNameInUse    = 3003
	ErrNotOwnMember       = 3004

	// General request error codes
	ErrRequestTooBig      = 4001
	ErrMissingPermissions = 4002

	// Moderation related error codes
	ErrReportAlreadyHandled = 5001
	ErrNotSelfDelete        = 5002
)

var errCodeMessages = map[int]string{
	ErrBadRequest:          "Bad request",
	ErrForbidden:           "Forbidden",
	ErrInternalServerError: "Internal server error",
	ErrNotFound:            "Not found",
	ErrTooManyRequests:     "Rate limit reached",
	ErrMethodNotAllowed:    "Method not allowed",

	ErrInvalidState:        "Invalid OAuth state",
	ErrInvalidOAuthCode:    "Invalid OAuth code",
	ErrInvalidToken:        "Supplied token was invalid",
	ErrInviteRequired:      "A valid invite code is required",
	ErrInvalidTicket:       "Invalid signup ticket",
	ErrInvalidUsername:     "Invalid username",
	ErrUsernameTaken:       "Username is already taken",
	ErrInvitesDisabled:     "Invites are disabled",
	ErrInviteLimitReached:  "Your account has reached the invite limit",
	ErrInviteAlreadyUsed:   "That invite code has already been used",
	ErrDeletionPending:     "Your account is pending deletion",
	ErrRecentExport:        "Your latest data export is less than 1 day old",
	ErrUnsupportedInstance: "Unsupported instance software",
	ErrAlreadyLinked:       "Your account is already linked to an account of this type",
	ErrNotLinked:           "Your account is already not linked to an account of this type",
	ErrLastProvider:        "This is your account's only authentication provider",
	ErrInvalidCaptcha:      "Invalid or missing captcha response",

	ErrUserNotFound:        "User not found",
	ErrMemberListPrivate:   "This user's member list is private",
	ErrFlagLimitReached:    "Maximum number of pride flags reached",
	ErrRerollingTooQuickly: "You can only reroll one short ID per hour.",

	ErrMemberNotFound:     "Member not found",
	ErrMemberLimitReached: "Member limit reached",
	ErrMemberNameInUse:    "Member name already in use",
	ErrNotOwnMember:       "Not your member",

	ErrRequestTooBig:      "Request too big (max 2 MB)",
	ErrMissingPermissions: "Your account or current token is missing required permissions for this action",

	ErrReportAlreadyHandled: "Report has already been resolved",
	ErrNotSelfDelete:        "Cannot cancel deletion for an account deleted by a moderator",
}

var errCodeStatuses = map[int]int{
	ErrBadRequest:          http.StatusBadRequest,
	ErrForbidden:           http.StatusForbidden,
	ErrInternalServerError: http.StatusInternalServerError,
	ErrNotFound:            http.StatusNotFound,
	ErrTooManyRequests:     http.StatusTooManyRequests,
	ErrMethodNotAllowed:    http.StatusMethodNotAllowed,

	ErrInvalidState:        http.StatusBadRequest,
	ErrInvalidOAuthCode:    http.StatusForbidden,
	ErrInvalidToken:        http.StatusUnauthorized,
	ErrInviteRequired:      http.StatusBadRequest,
	ErrInvalidTicket:       http.StatusBadRequest,
	ErrInvalidUsername:     http.StatusBadRequest,
	ErrUsernameTaken:       http.StatusBadRequest,
	ErrInvitesDisabled:     http.StatusForbidden,
	ErrInviteLimitReached:  http.StatusForbidden,
	ErrInviteAlreadyUsed:   http.StatusBadRequest,
	ErrDeletionPending:     http.StatusBadRequest,
	ErrRecentExport:        http.StatusBadRequest,
	ErrUnsupportedInstance: http.StatusBadRequest,
	ErrAlreadyLinked:       http.StatusBadRequest,
	ErrNotLinked:           http.StatusBadRequest,
	ErrLastProvider:        http.StatusBadRequest,
	ErrInvalidCaptcha:      http.StatusBadRequest,

	ErrUserNotFound:        http.StatusNotFound,
	ErrMemberListPrivate:   http.StatusForbidden,
	ErrFlagLimitReached:    http.StatusBadRequest,
	ErrRerollingTooQuickly: http.StatusForbidden,

	ErrMemberNotFound:     http.StatusNotFound,
	ErrMemberLimitReached: http.StatusBadRequest,
	ErrMemberNameInUse:    http.StatusBadRequest,
	ErrNotOwnMember:       http.StatusForbidden,

	ErrRequestTooBig:      http.StatusBadRequest,
	ErrMissingPermissions: http.StatusForbidden,

	ErrReportAlreadyHandled: http.StatusBadRequest,
	ErrNotSelfDelete:        http.StatusForbidden,
}