package regopolicyinterpreter

import (
	"bytes"
	"context"
	_ "embed"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"reflect"
	"slices"
	"sync"

	"github.com/open-policy-agent/opa/v1/ast"
	"github.com/open-policy-agent/opa/v1/rego"
	"github.com/open-policy-agent/opa/v1/storage/inmem"
	"github.com/open-policy-agent/opa/v1/topdown"
	"github.com/pkg/errors"
)

type LogLevel int

const (
	LogNone LogLevel = iota
	// Logs the output of Rego print() statements in the policy
	LogInfo
	// Logs the result objects returned from each query
	LogResults
	// Logs the full metadata state after each query
	LogMetadata
)

type RegoPolicyInterpreter struct {
	// Mutex to ensure query objects cannot change during query execution
	dataAndModulesMutex sync.Mutex
	// Rego which describes policy behavior (see above)
	code string
	// Rego data namespace
	data map[string]interface{}
	// Modules
	modules map[string]*RegoModule
	// Compiled modules
	compiledModules *ast.Compiler
	// Logging
	logLevel       LogLevel
	logFile        *os.File
	infoLogger     *log.Logger
	resultsLogger  *log.Logger
	metadataLogger *log.Logger
}

type RegoModule struct {
	// The Rego namespace of the module
	Namespace string
	// The feed from which the module was obtained
	Feed string
	// The issuer of the module
	Issuer string
	// The module Rego code
	Code string
}

/* See README for more details on Metadata */

// This is conceptually a map[string](regoMetadataMap|regoMetadataSet)
type regoMetadata map[string]interface{}

const metadataRootKey = "metadata"
const metadataOperationsKey = "metadata"

type regoMetadataAction string
type regoMetadataType string

type regoMetadataMap map[string]interface{}

// This uses a pointer to a slice so that we can update it after getting a
// reference
type regoMetadataSet []interface{}

const (
	// A string to anything map
	metadataTypeMap regoMetadataType = "map"

	// A list of anything that we treat as a set
	metadataTypeSet regoMetadataType = "set"
)

const (
	metadataAdd    regoMetadataAction = "add"
	metadataUpdate regoMetadataAction = "update"
	metadataRemove regoMetadataAction = "remove"
)

type regoMetadataOperation struct {
	Action regoMetadataAction `json:"action"`

	// Defaults to map
	Type regoMetadataType `json:"type"`

	Name  string      `json:"name"`
	Key   string      `json:"key"`
	Value interface{} `json:"value"`
}

// The result from a policy query
type RegoQueryResult map[string]interface{}

// An immutable, saved copy of the metadata state.
type SavedMetadata struct {
	metadataRoot regoMetadata
}

// deep copy for an object
func copyObject(data map[string]interface{}) (map[string]interface{}, error) {
	objJSON, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}

	var objCopy map[string]interface{}
	err = json.Unmarshal(objJSON, &objCopy)
	if err != nil {
		return nil, err
	}

	return objCopy, nil
}

// deep copy for a value
func copyValue(value interface{}) (interface{}, error) {
	valueJSON, err := json.Marshal(value)
	if err != nil {
		return nil, err
	}

	var valueCopy interface{}
	err = json.Unmarshal(valueJSON, &valueCopy)
	if err != nil {
		return nil, err
	}

	return valueCopy, nil
}

// deep copy for regoMetadata, preserves inner regoMetadataMap/Set types
func copyRegoMetadata(metadata regoMetadata) (regoMetadata, error) {
	metadataCopy := make(regoMetadata)
	for key, val := range metadata {
		switch v := val.(type) {
		case regoMetadataMap:
			var copyVal regoMetadataMap
			valueJSON, err := json.Marshal(v)
			if err != nil {
				return nil, err
			}
			err = json.Unmarshal(valueJSON, &copyVal)
			if err != nil {
				return nil, err
			}
			metadataCopy[key] = copyVal
		case regoMetadataSet:
			var copyVal regoMetadataSet
			valueJSON, err := json.Marshal(v)
			if err != nil {
				return nil, err
			}
			err = json.Unmarshal(valueJSON, &copyVal)
			if err != nil {
				return nil, err
			}
			metadataCopy[key] = copyVal
		default:
			// We technically shouldn't reach here
			copyVal, err := copyValue(v)
			if err != nil {
				return nil, err
			}
			metadataCopy[key] = copyVal
		}
	}

	return metadataCopy, nil
}

