package service

import (
	"os"
	"os/signal"
	rtime "runtime"
	"sync"

	"go-micro.dev/v6/client"
	"go-micro.dev/v6/cmd"
	signalutil "go-micro.dev/v6/internal/util/signal"
	log "go-micro.dev/v6/logger"
	"go-micro.dev/v6/model"
	"go-micro.dev/v6/server"
	"go-micro.dev/v6/store"
)

// Service is the interface for a go-micro service.
type Service interface {
	// Name returns the service name.
	Name() string
	// Init initializes options. Parses command line flags on first call.
	Init(...Option)
	// Options returns the current options.
	Options() Options
	// Handle registers a handler with optional server.HandlerOption args.
	Handle(v interface{}, opts ...server.HandlerOption) error
	// Client returns the RPC client.
	Client() client.Client
	// Server returns the RPC server.
	Server() server.Server
	// Model returns the data model backend.
	Model() model.Model
	// Start the service (non-blocking).
	Start() error
	// Stop the service.
	Stop() error
	// Run starts the service, blocks on signal/context, then stops.
	Run() error
	// String returns the implementation name.
	String() string
}

type serviceImpl struct {
	opts Options

	once sync.Once
}

// New creates a new service with the given options.
func New(opts ...Option) Service {
	return &serviceImpl{
		opts: newOptions(opts...),
	}
}

func (s *serviceImpl) Name() string {
	return s.opts.Server.Options().Name
}

// Init initializes options. Additionally it calls cmd.Init
// which parses command line flags. cmd.Init is only called
// on first Init.
func (s *serviceImpl) Init(opts ...Option) {
	// process options
	for _, o := range opts {
		o(&s.opts)
	}

	s.once.Do(func() {
		// set cmd name
		if len(s.opts.Cmd.App().Name) == 0 {
			s.opts.Cmd.App().Name = s.Server().Options().Name
		}

		// Initialize the command flags, overriding new service
		if err := s.opts.Cmd.Init(
			cmd.Auth(&s.opts.Auth),
			cmd.Broker(&s.opts.Broker),
			cmd.Registry(&s.opts.Registry),
			cmd.Transport(&s.opts.Transport),
			cmd.Client(&s.opts.Client),
			cmd.Config(&s.opts.Config),
			cmd.Server(&s.opts.Server),
			cmd.Store(&s.opts.Store),
			cmd.Profile(&s.opts.Profile),
		); err != nil {
			s.opts.Logger.Log(log.FatalLevel, err)
		}

		// Scope the service's store to its own table (database "service",
		// table = service name), consistent with how agents ("agent/{name}")
		// and flows ("flow/{name}") scope their state. This replaces the
		// older Init(store.Table(name)) global mutation with a composable
		// scoped handle: each service gets an isolated handle that works
		// even when several run in one process. When the service uses the
		// package default store, bridge it to the same scope so handlers
		// that reach for store.DefaultStore stay isolated too.
		name := s.opts.Cmd.App().Name
		wasDefault := s.opts.Store == store.DefaultStore
		s.opts.Store = store.Scope(s.opts.Store, "service", name)
		if wasDefault {
			store.DefaultStore = s.opts.Store
		}
	})
}

func (s *serviceImpl) Options() Options {
	return s.opts
}

func (s *serviceImpl) Client() client.Client {
	return s.opts.Client
}

func (s *serviceImpl) Server() server.Server {
	return s.opts.Server
}

func (s *serviceImpl) Model() model.Model {
	return s.opts.Model
}

func (s *serviceImpl) String() string {
	return "micro"
}

func (s *serviceImpl) Start() error {
	for _, fn := range s.opts.BeforeStart {
		if err := fn(); err != nil {
			return err
		}
	}

	if err := s.opts.Server.Start(); err != nil {
		return err
	}

	for _, fn := range s.opts.AfterStart {
		if err := fn(); err != nil {
			return err
		}
	}

	return nil
}

func (s *serviceImpl) Stop() error {
	var gerr error

	for _, fn := range s.opts.BeforeStop {
		if err := fn(); err != nil {
			gerr = err
		}
	}

	if err := s.opts.Server.Stop(); err != nil {
		return err
	}

	for _, fn := range s.opts.AfterStop {
		if err := fn(); err != nil {
			gerr = err
		}
	}

	return gerr
}

func (s *serviceImpl) Handle(v interface{}, opts ...server.HandlerOption) error {
	return s.opts.Server.Handle(
		s.opts.Server.NewHandler(v, opts...),
	)
}

func (s *serviceImpl) Run() (err error) {
	logger := s.opts.Logger

	// exit when help flag is provided
	for _, v := range os.Args[1:] {
		if v == "-h" || v == "--help" {
			os.Exit(0)
		}
	}

	// start the profiler
	if s.opts.Profile != nil {
		// to view mutex contention
		rtime.SetMutexProfileFraction(5)
		// to view blocking profile
		rtime.SetBlockProfileRate(1)

		if err = s.opts.Profile.Start(); err != nil {
			return err
		}

		defer func() {
			if nerr := s.opts.Profile.Stop(); nerr != nil {
				logger.Log(log.ErrorLevel, nerr)
			}
		}()
	}

	logger.Logf(log.InfoLevel, "Starting [service] %s", s.Name())

	if err = s.Start(); err != nil {
		return err
	}

	ch := make(chan os.Signal, 1)
	if s.opts.Signal {
		signal.Notify(ch, signalutil.Shutdown()...)
	}

	select {
	// wait on kill signal
	case <-ch:
	// wait on context cancel
	case <-s.opts.Context.Done():
	}

	return s.Stop()
}
