// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

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

	"github.com/twitchtv/twirp"

	"github.com/livekit/protocol/auth"
	"github.com/livekit/protocol/livekit"
)

const (
	authorizationHeader = "Authorization"
	bearerPrefix        = "Bearer "
	accessTokenParam    = "access_token"
)

type grantsKey struct{}

type grantsValue struct {
	claims    *auth.ClaimGrants
	apiKey    string
	expiresAt time.Time
}

var (
	ErrPermissionDenied          = errors.New("permissions denied")
	ErrMissingAuthorization      = errors.New("invalid authorization header. Must start with " + bearerPrefix)
	ErrInvalidAuthorizationToken = errors.New("invalid authorization token")
	ErrInvalidAPIKey             = errors.New("invalid API key")
)

// authentication middleware
type APIKeyAuthMiddleware struct {
	provider auth.KeyProvider
}

func NewAPIKeyAuthMiddleware(provider auth.KeyProvider) *APIKeyAuthMiddleware {
	return &APIKeyAuthMiddleware{
		provider: provider,
	}
}

func (m *APIKeyAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	if r.URL != nil && (r.URL.Path == "/rtc/validate" || r.URL.Path == "/rtc/v1/validate") {
		w.Header().Set("Access-Control-Allow-Origin", "*")
	}

	authHeader := r.Header.Get(authorizationHeader)
	var authToken string

	if authHeader != "" {
		if !strings.HasPrefix(authHeader, bearerPrefix) {
			HandleError(w, r, http.StatusUnauthorized, ErrMissingAuthorization)
			return
		}

		authToken = authHeader[len(bearerPrefix):]
	} else {
		// attempt to find from request header
		authToken = r.FormValue(accessTokenParam)
	}

	if authToken != "" {
		v, err := auth.ParseAPIToken(authToken)
		if err != nil {
			HandleError(w, r, http.StatusUnauthorized, ErrInvalidAuthorizationToken)
			return
		}

		secret := m.provider.GetSecret(v.APIKey())
		if secret == "" {
			HandleError(w, r, http.StatusUnauthorized, ErrInvalidAPIKey, "apiKey", v.APIKey())
			return
		}

		claims, grants, err := v.Verify(secret)
		if err != nil {
			HandleError(w, r, http.StatusUnauthorized, fmt.Errorf("%w: %s", ErrInvalidAuthorizationToken, err.Error()))
			return
		}

		var expiresAt time.Time
		if claims != nil && claims.ExpiresAt != nil {
			expiresAt = claims.ExpiresAt.Time
		}

		// set grants in context
		ctx := r.Context()
		r = r.WithContext(context.WithValue(ctx, grantsKey{}, &grantsValue{
			claims:    grants,
			apiKey:    v.APIKey(),
			expiresAt: expiresAt,
		}))
	}

	next.ServeHTTP(w, r)
}

func WithAPIKey(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
	return context.WithValue(ctx, grantsKey{}, &grantsValue{
		claims: grants,
		apiKey: apiKey,
	})
}

func GetGrants(ctx context.Context) *auth.ClaimGrants {
	val := ctx.Value(grantsKey{})
	v, ok := val.(*grantsValue)
	if !ok {
		return nil
	}
	return v.claims
}

func GetTokenExpiresAt(ctx context.Context) time.Time {
	val := ctx.Value(grantsKey{})
	v, ok := val.(*grantsValue)
	if !ok {
		return time.Time{}
	}
	return v.expiresAt
}

func GetAPIKey(ctx context.Context) string {
	val := ctx.Value(grantsKey{})
	v, ok := val.(*grantsValue)
	if !ok {
		return ""
	}
	return v.apiKey
}

func WithGrants(ctx context.Context, grants *auth.ClaimGrants, apiKey string) context.Context {
	return WithGrantsExpiry(ctx, grants, apiKey, time.Time{})
}

func WithGrantsExpiry(ctx context.Context, grants *auth.ClaimGrants, apiKey string, expiresAt time.Time) context.Context {
	return context.WithValue(ctx, grantsKey{}, &grantsValue{
		claims:    grants,
		apiKey:    apiKey,
		expiresAt: expiresAt,
	})
}

func SetAuthorizationToken(r *http.Request, token string) {
	r.Header.Set(authorizationHeader, bearerPrefix+token)
}

func EnsureJoinPermission(ctx context.Context) (name livekit.RoomName, err error) {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil {
		err = ErrPermissionDenied
		return
	}

	if claims.Video.RoomJoin {
		name = livekit.RoomName(claims.Video.Room)
	} else {
		err = ErrPermissionDenied
	}
	return
}

func EnsureAdminPermission(ctx context.Context, room livekit.RoomName) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil {
		return ErrPermissionDenied
	}

	if !claims.Video.RoomAdmin || room != livekit.RoomName(claims.Video.Room) {
		return ErrPermissionDenied
	}

	return nil
}

func EnsureCreatePermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil || !claims.Video.RoomCreate {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureListPermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil || !claims.Video.RoomList {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureRecordPermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil || !claims.Video.RoomRecord {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureIngressAdminPermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil || !claims.Video.IngressAdmin {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureSIPAdminPermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.SIP == nil || !claims.SIP.Admin {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureSIPCallPermission(ctx context.Context) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.SIP == nil || !claims.SIP.Call {
		return ErrPermissionDenied
	}
	return nil
}

func EnsureDestRoomPermission(ctx context.Context, source livekit.RoomName, destination livekit.RoomName) error {
	claims := GetGrants(ctx)
	if claims == nil || claims.Video == nil {
		return ErrPermissionDenied
	}

	if !claims.Video.RoomAdmin || source != livekit.RoomName(claims.Video.Room) || destination != livekit.RoomName(claims.Video.DestinationRoom) {
		return ErrPermissionDenied
	}

	return nil
}

// wraps authentication errors around Twirp
func twirpAuthError(err error) error {
	return twirp.NewError(twirp.Unauthenticated, err.Error())
}
