package publisher

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"sync"
)

// RouteFetcher exists to mock or override how route configuration is fetched.
type RouteFetcher interface {
	FetchRoute(env Environment, typeName string) (*Route, error)
}

var validEventNameRegex = regexp.MustCompile("^[a-zA-Z0-9]+$")

type routeKey struct {
	env      Environment
	typeName string
}

// Route defines configuration for a single Event Stream.
// Typically, routes are automatically fetched from the eventbus configuration
// store (an S3 bucket).
type Route struct {
	// the SNS topic ARN to publish to
	Arn string `json:"arn"`

	// Suppress sending no-op messages to this route when the publisher starts.
	// This is an option we hope we don't have to use, but exists in case there
	// is ever a 'rogue' publisher that is restarting on a loop.
	SuppressNoop bool `json:"suppress_noop,omitempty"`
}

func routes(fetcher RouteFetcher, env Environment, messageTypes []string) (map[string]*Route, error) {
	routes := make(map[string]*Route, len(messageTypes))

	for _, typeName := range messageTypes {

		validName := validEventName(typeName)
		if !validName {
			return nil, fmt.Errorf("event type %s is invalid, must be non-empty and alphanumeric", typeName)
		}

		r, err := fetcher.FetchRoute(env, typeName)
		if err != nil {
			return nil, err
		}

		routes[typeName] = r
	}

	return routes, nil
}

func validEventName(name string) bool {
	return validEventNameRegex.MatchString(name)
}

// DefaultRouteFetcher fetches the route configuration from an HTTP URI base.
//
// the baseURI is expected to have no trailing slash as the slash will be
// added by the fetcher.
func DefaultRouteFetcher(baseURI string) RouteFetcher {
	return &defaultRouteFetcher{
		baseURI: baseURI,
		routes: &routesCache{
			routeLookup: make(map[routeKey]*Route),
		},
	}
}

type defaultRouteFetcher struct {
	baseURI string
	routes  *routesCache
}

type routesCache struct {
	routeMutex  sync.Mutex
	routeLookup map[routeKey]*Route
}

func (f *defaultRouteFetcher) FetchRoute(env Environment, typeName string) (*Route, error) {
	key := routeKey{env, typeName}
	var r *Route
	f.routes.routeMutex.Lock()
	defer f.routes.routeMutex.Unlock()
	if r, ok := f.routes.routeLookup[key]; ok {
		return r, nil
	}

	if env == EnvLocal {
		r = &Route{
			Arn: "arn:aws:sns:eventbus-" + typeName,
		}
	} else {
		uri := fmt.Sprintf(routeURIFormat, f.baseURI, typeName, string(env))
		resp, err := http.Get(uri)
		if err != nil {
			return nil, err
		} else if resp.StatusCode >= 300 || resp.StatusCode < 200 {
			return nil, fmt.Errorf("Got code %v retrieving config from %s", resp.StatusCode, uri)
		}

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, err
		}
		resp.Body.Close()

		r = &Route{}
		if err = json.Unmarshal(body, r); err != nil {
			return nil, err
		}
	}

	f.routes.routeLookup[key] = r

	return r, nil
}
