package session

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

	"github.com/go-chi/chi"
	"github.com/go-chi/chi/middleware"
	"github.com/influxdata/influxdb/v2"
	"github.com/influxdata/influxdb/v2/kit/platform/errors"
	kithttp "github.com/influxdata/influxdb/v2/kit/transport/http"
	"go.uber.org/zap"
)

const (
	prefixSignIn  = "/api/v2/signin"
	prefixSignOut = "/api/v2/signout"
)

// SessionHandler represents an HTTP API handler for authorizations.
type SessionHandler struct {
	chi.Router
	api *kithttp.API
	log *zap.Logger

	sessionSvc influxdb.SessionService
	passSvc    influxdb.PasswordsService
	userSvc    influxdb.UserService
}

// NewSessionHandler returns a new instance of SessionHandler.
func NewSessionHandler(log *zap.Logger, sessionSvc influxdb.SessionService, userSvc influxdb.UserService, passwordsSvc influxdb.PasswordsService) *SessionHandler {
	svr := &SessionHandler{
		api: kithttp.NewAPI(kithttp.WithLog(log)),
		log: log,

		passSvc:    passwordsSvc,
		sessionSvc: sessionSvc,
		userSvc:    userSvc,
	}

	return svr
}

type resourceHandler struct {
	prefix string
	*SessionHandler
}

// Prefix is necessary to mount the router as a resource handler
func (r resourceHandler) Prefix() string { return r.prefix }

// SignInResourceHandler allows us to return 2 different resource handler
// for the appropriate mounting location
func (h SessionHandler) SignInResourceHandler() *resourceHandler {
	h.Router = chi.NewRouter()
	h.Router.Use(
		middleware.Recoverer,
		middleware.RequestID,
		middleware.RealIP,
	)
	h.Router.Post("/", h.handleSignin)
	return &resourceHandler{prefix: prefixSignIn, SessionHandler: &h}
}

// SignOutResourceHandler allows us to return 2 different resource handler
// for the appropriate mounting location
func (h SessionHandler) SignOutResourceHandler() *resourceHandler {
	h.Router = chi.NewRouter()
	h.Router.Use(
		middleware.Recoverer,
		middleware.RequestID,
		middleware.RealIP,
	)
	h.Router.Post("/", h.handleSignout)
	return &resourceHandler{prefix: prefixSignOut, SessionHandler: &h}
}

// handleSignin is the HTTP handler for the POST /signin route.
func (h *SessionHandler) handleSignin(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	req, decErr := decodeSigninRequest(ctx, r)
	if decErr != nil {
		h.api.Err(w, r, ErrUnauthorized)
		return
	}

	u, err := h.userSvc.FindUser(ctx, influxdb.UserFilter{
		Name: &req.Username,
	})
	if err != nil {
		h.api.Err(w, r, ErrUnauthorized)
		return
	}

	if err := h.passSvc.ComparePassword(ctx, u.ID, req.Password); err != nil {
		if !eBase.Is(err, errors.EPasswordChangeRequired) {
			err = ErrUnauthorized
		}
		h.api.Err(w, r, err)
		return
	}

	s, e := h.sessionSvc.CreateSession(ctx, req.Username)
	if e != nil {
		h.api.Err(w, r, ErrUnauthorized)
		return
	}

	encodeCookieSession(w, s, (r != nil) && (r.TLS != nil))
	w.WriteHeader(http.StatusNoContent)
}

type signinRequest struct {
	Username string
	Password string
}

func decodeSigninRequest(ctx context.Context, r *http.Request) (*signinRequest, *errors.Error) {
	u, p, ok := r.BasicAuth()
	if !ok {
		return nil, &errors.Error{
			Code: errors.EInvalid,
			Msg:  "invalid basic auth",
		}
	}

	return &signinRequest{
		Username: u,
		Password: p,
	}, nil
}

