// 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 apiv2

import (
	"context"
	"errors"
	"net/http"
	"strings"

	"code.vikunja.io/api/pkg/log"
	"code.vikunja.io/api/pkg/models"
	"code.vikunja.io/api/pkg/modules/auth"
	"code.vikunja.io/api/pkg/web"

	"github.com/danielgtaylor/huma/v2"
)

// authFromCtx retrieves the authed user from a Huma handler context,
// surfacing lookup failures as 401 instead of falling through to 500.
func authFromCtx(ctx context.Context) (web.Auth, error) {
	a, err := auth.GetAuthFromContext(ctx)
	if err != nil {
		// The underlying error can carry internal adapter/config detail
		// (e.g. a missing Echo context — a programming error, since the
		// token middleware authenticates before the handler runs). Log it
		// and return a generic 401 so nothing internal leaks to clients.
		log.Errorf("v2: could not resolve auth from context: %s", err)
		return nil, huma.Error401Unauthorized("invalid or missing authentication")
	}
	return a, nil
}

// translateDomainError maps a Vikunja domain error (web.HTTPErrorProcessor)
// onto Huma's status-error type so the response carries the right code
// and an RFC 9457 body. Errors without HTTP semantics fall through, which
// Huma treats as 500.
func translateDomainError(err error) error {
	if err == nil {
		return nil
	}
	var hp web.HTTPErrorProcessor
	if errors.As(err, &hp) {
		details := hp.HTTPError()
		msg := details.Message
		if msg == "" {
			msg = err.Error()
		}
		se := huma.NewError(details.HTTPCode, msg)
		// Preserve Vikunja's numeric domain error code (the value the
		// error docs key off) on the problem+json body. v1 exposes it as
		// `code`; without this v2 clients always read 0.
		if vm, ok := se.(*vikunjaErrorModel); ok {
			vm.Code = details.Code
		}
		return se
	}
	// v2 maps validation failures to 422 (not v1's 412) so a govalidator failure
	// looks identical to Huma's own schema validation. ValidationHTTPError isn't an
	// HTTPErrorProcessor (the embedded field shadows the method), so it lands here.
	var ve models.ValidationHTTPError
	if errors.As(err, &ve) {
		se := huma.NewError(http.StatusUnprocessableEntity, ve.Error(), invalidFieldDetails(ve.InvalidFields)...)
		if vm, ok := se.(*vikunjaErrorModel); ok {
			vm.Code = ve.GetCode()
		}
		return se
	}
	return err
}

// invalidFieldDetails turns ValidationHTTPError's invalid_fields into RFC 9457
// error details. Entries come in two shapes — govalidator's "field: message" and
// model call sites' bare field names — and both must yield a Location.
func invalidFieldDetails(fields []string) []error {
	details := make([]error, 0, len(fields))
	for _, f := range fields {
		name, msg, ok := strings.Cut(f, ": ")
		if !ok {
			msg = "Invalid data"
		}
		details = append(details, &huma.ErrorDetail{Location: "body." + name, Message: msg})
	}
	return details
}

// vikunjaErrorModel extends Huma's RFC 9457 body with Vikunja's numeric
// domain error code, preserving the v1 error-code contract on v2. Wired in
// as the global error type via the huma.NewError override in init().
type vikunjaErrorModel struct {
	huma.ErrorModel
	Code int `json:"code,omitempty" readOnly:"true" doc:"Vikunja numeric error code; see https://vikunja.io/docs/errors/"`
}

func init() {
	// Replace Huma's default error constructor so both the generated
	// OpenAPI schema and runtime responses use vikunjaErrorModel. Huma
	// derives the error-response schema from NewError(0, "") at register
	// time and routes runtime errors through the same constructor, so the
	// `code` field stays consistent between spec and wire.
	huma.NewError = func(status int, msg string, errs ...error) huma.StatusError {
		details := make([]*huma.ErrorDetail, 0, len(errs))
		for _, e := range errs {
			if e == nil {
				continue
			}
			if d, ok := e.(huma.ErrorDetailer); ok {
				details = append(details, d.ErrorDetail())
			} else {
				details = append(details, &huma.ErrorDetail{Message: e.Error()})
			}
		}
		return &vikunjaErrorModel{ErrorModel: huma.ErrorModel{
			Status: status,
			Title:  http.StatusText(status),
			Detail: msg,
			Errors: details,
		}}
	}

	// Strip internal detail from server errors. Huma's handler-error path
	// wraps a raw error as NewErrorWithContext(ctx, 500, "unexpected error
	// occurred", err) and — because the humaecho5 adapter writes the
	// response itself — bypasses Vikunja's CreateHTTPErrorHandler, which for
	// v1 returns a generic 500 with no detail. Without this override a raw
	// DB/driver error (SQL, table, column names) would leak into the
	// problem+json `errors[]`. Log the real cause, return a generic body.
	huma.NewErrorWithContext = func(_ huma.Context, status int, msg string, errs ...error) huma.StatusError {
		if status >= 500 {
			for _, e := range errs {
				if e != nil {
					log.Errorf("v2: internal server error: %s", e)
				}
			}
			errs = nil
		}
		return huma.NewError(status, msg, errs...)
	}
}
