package gotenberg

import (
	"fmt"
	"reflect"
)

// Context is a struct which helps initializing modules. When provisioning, a
// module may use the context to get other modules that it needs internally.
type Context struct {
	flags           ParsedFlags
	descriptors     []ModuleDescriptor
	moduleInstances map[string]interface{}
}

// NewContext creates a Context.
// In a module, prefer the Provisioner interface to get a Context.
func NewContext(
	flags ParsedFlags,
	descriptors []ModuleDescriptor,
) *Context {
	return &Context{
		flags:           flags,
		descriptors:     descriptors,
		moduleInstances: make(map[string]interface{}),
	}
}

// ParsedFlags returns the parsed flags.
//
//	func (m *YourModule) Provision(ctx *gotenberg.Context) error {
//		flags := ctx.ParsedFlags()
//		m.foo = flags.RequiredString("foo")
//	}
func (ctx Context) ParsedFlags() ParsedFlags {
	return ctx.flags
}

// Module returns a module which satisfies the requested interface.
//
//	func (m *YourModule) Provision(ctx *gotenberg.Context) error {
//		mod, _ := ctx.Module(new(ModuleInterface))
//		real := mod.(ModuleInterface)
//	}
//
// If the module has not yet been initialized, this method
// initializes it. Otherwise, returns the already initialized instance.
func (ctx *Context) Module(kind interface{}) (interface{}, error) {
	mods, err := ctx.Modules(kind)

	if err != nil {
		return nil, fmt.Errorf("get module: %w", err)
	}

	if len(mods) != 1 {
		return nil, fmt.Errorf("expected to have one and only one %s module", kind)
	}

	return mods[0], nil
}

// Modules returns the list of modules which satisfies the requested interface.
//
//	func (m *YourModule) Provision(ctx *gotenberg.Context) error {
//		mods, _ := ctx.Modules(new(ModuleInterface))
//		for _, mod := range mods {
//			real := mod.(ModuleInterface)
//			// ...
//		}
//	}
//
// If one or more modules have not yet been initialized, this method
// initializes them. Otherwise, returns the already initialized instances.
func (ctx *Context) Modules(kind interface{}) ([]interface{}, error) {
	realKind := reflect.TypeOf(kind).Elem()

	var mods []interface{}

	for _, desc := range ctx.descriptors {
		newInstance := desc.New()

		if ok := reflect.TypeOf(newInstance).Implements(realKind); ok {
			// The module implements the requested interface.
			// We check if it has already been initialized.
			instance, ok := ctx.moduleInstances[desc.ID]

			if ok {
				mods = append(mods, instance)
			} else {
				err := ctx.loadModule(desc.ID, newInstance)
				if err != nil {
					return nil, err
				}

				mods = append(mods, newInstance)
			}
		}
	}

	return mods, nil
}

// loadModule calls the Provision and/or Validate methods of the requested
// module if it satisfies the Provisioner and/or Validator interfaces.
func (ctx *Context) loadModule(id string, instance interface{}) error {
	if prov, ok := instance.(Provisioner); ok {
		// The instance can be provisioned.
		err := prov.Provision(ctx)
		if err != nil {
			return fmt.Errorf("provision module %s: %w", id, err)
		}
	}

	if validator, ok := instance.(Validator); ok {
		// The instance can be validated.
		err := validator.Validate()
		if err != nil {
			return fmt.Errorf("validate module %s: %w", id, err)
		}
	}

	ctx.moduleInstances[id] = instance

	return nil
}
