// 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 auth

import (
	"errors"
	"maps"
	"strings"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"golang.org/x/exp/slices"
	"google.golang.org/protobuf/encoding/protojson"

	"github.com/livekit/protocol/livekit"
	"github.com/livekit/protocol/logger"
	"github.com/livekit/protocol/utils"
)

type RoomConfiguration livekit.RoomConfiguration

var tokenMarshaler = protojson.MarshalOptions{
	EmitDefaultValues: false,
}

var ErrSensitiveCredentials = errors.New("room configuration should not contain sensitive credentials")

func (c *RoomConfiguration) Clone() *RoomConfiguration {
	if c == nil {
		return nil
	}
	return (*RoomConfiguration)(utils.CloneProto((*livekit.RoomConfiguration)(c)))
}

func (c *RoomConfiguration) MarshalJSON() ([]byte, error) {
	return tokenMarshaler.Marshal((*livekit.RoomConfiguration)(c))
}

func (c *RoomConfiguration) UnmarshalJSON(data []byte) error {
	return protojson.Unmarshal(data, (*livekit.RoomConfiguration)(c))
}

// CheckCredentials checks if the room configuration contains sensitive credentials
// and returns an error if it does.
//
// This is used to prevent sensitive credentials from being leaked to the client.
// It is not used to validate the credentials themselves, as that is done by the
// egress service.
func (c *RoomConfiguration) CheckCredentials() error {
	if c.Egress == nil {
		return nil
	}

	if c.Egress.Participant != nil {
		for _, output := range c.Egress.Participant.FileOutputs {
			if err := checkOutputForCredentials(output.Output); err != nil {
				return err
			}
		}
		for _, output := range c.Egress.Participant.SegmentOutputs {
			if err := checkOutputForCredentials(output.Output); err != nil {
				return err
			}
		}
	}
	if c.Egress.Room != nil {
		for _, output := range c.Egress.Room.FileOutputs {
			if err := checkOutputForCredentials(output.Output); err != nil {
				return err
			}
		}
		for _, output := range c.Egress.Room.SegmentOutputs {
			if err := checkOutputForCredentials(output.Output); err != nil {
				return err
			}
		}
		for _, output := range c.Egress.Room.ImageOutputs {
			if err := checkOutputForCredentials(output.Output); err != nil {
				return err
			}
		}
		if len(c.Egress.Room.StreamOutputs) > 0 {
			// do not leak stream key
			return ErrSensitiveCredentials
		}
	}
	if c.Egress.Tracks != nil {
		if err := checkOutputForCredentials(c.Egress.Tracks.Output); err != nil {
			return err
		}
	}
	return nil
}

func checkOutputForCredentials(output any) error {
	if output == nil {
		return nil
	}

	switch msg := output.(type) {
	case *livekit.EncodedFileOutput_S3:
		if msg.S3.Secret != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.SegmentedFileOutput_S3:
		if msg.S3.Secret != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.AutoTrackEgress_S3:
		if msg.S3.Secret != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.EncodedFileOutput_Gcp:
		if msg.Gcp.Credentials != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.SegmentedFileOutput_Gcp:
		if msg.Gcp.Credentials != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.AutoTrackEgress_Gcp:
		if msg.Gcp.Credentials != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.EncodedFileOutput_Azure:
		if msg.Azure.AccountKey != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.SegmentedFileOutput_Azure:
		if msg.Azure.AccountKey != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.AutoTrackEgress_Azure:
		if msg.Azure.AccountKey != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.EncodedFileOutput_AliOSS:
		if msg.AliOSS.Secret != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.SegmentedFileOutput_AliOSS:
		if msg.AliOSS.Secret != "" {
			return ErrSensitiveCredentials
		}
	case *livekit.AutoTrackEgress_AliOSS:
		if msg.AliOSS.Secret != "" {
			return ErrSensitiveCredentials
		}
	}
	return nil
}

