// SPDX-License-Identifier: BUSL-1.1
//
// Copyright (C) 2025, Berachain Foundation. All rights reserved.
// Use of this software is governed by the Business Source License included
// in the LICENSE file of this repository and at www.mariadb.com/bsl11.
//
// ANY USE OF THE LICENSED WORK IN VIOLATION OF THIS LICENSE WILL AUTOMATICALLY
// TERMINATE YOUR RIGHTS UNDER THIS LICENSE FOR THE CURRENT AND ALL OTHER
// VERSIONS OF THE LICENSED WORK.
//
// THIS LICENSE DOES NOT GRANT YOU ANY RIGHT IN ANY TRADEMARK OR LOGO OF
// LICENSOR OR ITS AFFILIATES (PROVIDED THAT YOU MAY USE A TRADEMARK OR LOGO OF
// LICENSOR AS EXPRESSLY REQUIRED BY THIS LICENSE).
//
// TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
// AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
// EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
// TITLE.

package service

import (
	"context"
	"fmt"
	"reflect"

	"github.com/berachain/beacon-kit/log"
)

// Basic is the minimal interface for a service.
type Basic interface {
	// Start spawns any goroutines required by the service.
	Start(ctx context.Context) error
	// Stop stops the service. It should be safe to call
	// Stop on a service that has not been started
	Stop() error
	// Name returns the name of the service.
	Name() string
}

// Registry provides a useful pattern for managing services.
// It allows for ease of dependency management and ensures services
// dependent on others use the same references in memory.
type Registry struct {
	// logger is the logger for the Registry.
	logger log.Logger
	// services is a map of service type -> service instance.
	services map[string]Basic
	// servicesStarted is a map of services we have called Start() on.
	servicesStarted map[string]struct{}
	// serviceTypes is an ordered slice of registered service types.
	serviceTypes []string
}

// NewRegistry starts a registry instance for convenience.
func NewRegistry(logger log.Logger, opts ...RegistryOption) *Registry {
	r := &Registry{
		logger:          logger,
		services:        make(map[string]Basic),
		servicesStarted: make(map[string]struct{}),
	}

	for _, opt := range opts {
		if err := opt(r); err != nil {
			panic(err)
		}
	}
	return r
}

// StartAll initialized each service in order of registration.
func (s *Registry) StartAll(ctx context.Context) error {
	// start all services
	s.logger.Info("Starting services", "num", len(s.serviceTypes))
	for _, typeName := range s.serviceTypes {
		s.logger.Info("Starting service", "type", typeName)
		svc := s.services[typeName]
		if svc == nil {
			s.logger.Error("service not found", "type", typeName)
			continue
		}

		if err := svc.Start(ctx); err != nil {
			return fmt.Errorf("error when starting service %s: %w", typeName, err)
		}

		s.servicesStarted[typeName] = struct{}{}
	}
	s.logger.Info("All services started", "num", len(s.servicesStarted))
	return nil
}

func (s *Registry) StopAll() {
	// stop all services in reverse order they were started
	s.logger.Info("Stopping services", "num", len(s.serviceTypes))
	for i := len(s.serviceTypes) - 1; i >= 0; i-- {
		typeName := s.serviceTypes[i]

		if _, started := s.servicesStarted[typeName]; !started {
			s.logger.Info("Service not started", "type", typeName)
			continue
		}

		s.logger.Info("Stopping service", "type", typeName)
		svc := s.services[typeName]
		if svc == nil {
			s.logger.Error("service not found", "type", typeName)
			continue
		}

		if err := svc.Stop(); err != nil {
			s.logger.Error("error when stopping service", "type", typeName, "err", err)
		}
	}
	s.logger.Info("All services stopped", "num", len(s.servicesStarted))
}

// RegisterService appends a service constructor function to the service
// registry.
func (s *Registry) RegisterService(service Basic) error {
	typeName := service.Name()
	if _, exists := s.services[typeName]; exists {
		return errServiceAlreadyExists
	}
	s.services[typeName] = service
	s.serviceTypes = append(s.serviceTypes, typeName)
	return nil
}

// FetchService takes in a struct pointer and sets the value of that pointer
// to a service currently stored in the service registry. This ensures the
// input argument is set to the right pointer that refers to the originally
// registered service.
func (s *Registry) FetchService(service interface{}) error {
	serviceType := reflect.TypeOf(service)
	if serviceType.Kind() != reflect.Ptr ||
		serviceType.Elem().Kind() != reflect.Ptr {
		return errInputIsNotPointer
	}

	element := reflect.ValueOf(service).Elem()

	typeName := ""
	for name, svc := range s.services {
		svcType := reflect.TypeOf(svc)
		if svcType.AssignableTo(serviceType.Elem()) {
			typeName = name
			break
		}
	}

	if typeName == "" {
		return errUnknownService
	}

	if running, ok := s.services[typeName]; ok {
		element.Set(reflect.ValueOf(running))
		return nil
	}
	return errUnknownService
}