// NewRegoPolicyInterpreter creates a new RegoPolicyInterpreter, using the code provided.
// inputData is the Rego data which should be used as the initial state
// of the interpreter. A deep copy is performed on it such that it will
// not be modified.
func NewRegoPolicyInterpreter(code string, inputData map[string]interface{}) (*RegoPolicyInterpreter, error) {
	data, err := copyObject(inputData)
	if err != nil {
		return nil, fmt.Errorf("unable to copy the input data: %w", err)
	}

	if _, ok := data[metadataRootKey]; !ok {
		data[metadataRootKey] = make(regoMetadata)
	}

	policy := &RegoPolicyInterpreter{
		code:     code,
		data:     data,
		modules:  make(map[string]*RegoModule),
		logLevel: LogNone,
	}

	return policy, nil
}

// AddModule adds the specified module to the interpreter such that it will be
// loaded along with the policy during query execution. The provided id
// should be used to refer to it for other methods. This will also
// invalidate the compliation artifact (i.e. Compile must be called again)
func (r *RegoPolicyInterpreter) AddModule(id string, module *RegoModule) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	r.modules[id] = module
	r.compiledModules = nil
}

// RemoveModule removes the specified module such that it will no longer be loaded.
// This will also invalidate the compliation artifact (i.e. Compile must be
// called again)
func (r *RegoPolicyInterpreter) RemoveModule(id string) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	delete(r.modules, id)
	r.compiledModules = nil
}

// IsModuleActive returns whether the specified module is currently active, i.e. being loaded
// along with the policy.
func (r *RegoPolicyInterpreter) IsModuleActive(id string) bool {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	_, ok := r.modules[id]
	return ok
}

// GetData attempts to retrieve and return a copy of the data value with the
// specified key.
func (r *RegoPolicyInterpreter) GetData(key string) (interface{}, error) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	if value, ok := r.data[key]; ok {
		copy, err := copyValue(value)
		if err != nil {
			return nil, fmt.Errorf("unable to copy value: %w", err)
		}

		return copy, nil
	}

	return nil, fmt.Errorf("data value not found for `%s`", key)
}

// UpdateData will perform an update to a value which is already within the data
// A deep copy will be performed on the value.
func (r *RegoPolicyInterpreter) UpdateData(key string, value interface{}) error {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	value, err := copyValue(value)
	if err != nil {
		return fmt.Errorf("unable to copy value: %w", err)
	}

	if _, ok := r.data[key]; ok {
		r.data[key] = value
		return nil
	} else {
		return fmt.Errorf("data value not found for `%s`", key)
	}
}

// Does not make any copies, caller must hold dataAndModulesMutex.
// Returns the zero value of T if the metadata item does not exist.
func _getMetadata[T any](r *RegoPolicyInterpreter, name string) (T, error) {
	metadataRoot, ok := r.data[metadataRootKey].(regoMetadata)
	if !ok {
		return *new(T), errors.New("illegal interpreter state: invalid metadata object type")
	}

	if metadata, ok := metadataRoot[name]; ok {
		value, ok := metadata.(T)
		if !ok {
			return *new(T), fmt.Errorf("metadata %s has the wrong type (wanted %T, but saved type was %T)", name, value, metadata)
		}
		return value, nil
	}

	return *new(T), nil
}

func _setMetadata[T any](r *RegoPolicyInterpreter, name string, value T) error {
	metadataRoot, ok := r.data[metadataRootKey].(regoMetadata)
	if !ok {
		return errors.New("illegal interpreter state: invalid metadata object type")
	}
	metadataRoot[name] = value
	return nil
}

