//  Copyright 2024 Google Inc. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

// Package service is a package with os specific service handling logic.
package service

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/GoogleCloudPlatform/galog"
)

// State is the type used in the state mapping enum.
type State int

const (
	// StateUnknown is the default state of the service.
	StateUnknown State = iota
	// StateRunning is a mapping of native representation for: running.
	StateRunning
	// StateStopped is a mapping of native representation for: stopped.
	StateStopped
)

var (
	// nativeServiceName is the string used to register with the service manager.
	nativeServiceName string
	// nativeHandler is the OS specific implementation of serviceHandler.
	nativeHandler serviceHandler
	// serviceManagerSignal is the channel handle cases when the OS service
	// manager notifies it is shutting down/stopping the service.
	serviceManagerSignal = make(chan bool)
)

// notRunningAsServiceError is returned when the agent is not running as a
// service and cannot perform service management operations.
type notRunningAsServiceError struct{}

// Error returns the error message for the notRunningAsServiceError.
func (e *notRunningAsServiceError) Error() string {
	return fmt.Sprintf("not running as service")
}

// Is checks if the error is a notRunningAsServiceError and returns true if it
// is, false otherwise.
func (e *notRunningAsServiceError) Is(err error) bool {
	_, ok := err.(*notRunningAsServiceError)
	return ok
}

// serviceHandler is the OS specific implementation interface.
type serviceHandler interface {
	// register registers the application into the service manager. It will
	// perform different steps depending on the OS in question.
	register(ctx context.Context) error
	// setState changes the service state with the service manager.
	setState(ctx context.Context, state State) error
	// serviceID returns the service implementation ID.
	serviceID() string
	// shouldHandleSignal indicates whether the handler should handle the SIGTERM
	// (and other signals like SIGINT, SIGQUIT, SIGHUP) signal from the OS.
	shouldHandleSignal(os.Signal) bool
}

// Init initializes the service management channels and signal handling.
func Init(ctx context.Context, cancel context.CancelFunc, serviceName string) error {
	nativeServiceName = serviceName
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGHUP)

	go func() {
		// Stop listening for signals when the context is cancelled.
		defer func() {
			signal.Stop(sigChan)
		}()

		for {
			select {
			case sig := <-sigChan:
				if nativeHandler.shouldHandleSignal(sig) {
					galog.Infof("GCE Guest Agent got signal: %d, leaving...", sig)
					cancel()
					return
				}
			case <-ctx.Done():
				return
			case <-serviceManagerSignal:
				// Cancels the context in case the OS service manager notifies us it's
				// shutting down/stopping.
				cancel()
				return
			}
		}
	}()

	if err := nativeHandler.register(ctx); err != nil {
		if errors.Is(err, &notRunningAsServiceError{}) {
			galog.Infof("Skipping service initialization as the agent is not running as a service.")
			return nil
		}
		return fmt.Errorf("failed to register with service manager: %w", err)
	}
	return nil
}

func registerNativeHandler(handler serviceHandler) {
	nativeHandler = handler
}

// SetState wraps the OS specific implementation for SetState operation.
func SetState(ctx context.Context, state State) error {
	return nativeHandler.setState(ctx, state)
}
