package container

import (
	"errors"
	"reflect"
	"slices"
)

func (this *Container) ConstructContainer() {
	container := reflect.ValueOf(this).Elem()

	for i := 0; i < container.NumField(); i++ {
		service := container.Field(i)
		// IsNil allows to pre-populate the container via custom services
		if service.Kind() == reflect.Ptr && service.IsNil() {
			if service.CanSet() {
				service.Set(reflect.New(service.Type().Elem()))
			}
		}
	}
}

func (this *Container) InjectContainer(skipInjection []string) {
	container := reflect.ValueOf(this).Elem()

	for i := 0; i < container.NumField(); i++ {
		service := container.Field(i)
		indService := reflect.Indirect(service)

		kind := service.Kind()
		// Interfaces are built via custom
		if kind == reflect.Interface {
			continue
		}

		if !service.IsValid() {
			panic(errors.New("Invalid service at " + service.String()))
		}
		if service.Kind() == reflect.Ptr && service.IsNil() {
			panic(errors.New("Nil pointer service at " + service.String()))

		}
		if !service.Elem().IsValid() {
			panic(errors.New("Invalid service elem at " + service.String()))
		}

		serviceName := service.Elem().Type().Name()
		if slices.Contains(skipInjection, serviceName) {
			continue
		}
		if kind != reflect.Pointer {
			panic(errors.New("Should be a pointer"))
		}

		this.injectServicesIntoService(container, indService, service)
	}
}

func (this *Container) injectServicesIntoService(container reflect.Value, indService reflect.Value, service reflect.Value) {
	for j := 0; j < indService.NumField(); j++ {
		injection := indService.Field(j)
		name := service.Elem().Type().Field(j).Name
		value := this.serviceByName(container, name)

		// Struct named Data are allowed to contain shared state values
		if name == "Data" {
			continue
		}

		if service.Elem().Type().Field(j).Type.Kind() == reflect.Pointer && !value.IsValid() {
			this.localInjection(container, injection)
			continue
		}

		if !value.IsValid() {
			serviceName := service.Elem().Type().Name()
			panic(errors.New("Service " + serviceName + "." + name + " cannot be injected"))
		}
		injection.Set(value)
	}
}

func (this *Container) localInjection(container reflect.Value, parentService reflect.Value) {
	if !(parentService.Kind() == reflect.Ptr && parentService.IsNil()) {
		panic("Invalid local injection")
	}
	if !parentService.CanSet() {
		panic(errors.New("Invalid local injection"))
	}

	service := reflect.New(parentService.Type().Elem())
	indService := reflect.Indirect(service)

	for j := 0; j < indService.NumField(); j++ {
		injection := indService.Field(j)
		kind := injection.Kind()
		name := service.Elem().Type().Field(j).Name
		// Struct named Data are allowed to contain shared state values
		if name == "Data" {
			continue
		}
		pointerOrInterface := kind == reflect.Pointer || kind == reflect.Interface
		if !pointerOrInterface {
			panic(errors.New("Only fields that are pointer or interface are allowed here. Field name/type: " + name + "/" + kind.String()))
		}
		value := this.serviceByName(container, name)
		if !value.IsValid() {
			panic(errors.New("Cannot find the service '" + name + "' asked to locally inject '" + parentService.Type().String() + "' from container"))
		}
		injection.Set(value)
	}
	parentService.Set(service)
}

func (this *Container) serviceByName(container reflect.Value, name string) reflect.Value {
	value := container.FieldByName(name)
	if value.IsValid() {
		return value
	}
	if name == "Component" || name == "Atoms" || name == "Molecules" || name == "Organisms" {
		return container.FieldByName("Kit").Elem().FieldByName(name)
	}
	return value
}
