package api

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/tevino/abool"

	"github.com/safing/portbase/config"
	"github.com/safing/portbase/log"
	"github.com/safing/portbase/modules"
	"github.com/safing/portbase/rng"
)

const (
	sessionCookieName = "Portmaster-API-Token"
	sessionCookieTTL  = 5 * time.Minute
)

var (
	apiKeys     = make(map[string]*AuthToken)
	apiKeysLock sync.Mutex

	authFnSet = abool.New()
	authFn    AuthenticatorFunc

	sessions     = make(map[string]*session)
	sessionsLock sync.Mutex

	// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
	// AuthenticatorFunc in order to signify a blocked request, including a error
	// message for the user. This is an empty message on purpose, as to allow the
	// function to define the full text of the error shown to the user.
	ErrAPIAccessDeniedMessage = errors.New("")
)

// Permission defines an API requests permission.
type Permission int8

const (
	// NotFound declares that the operation does not exist.
	NotFound Permission = -2

	// Dynamic declares that the operation requires permission to be processed,
	// but anyone can execute the operation, as it reacts to permissions itself.
	Dynamic Permission = -1

	// NotSupported declares that the operation is not supported.
	NotSupported Permission = 0

	// PermitAnyone declares that anyone can execute the operation without any
	// authentication.
	PermitAnyone Permission = 1

	// PermitUser declares that the operation may be executed by authenticated
	// third party applications that are categorized as representing a simple
	// user and is limited in access.
	PermitUser Permission = 2

	// PermitAdmin declares that the operation may be executed by authenticated
	// third party applications that are categorized as representing an
	// administrator and has broad in access.
	PermitAdmin Permission = 3

	// PermitSelf declares that the operation may only be executed by the
	// software itself and its own (first party) components.
	PermitSelf Permission = 4
)

// AuthenticatorFunc is a function that can be set as the authenticator for the
// API endpoint. If none is set, all requests will have full access.
// The returned AuthToken represents the permissions that the request has.
type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)

// AuthToken represents either a set of required or granted permissions.
// All attributes must be set when the struct is built and must not be changed
// later. Functions may be called at any time.
// The Write permission implicitly also includes reading.
type AuthToken struct {
	Read       Permission
	Write      Permission
	ValidUntil *time.Time
}

type session struct {
	sync.Mutex

	token      *AuthToken
	validUntil time.Time
}

// Expired returns whether the session has expired.
func (sess *session) Expired() bool {
	sess.Lock()
	defer sess.Unlock()

	return time.Now().After(sess.validUntil)
}

// Refresh refreshes the validity of the session with the given TTL.
func (sess *session) Refresh(ttl time.Duration) {
	sess.Lock()
	defer sess.Unlock()

	sess.validUntil = time.Now().Add(ttl)
}

// AuthenticatedHandler defines the handler interface to specify custom
// permission for an API handler. The returned permission is the required
// permission for the request to proceed.
type AuthenticatedHandler interface {
	ReadPermission(*http.Request) Permission
	WritePermission(*http.Request) Permission
}

// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
func SetAuthenticator(fn AuthenticatorFunc) error {
	if module.Online() {
		return ErrAuthenticationImmutable
	}

	if !authFnSet.SetToIf(false, true) {
		return ErrAuthenticationAlreadySet
	}

	authFn = fn
	return nil
}

