package auth

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

	"github.com/go-kit/kit/log/level"

	"git.c3pb.de/gbe/invinoveritas/log"
)

type contextKey string

var ErrAuthFailed = errors.New("authentication failed")

// User is an application user.
type User struct {
	Name     string
	IsAdmin  bool
	CanWrite bool // Set for non-anonymous users
}

func (u User) String() string {
	readOnly := " (ro)"
	if u.CanWrite {
		readOnly = ""
	}

	return "{" + u.Name + readOnly + "}"
}

type Provider interface {
	// Valid checks whether the given token represents a valid user session. If that is the case, the user
	// is returned. Otherwise, an error is returned.
	Valid(ctx context.Context, token string) (*User, error)
}

// Require wraps hdlr so that it requires authentication to use. Requests handled by hdlr will have the user
// name attached to their context. Use the User function to retrieve it.
//
// If authentication fails, for example because the users' session is no longer valid or doesn't exist, the
// request will be handled by the authFailed handler. It will receive the original request and response writer.
// This is triggered by provider returning an error wrapping ErrAuthFailed from its `Valid` method.
func Require(next http.Handler, authFailed http.Handler, provider Provider) http.Handler {
	key := contextKey("user")

	anonAuth := func(w http.ResponseWriter, r *http.Request) {
		// No session: allow GET and friends, deny POST by requesting authentication
		if r.Method == "POST" || strings.HasPrefix(r.URL.Path, "/user/") {
			level.Error(log.Get(r)).
				Log("msg", "denying POST access for unknown user")

			authFailed.ServeHTTP(w, r)

			return
		}

		level.Info(log.Get(r)).
			Log("msg", "no session, using anon auth")

		user := &User{
			Name: "anon",
		}

		ctx := context.WithValue(r.Context(), key, *user)

		next.ServeHTTP(w, r.WithContext(ctx))
	}

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var token string

		cookie, err := r.Cookie("session")
		if err == nil {
			token = cookie.Value
		}

		user, err := provider.Valid(r.Context(), token)
		if errors.Is(err, ErrAuthFailed) {
			anonAuth(w, r)
			return
		}

		if err != nil || user == nil {
			if user == nil && err == nil {
				// The auth provider did something weird: no error, but no user either
				err = errors.New("no user")
			}

			// Some error that isn't because the session is expired or doesn't exist (DB?)
			level.Error(log.Get(r)).
				Log("error", err, "msg", "can't authenticate user")

			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		user.CanWrite = true

		level.Info(log.Get(r)).
			Log("user", *user, "msg", "user authenticated")

		ctx := context.WithValue(r.Context(), key, *user)

		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// Get returns the user from r's context. If there is no user in r's context, returns an empty user.
func Get(r *http.Request) User {
	val := r.Context().Value(contextKey("user"))
	if val == nil {
		return User{}
	}

	return val.(User)
}