// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

package authentication

import (
	"net/http"
	"reflect"
	"strings"
	"text/template"

	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
)

// UserInformation contains information about the current user.
type UserInformation struct {
	Login     string `json:"login" header:"LOGIN" binding:"required"`
	Name      string `json:"name,omitempty" header:"NAME"`
	Email     string `json:"email,omitempty" header:"EMAIL" binding:"omitempty,email"`
	LogoutURL string `json:"logout-url,omitempty" header:"LOGOUT" binding:"omitempty,uri"`
	AvatarURL string `json:"avatar-url,omitempty" header:"AVATAR" binding:"omitempty,uri"`
}

// UserAuthentication is a middleware to fill information about the
// current user. It does not really perform authentication but relies
// on HTTP headers.
func (c *Component) UserAuthentication() gin.HandlerFunc {
	var logoutURLTmpl, avatarURLTmpl *template.Template
	if c.config.LogoutURL != "" {
		logoutURLTmpl, _ = template.New("logout").Parse(c.config.LogoutURL)
	}
	if c.config.AvatarURL != "" {
		avatarURLTmpl, _ = template.New("avatar").Parse(c.config.AvatarURL)
	}

	return func(gc *gin.Context) {
		var info UserInformation
		if err := gc.ShouldBindWith(&info, customHeaderBinding{c}); err != nil {
			if c.config.DefaultUser.Login == "" {
				gc.JSON(http.StatusUnauthorized, gin.H{"message": "No user logged in."})
				gc.Abort()
				return
			}
			info = c.config.DefaultUser
		}

		// Apply configured templates (they can access header values and choose to keep or override)
		if logoutURLTmpl != nil {
			var buf strings.Builder
			if err := logoutURLTmpl.Execute(&buf, info); err == nil {
				info.LogoutURL = buf.String()
			}
		}
		if avatarURLTmpl != nil {
			var buf strings.Builder
			if err := avatarURLTmpl.Execute(&buf, info); err == nil {
				info.AvatarURL = buf.String()
			}
		}

		gc.Set("user", info)
		gc.Next()
	}
}

type customHeaderBinding struct {
	c *Component
}

func (customHeaderBinding) Name() string {
	return "header"
}

// Bind will bind struct fields to HTTP headers using the configured mapping.
func (b customHeaderBinding) Bind(req *http.Request, obj any) error {
	value := reflect.ValueOf(obj).Elem()
	tValue := reflect.TypeOf(obj).Elem()
	if value.Kind() != reflect.Struct {
		panic("should be a struct")
	}
	for i := range tValue.NumField() {
		sf := tValue.Field(i)
		if sf.PkgPath != "" && !sf.Anonymous { // unexported
			continue
		}
		tag := sf.Tag.Get("header")
		if tag == "" || tag == "-" {
			continue
		}
		var header string
		switch tag {
		case "LOGIN":
			header = b.c.config.Headers.Login
		case "NAME":
			header = b.c.config.Headers.Name
		case "EMAIL":
			header = b.c.config.Headers.Email
		case "LOGOUT":
			header = b.c.config.Headers.LogoutURL
		case "AVATAR":
			header = b.c.config.Headers.AvatarURL
		}
		if header == "" {
			continue
		}
		value.Field(i).SetString(req.Header.Get(header))
	}

	return binding.Validator.ValidateStruct(obj)
}
