// 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 humaecho5 is a Huma adapter for labstack/echo/v5.
//
// Huma ships an official echo adapter at
// github.com/danielgtaylor/huma/v2/adapters/humaecho, but it targets
// echo/v4. The echo/v5 port lives on an unmerged upstream branch
// (https://github.com/danielgtaylor/huma/pull/959) so we vendor it
// locally until that PR lands. Once it does, delete this package and
// switch imports back to the upstream adapter.
//
// We also add one piece of Vikunja-specific glue: every request stashes
// its underlying *echo.Context on context.Context under EchoContextKey,
// so Huma-dispatched handlers can reach the echo context via
// auth.GetAuthFromContext without the adapter needing per-handler wiring.
package humaecho5

import (
	"context"
	"crypto/tls"
	"io"
	"mime/multipart"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/danielgtaylor/huma/v2"
	"github.com/labstack/echo/v5"
)

// MultipartMaxMemory is the maximum memory to use when parsing multipart
// form data.
var MultipartMaxMemory int64 = 8 * 1024

// echoContextKey is the context key under which the underlying *echo.Context
// is stashed on the request's context.Context. Handlers that run inside a
// Huma-dispatched call can retrieve it via ctx.Value(EchoContextKey).
type echoContextKey struct{}

// EchoContextKey is the exported key for retrieving the underlying echo
// context from a Huma handler's context.Context.
var EchoContextKey = echoContextKey{}

// Unwrap extracts the underlying Echo context from a Huma context. Panics if
// called on a context from a different adapter.
func Unwrap(ctx huma.Context) *echo.Context {
	for {
		if c, ok := ctx.(interface{ Unwrap() huma.Context }); ok {
			ctx = c.Unwrap()
			continue
		}
		break
	}
	if c, ok := ctx.(*echoCtx); ok {
		return c.Unwrap()
	}
	panic("not a humaecho5 context")
}

type echoCtx struct {
	op     *huma.Operation
	orig   *echo.Context
	status int
}

var _ huma.Context = &echoCtx{}

func (c *echoCtx) Unwrap() *echo.Context      { return c.orig }
func (c *echoCtx) Operation() *huma.Operation { return c.op }

func (c *echoCtx) Context() context.Context {
	// Stash the underlying echo context so downstream helpers
	// (e.g. auth.GetAuthFromContext) can retrieve it.
	return context.WithValue((*c.orig).Request().Context(), EchoContextKey, c.orig)
}

func (c *echoCtx) Method() string     { return (*c.orig).Request().Method }
func (c *echoCtx) Host() string       { return (*c.orig).Request().Host }
func (c *echoCtx) RemoteAddr() string { return (*c.orig).Request().RemoteAddr }
func (c *echoCtx) URL() url.URL       { return *(*c.orig).Request().URL }

func (c *echoCtx) Param(name string) string  { return (*c.orig).Param(name) }
func (c *echoCtx) Query(name string) string  { return (*c.orig).QueryParam(name) }
func (c *echoCtx) Header(name string) string { return (*c.orig).Request().Header.Get(name) }

func (c *echoCtx) EachHeader(cb func(name, value string)) {
	for name, values := range (*c.orig).Request().Header {
		for _, value := range values {
			cb(name, value)
		}
	}
}

func (c *echoCtx) BodyReader() io.Reader { return (*c.orig).Request().Body }

func (c *echoCtx) GetMultipartForm() (*multipart.Form, error) {
	err := (*c.orig).Request().ParseMultipartForm(MultipartMaxMemory)
	return (*c.orig).Request().MultipartForm, err
}

func (c *echoCtx) SetReadDeadline(deadline time.Time) error {
	return huma.SetReadDeadline((*c.orig).Response(), deadline)
}

func (c *echoCtx) SetStatus(code int) {
	c.status = code
	(*c.orig).Response().WriteHeader(code)
}

func (c *echoCtx) Status() int { return c.status }

func (c *echoCtx) AppendHeader(name, value string) {
	(*c.orig).Response().Header().Add(name, value)
}

func (c *echoCtx) SetHeader(name, value string) {
	(*c.orig).Response().Header().Set(name, value)
}

func (c *echoCtx) BodyWriter() io.Writer { return (*c.orig).Response() }

func (c *echoCtx) TLS() *tls.ConnectionState { return (*c.orig).Request().TLS }

func (c *echoCtx) Version() huma.ProtoVersion {
	r := (*c.orig).Request()
	return huma.ProtoVersion{
		Proto:      r.Proto,
		ProtoMajor: r.ProtoMajor,
		ProtoMinor: r.ProtoMinor,
	}
}

type router interface {
	Add(method, path string, handler echo.HandlerFunc, middlewares ...echo.MiddlewareFunc) echo.RouteInfo
}

type echoAdapter struct {
	http.Handler
	router router
	// groupPrefix is the Echo group's URL prefix (e.g. "/api/v2"). When
	// set, internal dispatches (ServeHTTP calls from huma machinery such
	// as autopatch) whose path does not already start with the prefix get
	// the prefix prepended before routing — this keeps autopatch's
	// relative path resolution working even when the adapter is mounted
	// on a sub-group.
	groupPrefix string
}

func (a *echoAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if a.groupPrefix != "" && !strings.HasPrefix(r.URL.Path, a.groupPrefix) {
		r = r.Clone(r.Context())
		r.URL.Path = a.groupPrefix + r.URL.Path
		if r.URL.RawPath != "" {
			r.URL.RawPath = a.groupPrefix + r.URL.RawPath
		}
	}
	a.Handler.ServeHTTP(w, r)
}

func (a *echoAdapter) Handle(op *huma.Operation, handler func(huma.Context)) {
	// Convert {param} to :param for Echo's router.
	path := op.Path
	path = strings.ReplaceAll(path, "{", ":")
	path = strings.ReplaceAll(path, "}", "")
	a.router.Add(op.Method, path, func(c *echo.Context) error {
		ctx := &echoCtx{op: op, orig: c}
		handler(ctx)
		return nil
	})
}

// New creates a new Huma API using the provided Echo router.
func New(r *echo.Echo, config huma.Config) huma.API {
	return huma.NewAPI(config, &echoAdapter{Handler: r, router: r})
}

// NewWithGroup creates a new Huma API using the provided Echo router and
// group. Handlers registered through the returned API land on `g`, so they
// inherit the group's middleware stack. `groupPrefix` must equal the
// prefix `g` was constructed with (e.g. "/api/v2"); the adapter uses it
// to rewrite internal Huma dispatches (notably autopatch's GET+PUT round
// trip) back onto the absolute URLs Echo routes on.
func NewWithGroup(r *echo.Echo, g *echo.Group, groupPrefix string, config huma.Config) huma.API {
	return huma.NewAPI(config, &echoAdapter{Handler: r, router: g, groupPrefix: groupPrefix})
}
