package nuke

import (
	"context"
	"fmt"
	"sync"

	awsv2 "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go/aws/session"

	liberrors "github.com/ekristen/libnuke/pkg/errors"
)

// SessionFactory support for custom endpoints
type SessionFactory func(regionName, svcType string) (*session.Session, error)

// ConfigFactory is the SDK v2 equivalent to SessionFactory.
type ConfigFactory func(ctx context.Context, regionName, svcType string) (*awsv2.Config, error)

// ResourceTypeResolver returns the service type from the resourceType
type ResourceTypeResolver func(regionName, resourceType string) string

// Region is an AWS Region with an attached SessionFactory
type Region struct {
	Name            string
	NewSession      SessionFactory // SDK v1
	NewConfig       ConfigFactory  // SDK v2
	ResTypeResolver ResourceTypeResolver

	cache    map[string]*session.Session
	cfgCache map[string]*awsv2.Config
	lock     *sync.RWMutex
}

// NewRegion creates a new Region and returns it.
func NewRegion(name string, typeResolver ResourceTypeResolver, sessionFactory SessionFactory, cfgFactory ConfigFactory) *Region {
	return &Region{
		Name:            name,
		NewSession:      sessionFactory,
		NewConfig:       cfgFactory,
		ResTypeResolver: typeResolver,
		lock:            &sync.RWMutex{},
		cache:           make(map[string]*session.Session),
		cfgCache:        make(map[string]*awsv2.Config),
	}
}

// Session returns a session for a given resource type for the region it's associated to.
func (region *Region) Session(resourceType string) (*session.Session, error) {
	svcType := region.ResTypeResolver(region.Name, resourceType)
	if svcType == "" {
		return nil, liberrors.ErrSkipRequest(fmt.Sprintf(
			"No service available in region '%s' to handle '%s'",
			region.Name, resourceType))
	}

	// Need to read
	region.lock.RLock()
	sess := region.cache[svcType]
	region.lock.RUnlock()
	if sess != nil {
		return sess, nil
	}

	// Need to write:
	region.lock.Lock()
	sess, err := region.NewSession(region.Name, svcType)
	if err != nil {
		region.lock.Unlock()
		return nil, err
	}
	region.cache[svcType] = sess
	region.lock.Unlock()
	return sess, nil
}

// Config returns an SDK v2 config for a given resource type for the region
// it's associated to.
func (region *Region) Config(resourceType string) (*awsv2.Config, error) {
	svcType := region.ResTypeResolver(region.Name, resourceType)
	if svcType == "" {
		return nil, liberrors.ErrSkipRequest(fmt.Sprintf(
			"No service available in region '%s' to handle '%s'",
			region.Name, resourceType))
	}

	// Need to read
	region.lock.RLock()
	cfg := region.cfgCache[svcType]
	region.lock.RUnlock()
	if cfg != nil {
		return cfg, nil
	}

	// Need to write:
	region.lock.Lock()
	cfg, err := region.NewConfig(context.TODO(), region.Name, svcType)
	if err != nil {
		region.lock.Unlock()
		return nil, err
	}
	region.cfgCache[svcType] = cfg
	region.lock.Unlock()
	return cfg, nil
}
