/*
 * SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package app

import (
	"context"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strconv"

	"golang.org/x/text/language"

	"dev.tandem.ws/tandem/camper/pkg/auth"
	"dev.tandem.ws/tandem/camper/pkg/database"
	"dev.tandem.ws/tandem/camper/pkg/form"
	httplib "dev.tandem.ws/tandem/camper/pkg/http"
	"dev.tandem.ws/tandem/camper/pkg/locale"
	"dev.tandem.ws/tandem/camper/pkg/template"
)

func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) {
	cookie := auth.GetSessionCookie(r)
	host := httplib.Host(r)
	if _, err := conn.Exec(r.Context(), "select set_cookie($1, $2)", cookie, host); err != nil {
		return nil, err
	}

	user := &auth.User{
		Email:    "",
		LoggedIn: false,
		Role:     "guest",
	}
	row := conn.QueryRow(r.Context(), "select user_id, coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile")
	var langTag string
	if err := row.Scan(&user.ID, &user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CSRFToken); err != nil {
		return nil, err
	}
	if lang, err := language.Parse(langTag); err == nil {
		user.Language = lang
	} else {
		return nil, err
	}

	user.Locale = h.locales[user.Language]
	if user.Locale == nil {
		user.Locale = h.matchLocale(r)
	}

	return user, nil
}

func (h *App) matchLocale(r *http.Request) *locale.Locale {
	l := locale.Match(r.Header.Get("Accept-Language"), h.locales, h.languageMatcher)
	if l == nil {
		l = h.defaultLocale
	}
	return l
}

type profileHandler struct {
	fileHandle   http.Handler
	avatarHandle http.Handler
	avatarsDir   string
}

func newProfileHandler(static http.Handler, avatarsDir string) (*profileHandler, error) {
	if err := os.MkdirAll(avatarsDir, 0755); err != nil {
		return nil, err
	}
	handler := &profileHandler{
		fileHandle:   static,
		avatarsDir:   avatarsDir,
		avatarHandle: http.FileServer(http.Dir(avatarsDir)),
	}
	return handler, nil
}

func (h *profileHandler) Handler(user *auth.User, company *auth.Company, conn *database.Conn) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !user.LoggedIn {
			w.WriteHeader(http.StatusUnauthorized)
			serveLoginForm(w, r, user, company, r.RequestURI)
			return
		}

		var head string
		head, r.URL.Path = httplib.ShiftPath(r.URL.Path)

		switch head {
		case "avatar":
			switch r.Method {
			case http.MethodGet:
				h.serveAvatar(w, r, user)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet)
			}
		case "session":
			switch r.Method {
			case http.MethodDelete:
				handleLogout(w, r, user, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodDelete)
			}
		case "":
			switch r.Method {
			case http.MethodGet:
				serveProfileForm(w, r, user, company, conn)
			case http.MethodPut:
				h.updateProfile(w, r, user, company, conn)
			default:
				httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
			}
		default:
			http.NotFound(w, r)
		}
	}
}

func (h *profileHandler) serveAvatar(w http.ResponseWriter, r *http.Request, user *auth.User) {
	avatarPath := h.avatarPath(user)
	if _, err := os.Stat(avatarPath); err == nil {
		r.URL.Path = path.Base(avatarPath)
		h.avatarHandle.ServeHTTP(w, r)
	} else {
		r.URL.Path = "default_avatar.svg"
		h.fileHandle.ServeHTTP(w, r)
	}
}

func (h *profileHandler) avatarPath(user *auth.User) string {
	return filepath.Join(h.avatarsDir, strconv.Itoa(user.ID))
}

func serveProfileForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	profile := newProfileForm(r.Context(), user.Locale, conn)
	profile.Name.Val = conn.MustGetText(r.Context(), "select name from user_profile")
	profile.Email.Val = user.Email
	profile.Language.Selected = []string{user.Language.String()}
	profile.MustRender(w, r, user, company)
}

func (h *profileHandler) updateProfile(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
	profile := newProfileForm(r.Context(), user.Locale, conn)
	if err := profile.Parse(w, r); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	defer profile.Close()
	if err := user.VerifyCSRFToken(r); err != nil {
		http.Error(w, err.Error(), http.StatusForbidden)
		return
	}
	if !profile.Valid(user.Locale) {
		if !httplib.IsHTMxRequest(r) {
			w.WriteHeader(http.StatusUnprocessableEntity)
		}
		profile.MustRender(w, r, user, company)
		return
	}
	//goland:noinspection SqlWithoutWhere
	cookie := conn.MustGetText(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", profile.Name, profile.Email, profile.Language)
	auth.SetSessionCookie(w, cookie)
	if profile.Password.Val != "" {
		conn.MustExec(r.Context(), "select change_password($1)", profile.Password)
	}
	redirect := user.Language.String() != profile.Language.String()
	if profile.HasAvatarFile() {
		redirect = true
		if err := profile.SaveAvatarTo(h.avatarPath(user)); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
	if redirect {
		httplib.Redirect(w, r, "/me", http.StatusSeeOther)
	} else {
		httplib.Relocate(w, r, "/me", http.StatusSeeOther)
	}
}

type profileForm struct {
	Name            *form.Input
	Email           *form.Input
	Password        *form.Input
	PasswordConfirm *form.Input
	Language        *form.Select
	Avatar          *form.File
}

func newProfileForm(ctx context.Context, l *locale.Locale, conn *database.Conn) *profileForm {
	automaticOption := l.Pgettext("Automatic", "language option")
	languages := form.MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
	return &profileForm{
		Name: &form.Input{
			Name: "name",
		},
		Email: &form.Input{
			Name: "email",
		},
		Password: &form.Input{
			Name: "password",
		},
		PasswordConfirm: &form.Input{
			Name: "password_confirm",
		},
		Language: &form.Select{
			Name:    "language",
			Options: languages,
		},
		Avatar: &form.File{
			Name:    "avatar",
			MaxSize: 1 << 20,
		},
	}
}

func (f *profileForm) Parse(w http.ResponseWriter, r *http.Request) error {
	maxSize := f.Avatar.MaxSize + 1024
	r.Body = http.MaxBytesReader(w, r.Body, maxSize)
	if err := r.ParseMultipartForm(maxSize); err != nil {
		return err
	}
	f.Email.FillValue(r)
	f.Name.FillValue(r)
	f.Password.FillValue(r)
	f.PasswordConfirm.FillValue(r)
	f.Language.FillValue(r)
	if err := f.Avatar.FillValue(r); err != nil {
		return err
	}
	return nil
}

func (f *profileForm) Close() error {
	return f.Avatar.Close()
}

func (f *profileForm) Valid(l *locale.Locale) bool {
	v := form.NewValidator(l)
	if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
		v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
	}
	v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty."))
	v.CheckPasswordConfirmation(f.Password, f.PasswordConfirm, l.GettextNoop("Confirmation does not match password."))
	v.CheckSelectedOptions(f.Language, l.GettextNoop("Selected language is not valid."))
	if f.HasAvatarFile() {
		v.CheckImageFile(f.Avatar, l.GettextNoop("File must be a valid PNG or JPEG image."))
	}
	return v.AllOK
}

func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
	template.MustRenderAdmin(w, r, user, company, "profile.gohtml", f)
}

func (f *profileForm) HasAvatarFile() bool {
	return f.Avatar.HasData()
}

func (f *profileForm) SaveAvatarTo(avatarPath string) error {
	if !f.HasAvatarFile() {
		return nil
	}
	avatar, err := os.OpenFile(avatarPath, os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		return err
	}
	defer avatar.Close()
	_, err = io.Copy(avatar, f.Avatar)
	return err
}
