/*
 * Copyright 2020-2024 The NATS Authors
 * 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 jwt

import (
	"encoding/json"
	"errors"
	"fmt"
	"sort"

	"github.com/nats-io/nkeys"
)

type Scope interface {
	SigningKey() string
	ValidateScopedSigner(claim Claims) error
	Validate(vr *ValidationResults)
}

type ScopeType int

const (
	UserScopeType ScopeType = iota + 1
)

func (t ScopeType) String() string {
	switch t {
	case UserScopeType:
		return "user_scope"
	}
	return "unknown"
}

func (t *ScopeType) MarshalJSON() ([]byte, error) {
	switch *t {
	case UserScopeType:
		return []byte("\"user_scope\""), nil
	}
	return nil, fmt.Errorf("unknown scope type %q", t)
}

func (t *ScopeType) UnmarshalJSON(b []byte) error {
	var s string
	err := json.Unmarshal(b, &s)
	if err != nil {
		return err
	}
	switch s {
	case "user_scope":
		*t = UserScopeType
		return nil
	}
	return fmt.Errorf("unknown scope type %q", t)
}

type UserScope struct {
	Kind        ScopeType            `json:"kind"`
	Key         string               `json:"key"`
	Role        string               `json:"role"`
	Template    UserPermissionLimits `json:"template"`
	Description string               `json:"description"`
}

func NewUserScope() *UserScope {
	var s UserScope
	s.Kind = UserScopeType
	s.Template.NatsLimits = NatsLimits{NoLimit, NoLimit, NoLimit}
	return &s
}

func (us UserScope) SigningKey() string {
	return us.Key
}

func (us UserScope) Validate(vr *ValidationResults) {
	if !nkeys.IsValidPublicAccountKey(us.Key) {
		vr.AddError("%s is not an account public key", us.Key)
	}
}

func (us UserScope) ValidateScopedSigner(c Claims) error {
	uc, ok := c.(*UserClaims)
	if !ok {
		return fmt.Errorf("not an user claim - scoped signing key requires user claim")
	}
	if uc.Claims().Issuer != us.Key {
		return errors.New("issuer not the scoped signer")
	}
	if !uc.HasEmptyPermissions() {
		return errors.New("scoped users require no permissions or limits set")
	}
	return nil
}

// SigningKeys is a map keyed by a public account key
type SigningKeys map[string]Scope

func (sk SigningKeys) Validate(vr *ValidationResults) {
	for k, v := range sk {
		// regular signing keys won't have a scope
		if v != nil {
			v.Validate(vr)
		} else {
			if !nkeys.IsValidPublicAccountKey(k) {
				vr.AddError("%q is not a valid account signing key", k)
			}
		}
	}
}

// MarshalJSON serializes the scoped signing keys as an array
func (sk *SigningKeys) MarshalJSON() ([]byte, error) {
	if sk == nil {
		return nil, nil
	}

	keys := sk.Keys()
	sort.Strings(keys)

	var a []interface{}
	for _, k := range keys {
		if (*sk)[k] != nil {
			a = append(a, (*sk)[k])
		} else {
			a = append(a, k)
		}
	}
	return json.Marshal(a)
}

func (sk *SigningKeys) UnmarshalJSON(data []byte) error {
	if *sk == nil {
		*sk = make(SigningKeys)
	}
	// read an array - we can have a string or an map
	var a []interface{}
	if err := json.Unmarshal(data, &a); err != nil {
		return err
	}
	for _, i := range a {
		switch v := i.(type) {
		case string:
			(*sk)[v] = nil
		case map[string]interface{}:
			d, err := json.Marshal(v)
			if err != nil {
				return err
			}
			switch v["kind"] {
			case UserScopeType.String():
				us := NewUserScope()
				if err := json.Unmarshal(d, &us); err != nil {
					return err
				}
				(*sk)[us.Key] = us
			default:
				return fmt.Errorf("unknown signing key scope %q", v["type"])
			}
		}
	}
	return nil
}

func (sk SigningKeys) Keys() []string {
	var keys []string
	for k := range sk {
		keys = append(keys, k)
	}
	return keys
}

// GetScope returns nil if the key is not associated
func (sk SigningKeys) GetScope(k string) (Scope, bool) {
	v, ok := sk[k]
	if !ok {
		return nil, false
	}
	return v, true
}

func (sk SigningKeys) Contains(k string) bool {
	_, ok := sk[k]
	return ok
}

func (sk SigningKeys) Add(keys ...string) {
	for _, k := range keys {
		sk[k] = nil
	}
}

func (sk SigningKeys) AddScopedSigner(s Scope) {
	sk[s.SigningKey()] = s
}

func (sk SigningKeys) Remove(keys ...string) {
	for _, k := range keys {
		delete(sk, k)
	}
}