type ClaimGrants struct {
	Identity      string              `json:"identity,omitempty"`
	Name          string              `json:"name,omitempty"`
	Kind          string              `json:"kind,omitempty"`
	KindDetails   []string            `json:"kindDetails,omitempty"`
	Video         *VideoGrant         `json:"video,omitempty"`
	SIP           *SIPGrant           `json:"sip,omitempty"`
	Agent         *AgentGrant         `json:"agent,omitempty"`
	Inference     *InferenceGrant     `json:"inference,omitempty"`
	Observability *ObservabilityGrant `json:"observability,omitempty"`
	// Room configuration to use if this participant initiates the room
	RoomConfig *RoomConfiguration `json:"roomConfig,omitempty"`
	// Cloud-only, config preset to use
	// when both room and roomPreset are set, parameters in room overrides the preset
	RoomPreset string `json:"roomPreset,omitempty"`
	// for verifying integrity of the message body
	Sha256   string `json:"sha256,omitempty"`
	Metadata string `json:"metadata,omitempty"`
	// Key/value attributes to attach to the participant
	Attributes map[string]string `json:"attributes,omitempty"`
}

func (c *ClaimGrants) SetParticipantKind(kind livekit.ParticipantInfo_Kind) {
	c.Kind = kindFromProto(kind)
}

func (c *ClaimGrants) GetParticipantKind() livekit.ParticipantInfo_Kind {
	return kindToProto(c.Kind)
}

func (c *ClaimGrants) SetKindDetail(details ...livekit.ParticipantInfo_KindDetail) {
	c.KindDetails = kindDetailsFromProto(details)
}

func (c *ClaimGrants) GetKindDetails() []livekit.ParticipantInfo_KindDetail {
	return kindDetailsToProto(c.KindDetails)
}

func (c *ClaimGrants) GetRoomConfiguration() *livekit.RoomConfiguration {
	if c.RoomConfig == nil {
		return nil
	}
	return (*livekit.RoomConfiguration)(c.RoomConfig)
}

func (c *ClaimGrants) Clone() *ClaimGrants {
	if c == nil {
		return nil
	}

	clone := *c
	clone.Video = c.Video.Clone()
	clone.SIP = c.SIP.Clone()
	clone.Agent = c.Agent.Clone()
	clone.Inference = c.Inference.Clone()
	clone.Observability = c.Observability.Clone()
	clone.Attributes = maps.Clone(c.Attributes)
	clone.RoomConfig = c.RoomConfig.Clone()
	if len(c.KindDetails) > 0 {
		clone.KindDetails = append([]string{}, c.KindDetails...)
	}

	return &clone
}

func (c *ClaimGrants) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if c == nil {
		return nil
	}

	e.AddString("Identity", c.Identity)
	e.AddString("Kind", c.Kind)
	zap.Strings("KindDetails", c.KindDetails).AddTo(e)
	e.AddObject("Video", c.Video)
	e.AddObject("SIP", c.SIP)
	e.AddObject("Agent", c.Agent)
	e.AddObject("Inference", c.Inference)
	e.AddObject("Observability", c.Observability)
	e.AddObject("RoomConfig", logger.Proto((*livekit.RoomConfiguration)(c.RoomConfig)))
	e.AddString("RoomPreset", c.RoomPreset)
	return nil
}

// -------------------------------------------------------------