// GetMetadata retrieves a copy of a single metadata item from the policy.
func (r *RegoPolicyInterpreter) GetMetadataMapValue(name string, key string) (interface{}, error) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	metadata, err := _getMetadata[regoMetadataMap](r, name)
	if err != nil {
		return nil, err
	}
	if metadata == nil {
		return nil, fmt.Errorf("value not found in %s for key %s (map has not been initialized)", name, key)
	}

	if value, ok := metadata[key]; ok {
		value, err := copyValue(value) //nolint:govet // shadow
		if err != nil {
			return nil, fmt.Errorf("unable to copy value: %w", err)
		}

		return value, nil
	} else {
		return nil, fmt.Errorf("value not found in %s for key %s", name, key)
	}
}

// Saves a copy of the internal policy metadata state.
func (r *RegoPolicyInterpreter) SaveMetadata() (s SavedMetadata, err error) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	metadataRoot, ok := r.data[metadataRootKey].(regoMetadata)
	if !ok {
		return SavedMetadata{}, errors.New("illegal interpreter state: invalid metadata object type")
	}
	s.metadataRoot, err = copyRegoMetadata(metadataRoot)
	return s, err
}

// Restores a previously saved metadata state.
func (r *RegoPolicyInterpreter) RestoreMetadata(m SavedMetadata) error {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	copied, err := copyRegoMetadata(m.metadataRoot)
	if err != nil {
		return fmt.Errorf("unable to copy metadata: %w", err)
	}
	r.data[metadataRootKey] = copied
	return nil
}

func newRegoMetadataOperation(operation interface{}) (*regoMetadataOperation, error) {
	var metadataOp regoMetadataOperation

	data, ok := operation.(map[string]interface{})
	if !ok {
		return nil, errors.New("unable to load metadata object")
	}
	dataType, ok := data["type"]
	if !ok {
		metadataOp.Type = metadataTypeMap
	} else {
		var dataTypeStr string
		dataTypeStr, ok = dataType.(string)
		if !ok {
			return nil, errors.New("unable to load metadata type")
		}
		metadataOp.Type = regoMetadataType(dataTypeStr)
	}
	metadataOp.Name, ok = data["name"].(string)
	if !ok {
		return nil, errors.New("unable to load metadata name")
	}
	action, ok := data["action"].(string)
	if !ok {
		return nil, errors.New("unable to load metadata action")
	}
	metadataOp.Action = regoMetadataAction(action)

	var hasKey, hasValue bool
	key, hasKey := data["key"]
	if hasKey {
		metadataOp.Key, ok = key.(string)
		if !ok {
			return nil, errors.New("unable to load metadata key")
		}
	}

	metadataOp.Value, hasValue = data["value"]

	if (metadataOp.Action != metadataRemove || metadataOp.Type == metadataTypeSet) && !hasValue {
		return nil, errors.New("missing metadata value")
	}

	if metadataOp.Type == metadataTypeMap && !hasKey {
		return nil, errors.New("missing metadata key")
	}

	return &metadataOp, nil
}

func (r *RegoPolicyInterpreter) UpdateOSType(os string) error {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()
	ops := []*regoMetadataOperation{
		{
			Action: metadataAdd,
			Type:   metadataTypeMap,
			Name:   "operatingsystem",
			Key:    "ostype",
			Value:  os,
		},
	}
	return r.updateMetadata(ops)
}