// handleSignout is the HTTP handler for the POST /signout route.
func (h *SessionHandler) handleSignout(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	req, err := decodeSignoutRequest(ctx, r)
	if err != nil {
		h.api.Err(w, r, ErrUnauthorized)
		return
	}

	if err := h.sessionSvc.ExpireSession(ctx, req.Key); err != nil {
		h.api.Err(w, r, ErrUnauthorized)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

type signoutRequest struct {
	Key string
}

func decodeSignoutRequest(ctx context.Context, r *http.Request) (*signoutRequest, error) {
	key, err := DecodeCookieSession(ctx, r)
	if err != nil {
		return nil, err
	}
	return &signoutRequest{
		Key: key,
	}, nil
}

const cookieSessionName = "influxdb-oss-session"

func encodeCookieSession(w http.ResponseWriter, s *influxdb.Session, tlsEnabled bool) {
	// We only need the session cookie for accesses to "/api/...", so limit
	// it to that using "Path".
	//
	// Since the cookie is limited to "/api/..." and we don't expect any
	// links directly into /api/..., use SameSite=Strict as a hardening
	// measure. This works because external links into the UI have the form
	// https://<url>/orgs/<origid>/..., https://<url>/signin, etc and don't
	// need the cookie sent. By the time the UI itself calls out to
	// /api/..., the location bar matches the cookie's domain and
	// everything is 1st party and Strict's restriction work fine.
	//
	// SameSite=Lax would also be safe to use (modern browser's default if
	// unset) since it only sends the cookie with GET (and other safe HTTP
	// methods like HEAD and OPTIONS as defined in RFC6264) requests when
	// the location bar matches the domain of the cookie and we know that
	// our APIs do not perform state-changing actions with GET and other
	// safe methods. Using SameSite=Strict helps future-proof us against
	// that changing (ie, we add a state-changing GET API).
	//
	// Note: it's generally recommended that SameSite should not be relied
	// upon (particularly Lax) because:
	// a) SameSite doesn't work with (cookie-less) Basic Auth. We don't
	//    share browser session BasicAuth with accesses to to /api/... so
	//    this isn't a problem
	// b) SameSite=lax allows GET (and other safe HTTP methods) and some
	//    services might allow state-changing requests via GET. Our API
	//    doesn't support state-changing GETs and SameSite=strict doesn't
	//    allow GETs from 3rd party sites at all, so this isn't a problem
	// c) similar to 'b', some frameworks will accept HTTP methods for
	//    other handlers. Eg, the application is designed for POST but it
	//    will accept requests converted to the GET method. Golang does not
	//    do this itself and our route mounts explicitly map the HTTP
	//    method to the specific handler and thus we are not susceptible to
	//    this
	// d) SameSite could be bypassed if the attacker were able to
	//    manipulate the location bar in the browser (a serious browser
	//    bug; it is reasonable for us to expect browsers to enforce their
	//    SameSite restrictions)
	c := &http.Cookie{
		Name:     cookieSessionName,
		Value:    s.Key,
		Path:     "/api/", // since UI doesn't need it, limit cookie usage to API requests
		Expires:  s.ExpiresAt,
		SameSite: http.SameSiteStrictMode,
		HttpOnly: true,
		Secure:   tlsEnabled,
	}

	http.SetCookie(w, c)
}

func DecodeCookieSession(ctx context.Context, r *http.Request) (string, error) {
	c, err := r.Cookie(cookieSessionName)
	if err != nil {
		return "", &errors.Error{
			Code: errors.EInvalid,
			Err:  err,
		}
	}
	return c.Value, nil
}

// SetCookieSession adds a cookie for the session to an http request
func SetCookieSession(key string, r *http.Request) {
	c := &http.Cookie{
		Name:     cookieSessionName,
		Value:    key,
		Secure:   true,
		SameSite: http.SameSiteStrictMode,
	}

	r.AddCookie(c)
}