type VideoGrant struct {
	// actions on rooms
	RoomCreate bool `json:"roomCreate,omitempty"`
	RoomList   bool `json:"roomList,omitempty"`
	RoomRecord bool `json:"roomRecord,omitempty"`

	// actions on a particular room
	RoomAdmin bool   `json:"roomAdmin,omitempty"`
	RoomJoin  bool   `json:"roomJoin,omitempty"`
	Room      string `json:"room,omitempty"`

	// permissions within a room, if none of the permissions are set explicitly
	// it will be granted with all publish and subscribe permissions
	CanPublish     *bool `json:"canPublish,omitempty"`
	CanSubscribe   *bool `json:"canSubscribe,omitempty"`
	CanPublishData *bool `json:"canPublishData,omitempty"`
	// TrackSource types that a participant may publish.
	// When set, it supersedes CanPublish. Only sources explicitly set here can be published
	CanPublishSources []string `json:"canPublishSources,omitempty"` // keys keep track of each source
	// by default, a participant is not allowed to update its own metadata
	CanUpdateOwnMetadata *bool `json:"canUpdateOwnMetadata,omitempty"`

	// actions on ingresses
	IngressAdmin bool `json:"ingressAdmin,omitempty"` // applies to all ingress

	// participant is not visible to other participants
	Hidden bool `json:"hidden,omitempty"`
	// indicates to the room that current participant is a recorder
	Recorder bool `json:"recorder,omitempty"`
	// indicates that the holder can register as an Agent framework worker
	Agent bool `json:"agent,omitempty"`

	// if a participant can subscribe to metrics
	CanSubscribeMetrics *bool `json:"canSubscribeMetrics,omitempty"`

	// destination room which this participant can forward to
	DestinationRoom string `json:"destinationRoom,omitempty"`
}

func (v *VideoGrant) SetCanPublish(val bool) {
	v.CanPublish = &val
}

func (v *VideoGrant) SetCanPublishData(val bool) {
	v.CanPublishData = &val
}

func (v *VideoGrant) SetCanSubscribe(val bool) {
	v.CanSubscribe = &val
}

func (v *VideoGrant) SetCanPublishSources(sources []livekit.TrackSource) {
	v.CanPublishSources = make([]string, 0, len(sources))
	for _, s := range sources {
		v.CanPublishSources = append(v.CanPublishSources, sourceToString(s))
	}
}

func (v *VideoGrant) SetCanUpdateOwnMetadata(val bool) {
	v.CanUpdateOwnMetadata = &val
}

func (v *VideoGrant) SetCanSubscribeMetrics(val bool) {
	v.CanSubscribeMetrics = &val
}

func (v *VideoGrant) GetCanPublish() bool {
	if v.CanPublish == nil {
		return true
	}
	return *v.CanPublish
}

func (v *VideoGrant) GetCanPublishSource(source livekit.TrackSource) bool {
	if !v.GetCanPublish() {
		return false
	}
	// don't differentiate between nil and unset, since that distinction doesn't survive serialization
	if len(v.CanPublishSources) == 0 {
		return true
	}
	sourceStr := sourceToString(source)
	for _, s := range v.CanPublishSources {
		if s == sourceStr {
			return true
		}
	}
	return false
}

func (v *VideoGrant) GetCanPublishSources() []livekit.TrackSource {
	if len(v.CanPublishSources) == 0 {
		return nil
	}

	sources := make([]livekit.TrackSource, 0, len(v.CanPublishSources))
	for _, s := range v.CanPublishSources {
		sources = append(sources, sourceToProto(s))
	}
	return sources
}

func (v *VideoGrant) GetCanPublishData() bool {
	if v.CanPublishData == nil {
		return v.GetCanPublish()
	}
	return *v.CanPublishData
}

func (v *VideoGrant) GetCanSubscribe() bool {
	if v.CanSubscribe == nil {
		return true
	}
	return *v.CanSubscribe
}

func (v *VideoGrant) GetCanUpdateOwnMetadata() bool {
	if v.CanUpdateOwnMetadata == nil {
		return false
	}
	return *v.CanUpdateOwnMetadata
}

func (v *VideoGrant) GetCanSubscribeMetrics() bool {
	if v.CanSubscribeMetrics == nil {
		return false
	}
	return *v.CanSubscribeMetrics
}

