package auth

import (
	"context"
	"errors"
	"html/template"
	"net/http"

	"github.com/FAU-CDI/wisski-distillery/internal/dis/component"
	"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server"
	"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
	"github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
	"github.com/tkw1536/pkglib/httpx/form"
	"github.com/tkw1536/pkglib/httpx/form/field"

	"github.com/gorilla/sessions"

	_ "embed"
)

// SessionOf returns the session and user logged into the provided request.
// token indicates if the user used a token to authenticate, or a browser session was used.
// A token takes priority over a user in a session.
//
// If there is no user associated with the given request, user and error are nil, and token is false.
// An invalid session, expired token, or disabled user all result in user = nil.
//
// When no SessionOf exists in the given session returns nil.
func (auth *Auth) SessionOf(r *http.Request) (session component.SessionInfo, user *AuthUser, err error) {
	// check the user from the token first
	{
		user, err := auth.UserOfToken(r)
		if user != nil && err == nil {
			return component.SessionInfo{User: &user.User, Token: true}, user, nil
		}
	}

	// fallback to using session
	{
		user, err := auth.UserOfSession(r)
		if err != nil {
			return component.SessionInfo{}, nil, err
		}
		if user == nil {
			return component.SessionInfo{}, nil, nil
		}
		return component.SessionInfo{User: &user.User, Token: false}, user, nil
	}
}

// UserOfToken returns the user associated with the token in request.
// To check the user of a token or session, use SessionOf.
func (auth *Auth) UserOfToken(r *http.Request) (user *AuthUser, err error) {
	// get the token object
	token, err := auth.dependencies.Tokens.TokenOf(r)
	if token == nil {
		return nil, err
	}
	return auth.checkUser(r.Context(), token.User)
}

// UserOfSession returns the user of the session associated with r.
func (auth *Auth) UserOfSession(r *http.Request) (user *AuthUser, err error) {
	ctx := r.Context()
	if user, ok := ctx.Value(ctxUserKey).(*AuthUser); ok && user != nil {
		return user, nil
	}

	// first read the session
	sess, err := auth.session(r)
	if err != nil {
		return nil, err
	}

	// try to read the name from the session
	name, ok := sess.Values[server.SessionUserKey]
	if !ok {
		return nil, nil
	}
	nameS, ok := name.(string)
	if !ok || nameS == "" {
		return nil, nil
	}
	return auth.checkUser(ctx, nameS)
}

func (auth *Auth) checkUser(ctx context.Context, name string) (user *AuthUser, err error) {
	// fetch the user, check if they still exist
	user, err = auth.User(ctx, name)
	if err == ErrUserNotFound {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	// user isn't enabled
	if user == nil || !user.IsEnabled() {
		return nil, nil
	}

	// get the user
	return user, nil
}

// session returns the session that belongs to a given request.
// If the session is not set, creates a new session.
func (auth *Auth) session(r *http.Request) (*sessions.Session, error) {
	return auth.store.Get(func() sessions.Store {
		return sessions.NewCookieStore([]byte(component.GetStill(auth).Config.SessionSecret))
	}).Get(r, server.SessionCookie)
}

func (auth *Auth) Menu(r *http.Request) []component.MenuItem {

	user, err := auth.UserOfSession(r)
	if user == nil || err != nil {
		return nil
	}
	return []component.MenuItem{
		{
			Title:    "Logout",
			Path:     "/auth/logout",
			Priority: component.MenuAuth,
		},
	}

}

type contextUserKey struct{}

var ctxUserKey = contextUserKey{}

// Login logs a user into the given request.
//
// If a user was previously logged into this session,
// UserOf may not return the correct user until the user makes a new request.
//
// It is recommended to send a HTTP redirect to make sure a new request is made.
func (auth *Auth) Login(w http.ResponseWriter, r *http.Request, user *AuthUser) error {
	sess, err := auth.session(r)
	if err != nil {
		return err
	}
	sess.Values[server.SessionUserKey] = user.User.User
	return sess.Save(r, w)
}

// Logout logs out the user from the given session.
//
// UserOf may return incorrect results until the user makes a new request.
// It is recommended to send a HTTP redirect to make sure a new request is made.
func (auth *Auth) Logout(w http.ResponseWriter, r *http.Request) error {
	sess, err := auth.session(r)
	if err != nil {
		return err
	}
	sess.Options.MaxAge = -1
	return sess.Save(r, w)
}

//go:embed "login.html"
var loginHTML []byte
var loginTemplate = templating.ParseForm(
	"login.html", loginHTML, form.FormTemplate,

	templating.Title("Login Required"),
	templating.Assets(assets.AssetsUser),
)

var errLoginFailed = errors.New("login failed")

// authLogin implements a view to login a user
func (auth *Auth) authLogin(ctx context.Context) http.Handler {
	tpl := loginTemplate.Prepare(
		auth.dependencies.Templating,
		func(flags templating.Flags, r *http.Request) templating.Flags {
			flags.Crumbs = []component.MenuItem{
				{Title: "Login", Path: template.URL(r.URL.RequestURI())},
			}
			return flags
		},
	)

	return &form.Form[*AuthUser]{
		Fields: []field.Field{
			{Name: "username", Type: field.Text, Autocomplete: field.Username, Label: "Username"},
			{Name: "password", Type: field.Password, Autocomplete: field.CurrentPassword, EmptyOnError: true, Label: "Password"},
			{Name: "otp", Type: field.Text, Autocomplete: field.OneTimeCode, EmptyOnError: true, Label: "Passcode (optional)"},
		},
		FieldTemplate: assets.PureCSSFieldTemplate,

		Template: tpl.Template(),
		TemplateContext: func(ctx form.FormContext, r *http.Request) any {
			if ctx.Err != nil {
				ctx.Err = errLoginFailed
			}
			return tpl.Context(r, templating.NewFormContext(ctx))
		},

		Validate: func(r *http.Request, values map[string]string) (*AuthUser, error) {
			username, password, passcode := values["username"], values["password"], values["otp"]

			// make sure that the user exists
			user, err := auth.User(ctx, username)
			if err != nil {
				return nil, err
			}

			// check the password and totp
			err = user.CheckCredentials(ctx, []byte(password), passcode)
			if err != nil {
				return nil, err
			}
			return user, nil
		},

		Skip: func(r *http.Request) (user *AuthUser, skip bool) {
			user, err := auth.UserOfSession(r)
			return user, err == nil && user != nil
		},

		Success: func(user *AuthUser, _ map[string]string, w http.ResponseWriter, r *http.Request) error {
			if err := auth.Login(w, r, user); err != nil {
				return err
			}

			// get the destination
			next := r.URL.Query().Get("next")
			if next == "" || next[0] != '/' {
				next = "/"
			}

			// and redirect to it!
			http.Redirect(w, r, next, http.StatusSeeOther)

			return nil
		},
	}
}

// authLogout implements the authLogout view to logout a user
func (auth *Auth) authLogout(context.Context) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// do the logout
		auth.Logout(w, r)

		// get the destination
		next := r.URL.Query().Get("next")
		if next == "" || next[0] != '/' {
			next = "/"
		}

		// and redirect to it!
		http.Redirect(w, r, next, http.StatusSeeOther)
	})
}