func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler, readMethod bool) *AuthToken {
	tracer := log.Tracer(r.Context())

	// Get required permission for target handler.
	requiredPermission := PermitSelf
	if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
		if readMethod {
			requiredPermission = authdHandler.ReadPermission(r)
		} else {
			requiredPermission = authdHandler.WritePermission(r)
		}
	}

	// Check if we need to do any authentication at all.
	switch requiredPermission { //nolint:exhaustive
	case NotFound:
		// Not found.
		tracer.Debug("api: no API endpoint registered for this path")
		http.Error(w, "Not found.", http.StatusNotFound)
		return nil
	case NotSupported:
		// A read or write permission can be marked as not supported.
		tracer.Trace("api: authenticated handler reported: not supported")
		http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed)
		return nil
	case PermitAnyone:
		// Don't process permissions, as we don't need them.
		tracer.Tracef("api: granted %s access to public handler", r.RemoteAddr)
		return &AuthToken{
			Read:  PermitAnyone,
			Write: PermitAnyone,
		}
	case Dynamic:
		// Continue processing permissions, but treat as PermitAnyone.
		requiredPermission = PermitAnyone
	}

	// The required permission must match the request permission values after
	// handling the specials.
	if requiredPermission < PermitAnyone || requiredPermission > PermitSelf {
		tracer.Warningf(
			"api: handler returned invalid permission: %s (%d)",
			requiredPermission,
			requiredPermission,
		)
		http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
		return nil
	}

	// Authenticate request.
	token, handled := checkAuth(w, r, requiredPermission > PermitAnyone)
	switch {
	case handled:
		return nil
	case token == nil:
		// Use default permissions.
		token = &AuthToken{
			Read:  PermitAnyone,
			Write: PermitAnyone,
		}
	}

	// Get effective permission for request.
	var requestPermission Permission
	if readMethod {
		requestPermission = token.Read
	} else {
		requestPermission = token.Write
	}

	// Check for valid request permission.
	if requestPermission < PermitAnyone || requestPermission > PermitSelf {
		tracer.Warningf(
			"api: authenticator returned invalid permission: %s (%d)",
			requestPermission,
			requestPermission,
		)
		http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
		return nil
	}

	// Check permission.
	if requestPermission < requiredPermission {
		// If the token is strictly public, return an authentication request.
		if token.Read == PermitAnyone && token.Write == PermitAnyone {
			w.Header().Set(
				"WWW-Authenticate",
				`Bearer realm="Portmaster API" domain="/"`,
			)
			http.Error(w, "Authorization required.", http.StatusUnauthorized)
			return nil
		}

		// Otherwise just inform of insufficient permissions.
		http.Error(w, "Insufficient permissions.", http.StatusForbidden)
		return nil
	}

	tracer.Tracef("api: granted %s access to protected handler", r.RemoteAddr)

	// Make a copy of the AuthToken in order mitigate the handler poisoning the
	// token, as changes would apply to future requests.
	return &AuthToken{
		Read:  token.Read,
		Write: token.Write,
	}
}

func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
	// Return highest possible permissions in dev mode.
	if devMode() {
		return &AuthToken{
			Read:  PermitSelf,
			Write: PermitSelf,
		}, false
	}

	// Database Bridge Access.
	if r.RemoteAddr == endpointBridgeRemoteAddress {
		return &AuthToken{
			Read:  dbCompatibilityPermission,
			Write: dbCompatibilityPermission,
		}, false
	}

	// Check for valid API key.
	token = checkAPIKey(r)
	if token != nil {
		return token, false
	}

	// Check for valid session cookie.
	token = checkSessionCookie(r)
	if token != nil {
		return token, false
	}

	// Check if an external authentication method is available.
	if !authFnSet.IsSet() {
		return nil, false
	}

	// Authenticate externally.
	token, err := authFn(r, server)
	if err != nil {
		// Check if the authentication process failed internally.
		if !errors.Is(err, ErrAPIAccessDeniedMessage) {
			log.Tracer(r.Context()).Errorf("api: authenticator failed: %s", err)
			http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
			return nil, true
		}

		// Return authentication failure message if authentication is required.
		if authRequired {
			log.Tracer(r.Context()).Warningf("api: denying api access from %s", r.RemoteAddr)
			http.Error(w, err.Error(), http.StatusForbidden)
			return nil, true
		}

		return nil, false
	}

	// Abort if no token is returned.
	if token == nil {
		return nil, false
	}

	// Create session cookie for authenticated request.
	err = createSession(w, r, token)
	if err != nil {
		log.Tracer(r.Context()).Warningf("api: failed to create session: %s", err)
	}
	return token, false
}