func (v *VideoGrant) MatchesPermission(permission *livekit.ParticipantPermission) bool {
	if permission == nil {
		return false
	}

	if v.GetCanPublish() != permission.CanPublish {
		return false
	}
	if v.GetCanPublishData() != permission.CanPublishData {
		return false
	}
	if v.GetCanSubscribe() != permission.CanSubscribe {
		return false
	}
	if v.GetCanUpdateOwnMetadata() != permission.CanUpdateMetadata {
		return false
	}
	if v.Hidden != permission.Hidden {
		return false
	}
	if v.Recorder != permission.Recorder {
		return false
	}
	if v.Agent != permission.Agent {
		return false
	}
	if !slices.Equal(v.GetCanPublishSources(), permission.CanPublishSources) {
		return false
	}
	if v.GetCanSubscribeMetrics() != permission.CanSubscribeMetrics {
		return false
	}

	return true
}

func (v *VideoGrant) UpdateFromPermission(permission *livekit.ParticipantPermission) {
	if permission == nil {
		return
	}

	v.SetCanPublish(permission.CanPublish)
	v.SetCanPublishData(permission.CanPublishData)
	v.SetCanPublishSources(permission.CanPublishSources)
	v.SetCanSubscribe(permission.CanSubscribe)
	v.SetCanUpdateOwnMetadata(permission.CanUpdateMetadata)
	v.Hidden = permission.Hidden
	v.Recorder = permission.Recorder
	v.Agent = permission.Agent
	v.SetCanSubscribeMetrics(permission.CanSubscribeMetrics)
}

func (v *VideoGrant) ToPermission() *livekit.ParticipantPermission {
	return &livekit.ParticipantPermission{
		CanPublish:          v.GetCanPublish(),
		CanPublishData:      v.GetCanPublishData(),
		CanSubscribe:        v.GetCanSubscribe(),
		CanPublishSources:   v.GetCanPublishSources(),
		CanUpdateMetadata:   v.GetCanUpdateOwnMetadata(),
		Hidden:              v.Hidden,
		Recorder:            v.Recorder,
		Agent:               v.Agent,
		CanSubscribeMetrics: v.GetCanSubscribeMetrics(),
	}
}

func (v *VideoGrant) Clone() *VideoGrant {
	if v == nil {
		return nil
	}

	clone := *v

	if v.CanPublish != nil {
		canPublish := *v.CanPublish
		clone.CanPublish = &canPublish
	}

	if v.CanSubscribe != nil {
		canSubscribe := *v.CanSubscribe
		clone.CanSubscribe = &canSubscribe
	}

	if v.CanPublishData != nil {
		canPublishData := *v.CanPublishData
		clone.CanPublishData = &canPublishData
	}

	if v.CanPublishSources != nil {
		clone.CanPublishSources = make([]string, len(v.CanPublishSources))
		copy(clone.CanPublishSources, v.CanPublishSources)
	}

	if v.CanUpdateOwnMetadata != nil {
		canUpdateOwnMetadata := *v.CanUpdateOwnMetadata
		clone.CanUpdateOwnMetadata = &canUpdateOwnMetadata
	}

	return &clone
}

func (v *VideoGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if v == nil {
		return nil
	}

	logBoolPtr := func(prop string, val *bool) {
		if val == nil {
			e.AddString(prop, "not-set")
		} else {
			e.AddBool(prop, *val)
		}
	}

	logBoolPtr("RoomCreate", &v.RoomCreate)
	logBoolPtr("RoomList", &v.RoomList)
	logBoolPtr("RoomRecord", &v.RoomRecord)

	logBoolPtr("RoomAdmin", &v.RoomAdmin)
	logBoolPtr("RoomJoin", &v.RoomJoin)
	e.AddString("Room", v.Room)

	logBoolPtr("CanPublish", v.CanPublish)
	logBoolPtr("CanSubscribe", v.CanSubscribe)
	logBoolPtr("CanPublishData", v.CanPublishData)
	e.AddArray("CanPublishSources", logger.StringSlice(v.CanPublishSources))
	logBoolPtr("CanUpdateOwnMetadata", v.CanUpdateOwnMetadata)

	logBoolPtr("IngressAdmin", &v.IngressAdmin)

	logBoolPtr("Hidden", &v.Hidden)
	logBoolPtr("Recorder", &v.Recorder)
	logBoolPtr("Agent", &v.Agent)

	logBoolPtr("CanSubscribeMetrics", v.CanSubscribeMetrics)
	e.AddString("DestinationRoom", v.DestinationRoom)
	return nil
}