// dataAndModulesMutex must be held before calling this
func (r *RegoPolicyInterpreter) updateMetadata(ops []*regoMetadataOperation) error {
	for _, op := range ops {
		switch op.Type {
		case metadataTypeMap:
			metadata, err := _getMetadata[regoMetadataMap](r, op.Name)
			if err != nil {
				return fmt.Errorf("unable to get metadata: %w", err)
			}
			if metadata == nil {
				metadata = make(regoMetadataMap)
			}
			switch op.Action {
			case metadataAdd:
				if _, ok := metadata[op.Key]; ok {
					return fmt.Errorf("cannot add metadata value, key %s[%s] already exists", op.Name, op.Key)
				} else {
					metadata[op.Key] = op.Value
				}

			case metadataUpdate:
				metadata[op.Key] = op.Value

			case metadataRemove:
				delete(metadata, op.Key)

			default:
				return fmt.Errorf("unrecognized metadata action: %s for map", op.Action)
			}

			if err := _setMetadata[regoMetadataMap](r, op.Name, metadata); err != nil {
				return fmt.Errorf("unable to set metadata: %w", err)
			}
		case metadataTypeSet:
			metadata, err := _getMetadata[regoMetadataSet](r, op.Name)
			if err != nil {
				return fmt.Errorf("unable to get metadata: %w", err)
			}
			if metadata == nil {
				metadata = make(regoMetadataSet, 0)
			}
			switch op.Action {
			case metadataAdd:
				if !slices.ContainsFunc(metadata, func(e interface{}) bool {
					return reflect.DeepEqual(e, op.Value)
				}) {
					metadata = append(metadata, op.Value)
				}
			case metadataRemove:
				metadata = slices.DeleteFunc(metadata, func(e interface{}) bool {
					return reflect.DeepEqual(e, op.Value)
				})
			default:
				return fmt.Errorf("unrecognized metadata action: %s for set", op.Action)
			}
			if err := _setMetadata[regoMetadataSet](r, op.Name, metadata); err != nil {
				return fmt.Errorf("unable to set metadata: %w", err)
			}
		default:
			return fmt.Errorf("unrecognized metadata type: %s", op.Type)
		}
	}

	r.logMetadata()

	return nil
}

// EnableLogging enables logging to the provided path at the specified level.
func (r *RegoPolicyInterpreter) EnableLogging(path string, level LogLevel) error {
	// this mutex ensures no-one reads compiledModules before we clear it
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	r.compiledModules = nil
	r.logLevel = level

	file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	if err != nil {
		return err
	}

	r.logFile = file
	r.infoLogger = log.New(file, "INFO: ", log.Ldate|log.Ltime)
	r.resultsLogger = log.New(file, "RESULT: ", log.Ldate|log.Ltime)
	r.metadataLogger = log.New(file, "METADATA: ", log.Ldate|log.Ltime)
	r.logInfo("Logging Enabled at level %d", level)

	return nil
}

// SetLogLevel sets the logging level. To actually produce a log, however, EnableLogging
// must be called first.
func (r *RegoPolicyInterpreter) SetLogLevel(level LogLevel) {
	// this mutex ensures no-one reads compiledModules before we clear it
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	r.compiledModules = nil
	r.logLevel = level
}

// DisableLogging disables logging and closes the underlying log file.
func (r *RegoPolicyInterpreter) DisableLogging() error {
	// this mutex ensures no-one reads compiledModules before we clear it
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	r.compiledModules = nil
	r.logLevel = LogNone
	if r.logFile != nil {
		r.logInfo("Logging disabled")
		r.infoLogger = nil
		r.resultsLogger = nil
		r.metadataLogger = nil
		err := r.logFile.Close()
		if err != nil {
			return err
		}

		r.logFile = nil
	}

	return nil
}

func (r *RegoPolicyInterpreter) compile() error {
	// dataAndModulesMutex must be held before calling this

	modules := make(map[string]string)
	for _, module := range r.modules {
		modules[module.Namespace+".rego"] = module.Code
	}

	modules["policy.rego"] = r.code

	options := ast.CompileOpts{
		EnablePrintStatements: r.logLevel != LogNone,
	}

	if compiled, err := ast.CompileModulesWithOpt(modules, options); err == nil {
		r.compiledModules = compiled
		return nil
	} else {
		return fmt.Errorf("rego compilation failed: %w", err)
	}
}

