// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2024 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package context

import (
	"errors"
	"fmt"
	"html/template"
	"net"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"
	"syscall"
	"time"

	"forgejo.org/models/auth"
	user_model "forgejo.org/models/user"
	"forgejo.org/modules/base"
	"forgejo.org/modules/httplib"
	"forgejo.org/modules/log"
	"forgejo.org/modules/setting"
	"forgejo.org/modules/templates"
	"forgejo.org/modules/web/middleware"
)

// RedirectToUser redirect to a differently-named user
func RedirectToUser(ctx *Base, userName string, redirectUserID int64) {
	user, err := user_model.GetUserByID(ctx, redirectUserID)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "unable to get user")
		return
	}

	redirectPath := strings.Replace(
		ctx.Req.URL.EscapedPath(),
		url.PathEscape(userName),
		url.PathEscape(user.Name),
		1,
	)
	if ctx.Req.URL.RawQuery != "" {
		redirectPath += "?" + ctx.Req.URL.RawQuery
	}
	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath), http.StatusTemporaryRedirect)
}

// RedirectToFirst redirects to first not empty URL which likely belongs to current site.
// If no suitable redirection is found, it redirects to the home.
// It returns the location it redirected to.
func (ctx *Context) RedirectToFirst(location ...string) string {
	for _, loc := range location {
		if len(loc) == 0 {
			continue
		}

		if httplib.IsRiskyRedirectURL(loc) {
			continue
		}

		ctx.Redirect(loc)
		return loc
	}

	ctx.Redirect(setting.AppSubURL + "/")
	return setting.AppSubURL + "/"
}

const (
	tplStatus404 base.TplName = "status/404"
	tplStatus500 base.TplName = "status/500"
)

// HTML calls Context.HTML and renders the template to HTTP response
func (ctx *Context) HTML(status int, name base.TplName) {
	log.Debug("Template: %s", name)

	tmplStartTime := time.Now()
	if !setting.IsProd {
		ctx.Data["TemplateName"] = name
	}
	ctx.Data["TemplateLoadTimes"] = func() string {
		return strconv.FormatInt(time.Since(tmplStartTime).Nanoseconds()/1e6, 10) + "ms"
	}

	err := ctx.Render.HTML(ctx.Resp, status, string(name), ctx.Data, ctx.TemplateContext)
	if err == nil || errors.Is(err, syscall.EPIPE) {
		return
	}

	// if rendering fails, show error page
	if name != tplStatus500 {
		err = fmt.Errorf("failed to render template: %s, error: %s", name, templates.HandleTemplateRenderingError(err))
		ctx.ServerError("Render failed", err) // show the 500 error page
	} else {
		ctx.PlainText(http.StatusInternalServerError, "Unable to render status/500 page, the template system is broken, or Gitea can't find your template files.")
		return
	}
}

// JSONTemplate renders the template as JSON response
// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape
func (ctx *Context) JSONTemplate(tmpl base.TplName) {
	t, err := ctx.Render.TemplateLookup(string(tmpl), nil)
	if err != nil {
		ctx.ServerError("unable to find template", err)
		return
	}
	ctx.Resp.Header().Set("Content-Type", "application/json")
	if err = t.Execute(ctx.Resp, ctx.Data); err != nil {
		ctx.ServerError("unable to execute template", err)
	}
}

// RenderToHTML renders the template content to a HTML string
func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
	var buf strings.Builder
	err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
	return template.HTML(buf.String()), err
}

// RenderWithErr used for page has form validation but need to prompt error to users.
func (ctx *Context) RenderWithErr(msg any, tpl base.TplName, form any) {
	if form != nil {
		middleware.AssignForm(form, ctx.Data)
	}
	ctx.Flash.Error(msg, true)
	ctx.HTML(http.StatusOK, tpl)
}

// validateTwoFactorRequirement sets ctx-data to hide/show ui-elements depending on the GlobalTwoFactorRequirement
func (ctx *Context) validateTwoFactorRequirement() {
	if ctx.Doer == nil || !ctx.Doer.MustHaveTwoFactor() {
		return
	}

	hasTwoFactor, err := auth.HasTwoFactorByUID(ctx, ctx.Doer.ID)
	if err != nil {
		log.ErrorWithSkip(2, "Error getting 2fa: %s", err)
	}
	ctx.Data["MustEnableTwoFactor"] = !hasTwoFactor
	ctx.Data["HideNavbarLinks"] = !hasTwoFactor
}

// NotFound displays a 404 (Not Found) page and prints the given error, if any.
func (ctx *Context) NotFound(logMsg string, logErr error) {
	ctx.notFoundInternal(logMsg, logErr)
}

func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
	if logErr != nil {
		log.Log(2, log.DEBUG, "%s: %v", logMsg, logErr)
		if !setting.IsProd {
			ctx.Data["ErrorMsg"] = logErr
		}
	}

	// response simple message if Accept isn't text/html
	showHTML := false
	for _, part := range ctx.Req.Header["Accept"] {
		if strings.Contains(part, "text/html") {
			showHTML = true
			break
		}
	}

	if !showHTML {
		ctx.plainTextInternal(3, http.StatusNotFound, []byte("Not found.\n"))
		return
	}

	ctx.validateTwoFactorRequirement()
	ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
	ctx.Data["Title"] = ctx.Locale.TrString("error.not_found.title")
	ctx.HTML(http.StatusNotFound, tplStatus404)
}

// ServerError displays a 500 (Internal Server Error) page and prints the given error, if any.
func (ctx *Context) ServerError(logMsg string, logErr error) {
	ctx.serverErrorInternal(logMsg, logErr)
}

func (ctx *Context) serverErrorInternal(logMsg string, logErr error) {
	if logErr != nil {
		log.ErrorWithSkip(2, "%s: %v", logMsg, logErr)
		var opError *net.OpError
		if errors.As(logErr, &opError) {
			// This is an error within the underlying connection
			// and further rendering will not work so just return
			return
		}

		// it's safe to show internal error to admin users, and it helps
		if !setting.IsProd || (ctx.Doer != nil && ctx.Doer.IsAdmin) {
			ctx.Data["ErrorMsg"] = fmt.Sprintf("%s, %s", logMsg, logErr)
		}
	}

	ctx.validateTwoFactorRequirement()
	ctx.HTML(http.StatusInternalServerError, tplStatus500)
}

// NotFoundOrServerError use error check function to determine if the error
// is about not found. It responds with 404 status code for not found error,
// or error context description for logging purpose of 500 server error.
// TODO: remove the "errCheck" and use util.ErrNotFound to check
func (ctx *Context) NotFoundOrServerError(logMsg string, errCheck func(error) bool, logErr error) {
	if errCheck(logErr) {
		ctx.notFoundInternal(logMsg, logErr)
		return
	}
	ctx.serverErrorInternal(logMsg, logErr)
}