// ----------------------------------------------------------------

type SIPGrant struct {
	// Admin grants access to all SIP features.
	Admin bool `json:"admin,omitempty"`

	// Call allows making outbound SIP calls.
	Call bool `json:"call,omitempty"`
}

func (s *SIPGrant) Clone() *SIPGrant {
	if s == nil {
		return nil
	}

	clone := *s

	return &clone
}

func (s *SIPGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if s == nil {
		return nil
	}

	e.AddBool("Admin", s.Admin)
	e.AddBool("Call", s.Call)
	return nil
}

// ------------------------------------------------------------------

// ------------------------------------------------------------------

type AgentGrant struct {
	// Admin grants to create/update/delete Cloud Agents.
	Admin bool `json:"admin,omitempty"`
}

func (s *AgentGrant) Clone() *AgentGrant {
	if s == nil {
		return nil
	}

	clone := *s

	return &clone
}

func (s *AgentGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if s == nil {
		return nil
	}

	e.AddBool("Admin", s.Admin)
	return nil
}

// ------------------------------------------------------------------

type InferenceGrant struct {
	// Perform grants to all inference features (LLM, STT, TTS)
	Perform bool `json:"perform,omitempty"`
}

func (s *InferenceGrant) Clone() *InferenceGrant {
	if s == nil {
		return nil
	}

	clone := *s

	return &clone
}

func (s *InferenceGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if s == nil {
		return nil
	}

	e.AddBool("Perform", s.Perform)
	return nil
}

// ------------------------------------------------------------------

type ObservabilityGrant struct {
	// Write grants to publish observability data
	Write bool `json:"write,omitempty"`
}

func (s *ObservabilityGrant) Clone() *ObservabilityGrant {
	if s == nil {
		return nil
	}

	clone := *s

	return &clone
}

func (s *ObservabilityGrant) MarshalLogObject(e zapcore.ObjectEncoder) error {
	if s == nil {
		return nil
	}

	e.AddBool("Write", s.Write)
	return nil
}

// ------------------------------------------------------------------

func sourceToString(source livekit.TrackSource) string {
	return strings.ToLower(source.String())
}

func sourceToProto(sourceStr string) livekit.TrackSource {
	if val, ok := livekit.TrackSource_value[strings.ToUpper(sourceStr)]; ok {
		return livekit.TrackSource(val)
	}
	return livekit.TrackSource_UNKNOWN
}

func kindFromProto(source livekit.ParticipantInfo_Kind) string {
	return strings.ToLower(source.String())
}

func kindToProto(sourceStr string) livekit.ParticipantInfo_Kind {
	if val, ok := livekit.ParticipantInfo_Kind_value[strings.ToUpper(sourceStr)]; ok {
		return livekit.ParticipantInfo_Kind(val)
	}
	return livekit.ParticipantInfo_STANDARD
}

func kindDetailsFromProto(details []livekit.ParticipantInfo_KindDetail) []string {
	result := make([]string, 0, len(details))
	for _, d := range details {
		result = append(result, strings.ToLower(d.String()))
	}
	return result
}

func kindDetailsToProto(details []string) []livekit.ParticipantInfo_KindDetail {
	result := make([]livekit.ParticipantInfo_KindDetail, 0, len(details))
	for _, d := range details {
		if val, ok := livekit.ParticipantInfo_KindDetail_value[strings.ToUpper(d)]; ok {
			result = append(result, livekit.ParticipantInfo_KindDetail(val))
		}
	}
	return result
}