func checkAPIKey(r *http.Request) *AuthToken {
	// Get API key from request.
	key := r.Header.Get("Authorization")
	if key == "" {
		return nil
	}

	// Parse API key.
	switch {
	case strings.HasPrefix(key, "Bearer "):
		key = strings.TrimPrefix(key, "Bearer ")
	case strings.HasPrefix(key, "Basic "):
		user, pass, _ := r.BasicAuth()
		key = user + pass
	default:
		log.Tracer(r.Context()).Tracef(
			"api: provided api key type %s is unsupported", strings.Split(key, " ")[0],
		)
		return nil
	}

	apiKeysLock.Lock()
	defer apiKeysLock.Unlock()

	// Check if the provided API key exists.
	token, ok := apiKeys[key]
	if !ok {
		log.Tracer(r.Context()).Tracef(
			"api: provided api key %s... is unknown", key[:4],
		)
		return nil
	}

	// Abort if the token is expired.
	if token.ValidUntil != nil && time.Now().After(*token.ValidUntil) {
		log.Tracer(r.Context()).Warningf("api: denying api access from %s using expired token", r.RemoteAddr)
		return nil
	}

	return token
}

func updateAPIKeys(_ context.Context, _ interface{}) error {
	apiKeysLock.Lock()
	defer apiKeysLock.Unlock()

	log.Debug("api: importing possibly updated API keys from config")

	// Delete current keys.
	for k := range apiKeys {
		delete(apiKeys, k)
	}

	// whether or not we found expired API keys that should be removed
	// from the setting
	hasExpiredKeys := false

	// a list of valid API keys. Used when hasExpiredKeys is set to true.
	// in that case we'll update the setting to only contain validAPIKeys
	validAPIKeys := []string{}

	// Parse new keys.
	for _, key := range configuredAPIKeys() {
		u, err := url.Parse(key)
		if err != nil {
			log.Errorf("api: failed to parse configured API key %s: %s", key, err)

			continue
		}

		if u.Path == "" {
			log.Errorf("api: malformed API key %s: missing path section", key)

			continue
		}

		// Create token with default permissions.
		token := &AuthToken{
			Read:  PermitAnyone,
			Write: PermitAnyone,
		}

		// Update with configured permissions.
		q := u.Query()
		// Parse read permission.
		readPermission, err := parseAPIPermission(q.Get("read"))
		if err != nil {
			log.Errorf("api: invalid API key %s: %s", key, err)
			continue
		}
		token.Read = readPermission
		// Parse write permission.
		writePermission, err := parseAPIPermission(q.Get("write"))
		if err != nil {
			log.Errorf("api: invalid API key %s: %s", key, err)
			continue
		}
		token.Write = writePermission

		expireStr := q.Get("expires")
		if expireStr != "" {
			validUntil, err := time.Parse(time.RFC3339, expireStr)
			if err != nil {
				log.Errorf("api: invalid API key %s: %s", key, err)
				continue
			}

			// continue to the next token if this one is already invalid
			if time.Now().After(validUntil) {
				// mark the key as expired so we'll remove it from the setting afterwards
				hasExpiredKeys = true

				continue
			}

			token.ValidUntil = &validUntil
		}

		// Save token.
		apiKeys[u.Path] = token
		validAPIKeys = append(validAPIKeys, key)
	}

	if hasExpiredKeys {
		module.StartLowPriorityMicroTask("api key cleanup", 0, func(ctx context.Context) error {
			if err := config.SetConfigOption(CfgAPIKeys, validAPIKeys); err != nil {
				log.Errorf("api: failed to remove expired API keys: %s", err)
			} else {
				log.Infof("api: removed expired API keys from %s", CfgAPIKeys)
			}

			return nil
		})
	}

	return nil
}

