//  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.

//go:build windows

package daemon

import (
	"context"
	"fmt"
	"time"

	"github.com/GoogleCloudPlatform/galog"
	"github.com/GoogleCloudPlatform/google-guest-agent/internal/retry"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

const (
	// GuestAgent is the name of the guest agent daemon.
	GuestAgent = "GCEAgent"
	// GuestAgentManager is the name of the guest agent manager daemon.
	GuestAgentManager = "GCEAgentManager"
	// GuestAgentCompatManager is the name of the guest agent compat manager
	// daemon.
	GuestAgentCompatManager = "GCEWindowsCompatManager"
)

// serviceState is a map of windows service states to strings.
var serviceState = map[svc.State]string{
	1: "Stopped",
	2: "StartPending",
	3: "StopPending",
	4: "Running",
	5: "ContinuePending",
	6: "PausePending",
	7: "Paused",
}

func init() {
	// Client is the client for interacting with windows services.
	Client = winServiceClient{}
}

// winServiceClient is the windows implementation of ClientInterface.
type winServiceClient struct{}

func (w winServiceClient) RestartService(ctx context.Context, service string, method RestartMethod) error {
	if err := w.StopDaemon(ctx, service); err != nil {
		return fmt.Errorf("failed to stop service %q: %w", service, err)
	}
	if err := w.StartDaemon(ctx, service); err != nil {
		return fmt.Errorf("failed to start service %q: %w", service, err)
	}
	return nil
}

func (winServiceClient) CheckUnitExists(ctx context.Context, unit string) (bool, error) {
	return false, fmt.Errorf("checking unit existence not supported on Windows")
}

func (winServiceClient) ReloadDaemon(ctx context.Context, daemon string) error {
	return fmt.Errorf("reloading daemons not supported on Windows")
}

func (winServiceClient) UnitStatus(ctx context.Context, unit string) (ServiceStatus, error) {
	mgr, ctrlr, err := openSvcController(unit)
	defer closeSvcController(ctrlr, mgr)
	if err != nil {
		return Unknown, fmt.Errorf("failed to open service controller for %q: %w", unit, err)
	}

	status, err := ctrlr.Query()
	if err != nil {
		return Unknown, fmt.Errorf("failed to query service %q: %w", unit, err)
	}

	switch status.State {
	case svc.Running:
		return Active, nil
	case svc.Stopped:
		return Inactive, nil
	default:
		return Unknown, fmt.Errorf("unknown service state: %q", serviceState[status.State])
	}
}

func (winServiceClient) StopDaemon(ctx context.Context, daemon string) error {
	mgr, ctrlr, err := openSvcController(daemon)
	defer closeSvcController(ctrlr, mgr)
	if err != nil {
		return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
	}

	if _, err := ctrlr.Control(svc.Stop); err != nil {
		return fmt.Errorf("failed to stop service %q: %w", daemon, err)
	}

	return waitForState(ctx, ctrlr, svc.Stopped)
}

func (winServiceClient) EnableService(ctx context.Context, daemon string) error {
	m, ctrlr, err := openSvcController(daemon)
	defer closeSvcController(ctrlr, m)
	if err != nil {
		return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
	}

	currentConfig, err := ctrlr.Config()
	if err != nil {
		return fmt.Errorf("failed to get current config for service %q: %w", daemon, err)
	}

	// Set the start type to Automatic Delayed Start, this matches the current
	// set up for the guest agent services.
	currentConfig.StartType = mgr.StartAutomatic
	currentConfig.DelayedAutoStart = true
	return ctrlr.UpdateConfig(currentConfig)
}

func (winServiceClient) DisableService(ctx context.Context, daemon string) error {
	m, ctrlr, err := openSvcController(daemon)
	defer closeSvcController(ctrlr, m)
	if err != nil {
		return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
	}

	currentConfig, err := ctrlr.Config()
	if err != nil {
		return fmt.Errorf("failed to get current config for service %q: %w", daemon, err)
	}

	currentConfig.StartType = mgr.StartDisabled
	return ctrlr.UpdateConfig(currentConfig)
}

func (winServiceClient) StartDaemon(ctx context.Context, daemon string) error {
	mgr, ctrlr, err := openSvcController(daemon)
	defer closeSvcController(ctrlr, mgr)
	if err != nil {
		return fmt.Errorf("failed to open service controller for %q: %w", daemon, err)
	}

	if err := ctrlr.Start(); err != nil {
		return fmt.Errorf("failed to start service %q: %w", daemon, err)
	}

	return waitForState(ctx, ctrlr, svc.Running)
}

// waitForState waits for a service to reach a specific state. It returns an
// error if the service does not reach the expected state within timeout.
func waitForState(ctx context.Context, ctrlr *mgr.Service, state svc.State) error {
	waitFunc := func() error {
		status, err := ctrlr.Query()
		if err != nil {
			return fmt.Errorf("failed to query service %q: %w", ctrlr.Name, err)
		}
		if status.State != state {
			return fmt.Errorf("service %q expected state %q, got %q", ctrlr.Name, serviceState[state], serviceState[status.State])
		}
		galog.Infof("Service %q reached state %q", ctrlr.Name, serviceState[state])
		return nil
	}

	policy := retry.Policy{MaxAttempts: 5, BackoffFactor: 2, Jitter: time.Second * 2}
	return retry.Run(ctx, policy, waitFunc)
}

// openSvcController connects to the windows service controller manager and
// opens a controller for a given service name. It returns service manager and
// [name] service controller. Make sure to call closeSvcController to disconnect
// and release all resources.
func openSvcController(name string) (*mgr.Mgr, *mgr.Service, error) {
	m, err := mgr.Connect()
	if err != nil {
		return m, nil, fmt.Errorf("failed to connect to windows services: %w", err)
	}

	svcCtrlr, err := m.OpenService(name)
	if err != nil {
		return m, svcCtrlr, fmt.Errorf("failed to open service for %q: %w", name, err)
	}

	return m, svcCtrlr, nil
}

// closeSvcController closes the service controller and disconnects from the
// service controller manager.
func closeSvcController(svcCtrlr *mgr.Service, svc *mgr.Mgr) {
	if svcCtrlr != nil {
		if err := svcCtrlr.Close(); err != nil {
			galog.Warnf("Failed to close service controller(%q): %v", svcCtrlr.Name, err)
		}
	}

	if svc != nil {
		if err := svc.Disconnect(); err != nil {
			galog.Warnf("Failed to disconnect from windows services: %v", err)
		}
	}
}
