// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package routes

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

	"code.vikunja.io/api/pkg/files"
	"code.vikunja.io/api/pkg/log"
	"code.vikunja.io/api/pkg/web"

	"github.com/getsentry/sentry-go"
	"github.com/labstack/echo/v5"
)

// httpCodeGetter is an interface for errors that can provide their HTTP status code.
type httpCodeGetter interface {
	GetHTTPCode() int
}

// errorMessage is used to wrap string error messages in a consistent JSON structure.
type errorMessage struct {
	Message interface{} `json:"message"`
}

// CreateHTTPErrorHandler creates a centralized HTTP error handler that:
// 1. Converts all error types to proper HTTP responses
// 2. Preserves full error details (like ValidationHTTPError.InvalidFields)
// 3. Handles Sentry reporting for 5xx errors
// 4. Logs all errors appropriately
func CreateHTTPErrorHandler(e *echo.Echo, enableSentry bool) echo.HTTPErrorHandler {
	return func(c *echo.Context, err error) {
		// Check if the response has already been committed (e.g., by the RequestLogger middleware
		// with HandleError=true). If so, we should not try to write another response.
		if r, _ := echo.UnwrapResponse(c.Response()); r != nil && r.Committed {
			return
		}

		var (
			code                = http.StatusInternalServerError
			message interface{} = http.StatusText(http.StatusInternalServerError)
		)

		// Keep track of the original error for logging/sentry
		originalErr := err

		// 1. Check if it implements HTTPStatusCoder (includes echo.ErrForbidden, etc.)
		// In Echo v5, predefined errors like ErrForbidden are *httpError (unexported),
		// not *HTTPError, so we must check the interface instead of the concrete type.
		var sc echo.HTTPStatusCoder
		if errors.As(err, &sc) {
			code = sc.StatusCode()
			// HTTPStatusCoder doesn't have Error(), so we use the status text
			message = http.StatusText(code)
		}

		// 2. If it's specifically an HTTPError, use its message for more details
		var he *echo.HTTPError
		if errors.As(err, &he) {
			code = he.Code
			if he.Message != "" {
				message = he.Message
			}
		}

		// 3. Special case: 413 body limit → convert to ErrFileIsTooLarge
		// Check both the code (if it was an HTTPError) and errors.Is for wrapped errors
		// In Echo v5, body limit errors during multipart parsing may be wrapped
		if code == http.StatusRequestEntityTooLarge || errors.Is(err, echo.ErrStatusRequestEntityTooLarge) {
			fileErr := files.ErrFileIsTooLarge{}
			errDetails := fileErr.HTTPError()
			code = errDetails.HTTPCode
			message = errDetails
		} else if _, isMarshaler := err.(json.Marshaler); isMarshaler {
			// 4. Check for json.Marshaler (preserves full struct like ValidationHTTPError)
			// This allows errors with extra fields (like InvalidFields) to be serialized correctly
			if codeGetter, hasCode := err.(httpCodeGetter); hasCode {
				code = codeGetter.GetHTTPCode()
			}
			message = err // Echo will serialize via MarshalJSON
		} else if hp, ok := err.(web.HTTPErrorProcessor); ok {
			// 5. Standard HTTPErrorProcessor (domain errors like ErrProjectDoesNotExist)
			errDetails := hp.HTTPError()
			code = errDetails.HTTPCode
			message = errDetails
		}
		// 6. For any other error type, we keep the defaults (500 with generic message)
		// or the echo.HTTPStatusCoder/HTTPError values if it was that type

		// Sentry reporting for 5xx errors
		if enableSentry && code >= 500 {
			reportToSentry(originalErr, c)
		}

		// Send response
		if c.Request().Method == http.MethodHead {
			err = c.NoContent(code)
		} else {
			// Wrap string messages in a struct to ensure consistent JSON format
			// e.g., "Forbidden" becomes {"message": "Forbidden"}
			if _, isString := message.(string); isString {
				message = errorMessage{Message: message}
			}
			err = c.JSON(code, message)
		}
		if err != nil {
			e.Logger.Error(err.Error())
		}
	}
}

// reportToSentry sends an error to Sentry with request context
func reportToSentry(err error, c *echo.Context) {
	hub := GetSentryHubFromContext(c)
	if hub != nil {
		hub.WithScope(func(scope *sentry.Scope) {
			scope.SetExtra("url", c.Request().URL)
			hub.CaptureException(err)
		})
	} else {
		sentry.CaptureException(err)
		log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
	}
	log.Debugf("Error '%s' sent to sentry", err.Error())
}