func checkSessionCookie(r *http.Request) *AuthToken {
	// Get session cookie from request.
	c, err := r.Cookie(sessionCookieName)
	if err != nil {
		return nil
	}

	// Check if session cookie is registered.
	sessionsLock.Lock()
	sess, ok := sessions[c.Value]
	sessionsLock.Unlock()
	if !ok {
		log.Tracer(r.Context()).Tracef("api: provided session cookie %s is unknown", c.Value)
		return nil
	}

	// Check if session is still valid.
	if sess.Expired() {
		log.Tracer(r.Context()).Tracef("api: provided session cookie %s has expired", c.Value)
		return nil
	}

	// Refresh session and return.
	sess.Refresh(sessionCookieTTL)
	log.Tracer(r.Context()).Tracef("api: session cookie %s is valid, refreshing", c.Value)
	return sess.token
}

func createSession(w http.ResponseWriter, r *http.Request, token *AuthToken) error {
	// Generate new session key.
	secret, err := rng.Bytes(32) // 256 bit
	if err != nil {
		return err
	}
	sessionKey := base64.RawURLEncoding.EncodeToString(secret)

	// Set token cookie in response.
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    sessionKey,
		Path:     "/",
		HttpOnly: true,
		SameSite: http.SameSiteStrictMode,
	})

	// Create session.
	sess := &session{
		token: token,
	}
	sess.Refresh(sessionCookieTTL)

	// Save session.
	sessionsLock.Lock()
	defer sessionsLock.Unlock()
	sessions[sessionKey] = sess
	log.Tracer(r.Context()).Debug("api: issued session cookie")

	return nil
}

func cleanSessions(_ context.Context, _ *modules.Task) error {
	sessionsLock.Lock()
	defer sessionsLock.Unlock()

	for sessionKey, sess := range sessions {
		if sess.Expired() {
			delete(sessions, sessionKey)
		}
	}

	return nil
}

func deleteSession(sessionKey string) {
	sessionsLock.Lock()
	defer sessionsLock.Unlock()

	delete(sessions, sessionKey)
}

func getEffectiveMethod(r *http.Request) (eMethod string, readMethod bool, ok bool) {
	method := r.Method

	// Get CORS request method if OPTIONS request.
	if r.Method == http.MethodOptions {
		method = r.Header.Get("Access-Control-Request-Method")
		if method == "" {
			return "", false, false
		}
	}

	switch method {
	case http.MethodGet, http.MethodHead:
		return http.MethodGet, true, true
	case http.MethodPost, http.MethodPut, http.MethodDelete:
		return method, false, true
	default:
		return "", false, false
	}
}

func parseAPIPermission(s string) (Permission, error) {
	switch strings.ToLower(s) {
	case "", "anyone":
		return PermitAnyone, nil
	case "user":
		return PermitUser, nil
	case "admin":
		return PermitAdmin, nil
	default:
		return PermitAnyone, fmt.Errorf("invalid permission: %s", s)
	}
}

func (p Permission) String() string {
	switch p {
	case NotSupported:
		return "NotSupported"
	case Dynamic:
		return "Dynamic"
	case PermitAnyone:
		return "PermitAnyone"
	case PermitUser:
		return "PermitUser"
	case PermitAdmin:
		return "PermitAdmin"
	case PermitSelf:
		return "PermitSelf"
	case NotFound:
		return "NotFound"
	default:
		return "Unknown"
	}
}

// Role returns a string representation of the permission role.
func (p Permission) Role() string {
	switch p {
	case PermitAnyone:
		return "Anyone"
	case PermitUser:
		return "User"
	case PermitAdmin:
		return "Admin"
	case PermitSelf:
		return "Self"
	case Dynamic, NotFound, NotSupported:
		return "Invalid"
	default:
		return "Invalid"
	}
}
