/*
 * Copyright 2018-2020 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 describes a mapping from another account into this one
type Import struct {
	Name string `json:"name,omitempty"`
	// Subject field in an import is always from the perspective of the
	// initial publisher - in the case of a stream it is the account owning
	// the stream (the exporter), and in the case of a service it is the
	// account making the request (the importer).
	Subject Subject `json:"subject,omitempty"`
	Account string  `json:"account,omitempty"`
	Token   string  `json:"token,omitempty"`
	// Deprecated: use LocalSubject instead
	// To field in an import is always from the perspective of the subscriber
	// in the case of a stream it is the client of the stream (the importer),
	// from the perspective of a service, it is the subscription waiting for
	// requests (the exporter). If the field is empty, it will default to the
	// value in the Subject field.
	To Subject `json:"to,omitempty"`
	// Local subject used to subscribe (for streams) and publish (for services) to.
	// This value only needs setting if you want to change the value of Subject.
	// If the value of Subject ends in > then LocalSubject needs to end in > as well.
	// LocalSubject can contain $<number> wildcard references where number references the nth wildcard in Subject.
	// The sum of wildcard reference and * tokens needs to match the number of * token in Subject.
	LocalSubject RenamingSubject `json:"local_subject,omitempty"`
	Type         ExportType      `json:"type,omitempty"`
	Share        bool            `json:"share,omitempty"`
	AllowTrace   bool            `json:"allow_trace,omitempty"`
}

// IsService returns true if the import is of type service
func (i *Import) IsService() bool {
	return i.Type == Service
}

// IsStream returns true if the import is of type stream
func (i *Import) IsStream() bool {
	return i.Type == Stream
}

// Returns the value of To without triggering the deprecation warning for a read
func (i *Import) GetTo() string {
	return string(i.To)
}

// Validate checks if an import is valid for the wrapping account
func (i *Import) Validate(actPubKey string, vr *ValidationResults) {
	if i == nil {
		vr.AddError("null import is not allowed")
		return
	}
	if !i.IsService() && !i.IsStream() {
		vr.AddError("invalid import type: %q", i.Type)
	}
	if i.IsService() && i.AllowTrace {
		vr.AddError("AllowTrace only valid for stream import")
	}

	if i.Account == "" {
		vr.AddError("account to import from is not specified")
	}

	if i.GetTo() != "" {
		vr.AddWarning("the field to has been deprecated (use LocalSubject instead)")
	}

	i.Subject.Validate(vr)
	if i.LocalSubject != "" {
		i.LocalSubject.Validate(i.Subject, vr)
		if i.To != "" {
			vr.AddError("Local Subject replaces To")
		}
	}

	if i.Share && !i.IsService() {
		vr.AddError("sharing information (for latency tracking) is only valid for services: %q", i.Subject)
	}
	var act *ActivationClaims

	if i.Token != "" {
		var err error
		act, err = DecodeActivationClaims(i.Token)
		if err != nil {
			vr.AddError("import %q contains an invalid activation token", i.Subject)
		}
	}

	if act != nil {
		if !(act.Issuer == i.Account || act.IssuerAccount == i.Account) {
			vr.AddError("activation token doesn't match account for import %q", i.Subject)
		}
		if act.ClaimsData.Subject != actPubKey {
			vr.AddError("activation token doesn't match account it is being included in, %q", i.Subject)
		}
		if act.ImportType != i.Type {
			vr.AddError("mismatch between token import type %s and type of import %s", act.ImportType, i.Type)
		}
		act.validateWithTimeChecks(vr, false)
		subj := i.Subject
		if i.IsService() && i.To != "" {
			subj = i.To
		}
		if !subj.IsContainedIn(act.ImportSubject) {
			vr.AddError("activation token import subject %q doesn't match import %q", act.ImportSubject, i.Subject)
		}
	}
}

// Imports is a list of import structs
type Imports []*Import

// Validate checks if an import is valid for the wrapping account
func (i *Imports) Validate(acctPubKey string, vr *ValidationResults) {
	// Group subjects by account to check for overlaps only within the same account
	subsByAcct := make(map[string]map[Subject]struct{}, len(*i))
	for _, v := range *i {
		if v == nil {
			vr.AddError("null import is not allowed")
			continue
		}
		if v.Type == Service {
			sub := v.To
			if sub == "" {
				sub = v.LocalSubject.ToSubject()
			}
			if sub == "" {
				sub = v.Subject
			}
			// Check for overlapping subjects only within the same account
			for subOther := range subsByAcct[v.Account] {
				if sub.IsContainedIn(subOther) || subOther.IsContainedIn(sub) {
					vr.AddError("overlapping subject namespace for %q and %q in same account %q", sub, subOther, v.Account)
				}
			}
			if subsByAcct[v.Account] == nil {
				subsByAcct[v.Account] = make(map[Subject]struct{}, len(*i))
			}
			if _, ok := subsByAcct[v.Account][sub]; ok {
				vr.AddError("overlapping subject namespace for %q in account %q", sub, v.Account)
			}
			subsByAcct[v.Account][sub] = struct{}{}
		}
		v.Validate(acctPubKey, vr)
	}
}

// Add is a simple way to add imports
func (i *Imports) Add(a ...*Import) {
	*i = append(*i, a...)
}

func (i Imports) Len() int {
	return len(i)
}

func (i Imports) Swap(j, k int) {
	i[j], i[k] = i[k], i[j]
}

func (i Imports) Less(j, k int) bool {
	return i[j].Subject < i[k].Subject
}
