package finder

import (
	"fmt"
	"strings"
	"sync"

	"github.com/hashicorp/consul/api"
)

// Finder finds intentions by a src/dst exact match. There is currently
// no direct API to do this so this struct downloads all intentions and
// caches them once, and searches in-memory for this. For now this works since
// even with a very large number of intentions, the size of the data gzipped
// over HTTP will be relatively small.
//
// The Finder will only download the intentions one time. This struct is
// not expected to be used over a long period of time. Though it may be
// reused multile times, the intentions list is only downloaded once.
type Finder struct {
	// Client is the API client to use for any requests.
	Client *api.Client

	lock sync.Mutex
	ixns []*api.Intention // cached list of intentions
}

// ID returns the intention ID for the given CLI args. An error is returned
// if args is not 1 or 2 elements.
func (f *Finder) IDFromArgs(args []string) (string, error) {
	switch len(args) {
	case 1:
		return args[0], nil

	case 2:
		ixn, err := f.Find(args[0], args[1])
		if err != nil {
			return "", err
		}
		if ixn == nil {
			return "", fmt.Errorf(
				"Intention with source %q and destination %q not found.",
				args[0], args[1])
		}

		return ixn.ID, nil

	default:
		return "", fmt.Errorf("command requires exactly 1 or 2 arguments")
	}
}

// Find finds the intention that matches the given src and dst. This will
// return nil when the result is not found.
func (f *Finder) Find(src, dst string) (*api.Intention, error) {
	src = StripDefaultNS(src)
	dst = StripDefaultNS(dst)

	f.lock.Lock()
	defer f.lock.Unlock()

	// If the list of ixns is nil, then we haven't fetched yet, so fetch
	if f.ixns == nil {
		ixns, _, err := f.Client.Connect().Intentions(nil)
		if err != nil {
			return nil, err
		}

		f.ixns = ixns
	}

	// Go through the intentions and find an exact match
	for _, ixn := range f.ixns {
		if ixn.SourceString() == src && ixn.DestinationString() == dst {
			return ixn, nil
		}
	}

	return nil, nil
}

// StripDefaultNS strips the default namespace from an argument. For now,
// the API and lookups strip this value from string output so we strip it.
func StripDefaultNS(v string) string {
	if idx := strings.IndexByte(v, '/'); idx > 0 {
		if v[:idx] == api.IntentionDefaultNamespace {
			return v[idx+1:]
		}
	}

	return v
}