// Compile compiles the policy and its modules. This will increase the speed of policy
// execution.
func (r *RegoPolicyInterpreter) Compile() error {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	return r.compile()
}

func (r *RegoPolicyInterpreter) logInfo(message string, args ...interface{}) {
	if r.logLevel < LogInfo || len(message) == 0 {
		return
	}
	r.infoLogger.Printf(message, args...)
}

func (r *RegoPolicyInterpreter) logResult(rule string, resultSet interface{}) {
	if r.logLevel < LogResults {
		return
	}

	contents, err := json.Marshal(resultSet)
	if err != nil {
		r.resultsLogger.Printf("error marshaling result set: %v\n", err.Error())
	} else {
		r.resultsLogger.Printf("%s -> %s", rule, string(contents))
	}
}

func (r *RegoPolicyInterpreter) logMetadata() {
	if r.logLevel < LogMetadata {
		return
	}

	contents, err := json.Marshal(r.data[metadataRootKey])
	if err != nil {
		r.metadataLogger.Printf("error marshaling metadata: %v\n", err.Error())
	} else {
		r.metadataLogger.Println(string(contents))
	}
}

// Value returns the raw value from a Rego query result.
func (r RegoQueryResult) Value(key string) (interface{}, error) {
	if value, ok := r[key]; ok {
		return value, nil
	} else {
		return nil, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// Object attempts to interpret the result value as an object
func (r RegoQueryResult) Object(key string) (map[string]interface{}, error) {
	if value, ok := r[key]; ok {
		if obj, ok := value.(map[string]interface{}); ok {
			return obj, nil
		} else {
			return nil, fmt.Errorf("value for '%s' is not an object", key)
		}
	} else {
		return nil, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// Array attempts to interpret the result value as an array.
func (r RegoQueryResult) Array(key string) ([]interface{}, error) {
	if value, ok := r[key]; ok {
		if arr, ok := value.([]interface{}); ok {
			return arr, nil
		} else {
			return nil, fmt.Errorf("value for '%s' is not an array", key)
		}
	} else {
		return nil, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// Bool attempts to interpret a result value as a boolean.
func (r RegoQueryResult) Bool(key string) (bool, error) {
	if value, ok := r[key]; ok {
		if flag, ok := value.(bool); ok {
			return flag, nil
		} else {
			return false, fmt.Errorf("value for '%s' is not a boolean", key)
		}
	} else {
		return false, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// String attempts to interpret the result value as a string.
func (r RegoQueryResult) String(key string) (string, error) {
	if value, ok := r[key]; ok {
		if str, ok := value.(string); ok {
			return str, nil
		} else {
			return "", fmt.Errorf("value for '%s' is not a string", key)
		}
	} else {
		return "", fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// Float attempts to interpret the result value as a floating point number.
func (r RegoQueryResult) Float(key string) (float64, error) {
	if value, ok := r[key]; ok {
		if number, ok := value.(json.Number); ok {
			if floatValue, err := number.Float64(); err == nil {
				return floatValue, nil
			} else {
				return 0, err
			}
		} else {
			return 0, fmt.Errorf("value for %s is not a json Number", key)
		}
	} else {
		return 0, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// Int attempts to interpret the result value as an integer.
func (r RegoQueryResult) Int(key string) (int, error) {
	if value, ok := r[key]; ok {
		if number, ok := value.(json.Number); ok {
			if intValue, err := number.Int64(); err == nil {
				return int(intValue), nil
			} else {
				return 0, err
			}
		} else {
			return 0, errors.New("value is not a json Number")
		}
	} else {
		return 0, fmt.Errorf("unable to find value for key '%s'", key)
	}
}

// IsEmpty tests if the query result is empty.
func (r RegoQueryResult) IsEmpty() bool {
	return len(r) == 0
}

// Union creates a new result object which is the union of this result with
// another result, in which the results of the other will take precedence.
func (r RegoQueryResult) Union(other RegoQueryResult) RegoQueryResult {
	result := make(RegoQueryResult)
	for key := range r {
		result[key] = r[key]
	}
	for key := range other {
		result[key] = other[key]
	}
	return result
}

func (r *RegoPolicyInterpreter) query(rule string, input map[string]interface{}) (rego.ResultSet, error) {
	// dataAndModulesMutex must be held before calling this

	store := inmem.NewFromObject(r.data)

	var buf bytes.Buffer
	query := rego.New(
		rego.Query(rule),
		rego.Input(input),
		rego.Store(store),
		rego.EnablePrintStatements(r.logLevel != LogNone),
		rego.PrintHook(topdown.NewPrintHook(&buf)),
		rego.Compiler(r.compiledModules))

	ctx := context.Background()
	resultSet, err := query.Eval(ctx)
	output := buf.String()

	r.logInfo("%s", output)

	return resultSet, err
}

func (r *RegoPolicyInterpreter) RawQuery(rule string, input map[string]interface{}) (rego.ResultSet, error) {
	// this mutex ensures no other threads modify the data and compiledModules fields during query execution
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	if r.compiledModules == nil {
		err := r.compile()
		if err != nil {
			return nil, fmt.Errorf("error when compiling modules: %w", err)
		}
	}

	resultSet, err := r.query(rule, input)

	if err != nil {
		return nil, err
	}

	r.logResult(rule, resultSet)

	return resultSet, nil
}

// MetadataJSON returns the entire metadata object as a JSON string.
// The returned JSON is a snapshot (deep-copied via marshal/unmarshal).
func (r *RegoPolicyInterpreter) MetadataJSON() (string, error) {
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	root, ok := r.data["metadata"].(regoMetadata)
	if !ok {
		return "", errors.New("incorrect interpreter state: invalid metadata object type")
	}

	// Deep copy to avoid callers modifying internal maps.
	b, err := json.Marshal(root)
	if err != nil {
		return "", fmt.Errorf("unable to marshal metadata: %w", err)
	}

	return string(b), nil
}

// Query queries the policy with the given rule and input data and returns the result.
func (r *RegoPolicyInterpreter) Query(rule string, input map[string]interface{}) (RegoQueryResult, error) {
	// this mutex ensures no other threads modify the data and compiledModules fields during query execution
	r.dataAndModulesMutex.Lock()
	defer r.dataAndModulesMutex.Unlock()

	if r.compiledModules == nil {
		err := r.compile()
		if err != nil {
			return nil, fmt.Errorf("error when compiling modules: %w", err)
		}
	}

	rawResult, err := r.query(rule, input)

	if err != nil {
		return nil, err
	}

	result := make(RegoQueryResult)
	if len(rawResult) == 0 {
		return result, nil
	}

	resultSet, ok := rawResult[0].Expressions[0].Value.(map[string]interface{})
	if !ok {
		return nil, errors.New("unable to load results object from Rego query")
	}

	r.logResult(rule, resultSet)

	ops := []*regoMetadataOperation{}
	if rawMetadata, ok := resultSet[metadataOperationsKey]; ok {
		metadata, ok := rawMetadata.([]interface{})
		if !ok {
			return nil, errors.New("error loading metadata array: invalid type")
		}

		for _, value := range metadata {
			op, err := newRegoMetadataOperation(value)
			if err != nil {
				return nil, fmt.Errorf("error loading metadata operation: %w", err)
			}
			ops = append(ops, op)
		}

		if len(ops) > 0 {
			err = r.updateMetadata(ops)
			if err != nil {
				return nil, fmt.Errorf("error applying metadata operations: %w", err)
			}
		}
	}

	for name, value := range resultSet {
		if name == metadataOperationsKey {
			continue
		} else {
			result[name] = value
		}
	}

	return result, nil
}

// ModuleID computes a unique ID for a Module from its issuer and feed.
func ModuleID(issuer string, feed string) string {
	return issuer + ">" + feed
}

// ID is the unique ID of a module.
func (f RegoModule) ID() string {
	return ModuleID(f.Issuer, f.Feed)
}
