//go:build windows
// +build windows

/*
** Job Arranger for ZABBIX
** Copyright (C) 2025 Daiwa Institute of Research Ltd. All Rights Reserved.
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
**/

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"time"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/eventlog"
	"golang.org/x/sys/windows/svc/mgr"
)

const (
	startTypeAutomatic = "automatic"
	startTypeDelayed   = "delayed"
	startTypeManual    = "manual"
	startTypeDisabled  = "disabled"
)

var (
	serviceName = "Job Arranger Agent"

	svcInstallFlag   bool
	svcUninstallFlag bool
	svcStartFlag     bool
	svcStopFlag      bool
	svcStartType     string

	winServiceRun bool

	eLog *eventlog.Log

	winServiceWg sync.WaitGroup
	fatalStopWg  sync.WaitGroup

	fatalStopChan chan bool
	startChan     chan bool
	stopChan      = make(chan bool)
	CloseChan     = make(chan bool)
)

func setServiceRun(foreground bool) {
	winServiceRun = !foreground
}

func isWinLauncher() bool {
	if svcInstallFlag || svcUninstallFlag || svcStartFlag || svcStopFlag ||
		svcStartType != "" {
		return true
	}
	return false
}

func openEventLog() (err error) {
	if isWinLauncher() {
		eLog, err = eventlog.Open(serviceName)
		return err
	}
	return
}

func eventLogInfo(msg string) (err error) {
	if isWinLauncher() {
		return eLog.Info(1, msg)
	}
	return nil
}

func closeEventLog() {
	if eLog != nil {
		eLog.Close()
	}
}

func getAgentPath() (p string, err error) {
	if p, err = filepath.Abs(os.Args[0]); err != nil {
		return
	}

	var i os.FileInfo
	if i, err = os.Stat(p); err != nil {
		if filepath.Ext(p) == "" {
			p += ".exe"
			i, err = os.Stat(p)
			if err != nil {
				return
			}
		}
	}

	if i.Mode().IsDir() {
		return p, fmt.Errorf("incorrect path to executable '%s'", p)
	}

	return
}

func svcStartTypeFlagParse() (uint32, bool, error) {
	var startType uint32
	var delayedAutoStart bool
	var err error

	switch svcStartType {
	case "":
		startType = mgr.StartAutomatic
	case startTypeAutomatic:
		startType = mgr.StartAutomatic
	case startTypeDelayed:
		delayedAutoStart = true
		startType = mgr.StartAutomatic
	case startTypeManual:
		startType = mgr.StartManual
	case startTypeDisabled:
		startType = mgr.StartDisabled
	default:
		err = fmt.Errorf("unknown service start type: '%s'", svcStartType)
	}

	return startType, delayedAutoStart, err
}

// Service management functions
func svcInstall(conf string) error {
	exepath, err := getAgentPath()
	if err != nil {
		return fmt.Errorf("failed to get Job Arranger Agent executable path: %s", err.Error())
	}

	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf(
			"failed to connect to service manager: %s",
			err.Error(),
		)
	}
	defer m.Disconnect()

	s, err := m.OpenService(serviceName)
	if err == nil {
		s.Close()
		return fmt.Errorf("service already exists")
	}

	startType, delayedAutoStart, err := svcStartTypeFlagParse()
	if err != nil {
		return fmt.Errorf("failed to get new startup type: %s", err.Error())
	}

	s, err = m.CreateService(
		serviceName,
		exepath,
		mgr.Config{
			StartType:        startType,
			DisplayName:      serviceName,
			Description:      "Provides job scheduling",
			BinaryPathName:   fmt.Sprintf("%s -config-path %s", exepath, conf),
			Dependencies:     []string{"EventLog", "Tcpip"},
			DelayedAutoStart: delayedAutoStart,
		},
		"-config-path", conf,
		"-go-plugin-id", "12",
		"-target-server", "agent",
		"-use-tcp",
	)
	if err != nil {
		return fmt.Errorf("failed to create service: %s", err.Error())
	}
	defer s.Close()

	if err = eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
		err = fmt.Errorf(
			"failed to report service into the event log: %s",
			err.Error(),
		)
		derr := s.Delete()
		if derr != nil {
			return fmt.Errorf("%s and %s", err.Error(), derr.Error())
		}

		return err
	}
	return nil
}

func svcUninstall() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf(
			"failed to connect to service manager: %s",
			err.Error(),
		)
	}
	defer m.Disconnect()

	s, err := m.OpenService(serviceName)
	if err != nil {
		return fmt.Errorf("failed to open service: %s", err.Error())
	}
	defer s.Close()

	if err = s.Delete(); err != nil {
		return fmt.Errorf("failed to delete service: %s", err.Error())
	}

	if err = eventlog.Remove(serviceName); err != nil {
		return fmt.Errorf(
			"failed to remove service from the event log: %s",
			err.Error(),
		)
	}

	return nil
}

func svcStart(conf string) error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf(
			"failed to connect to service manager: %s",
			err.Error(),
		)
	}
	defer m.Disconnect()

	s, err := m.OpenService(serviceName)
	if err != nil {
		return fmt.Errorf("failed to open service: %s", err.Error())
	}
	defer s.Close()

	if err = s.Start("-c", conf); err != nil {
		return fmt.Errorf("failed to start service: %s", err.Error())
	}

	return nil
}

func svcStop() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf(
			"failed to connect to service manager: %s",
			err.Error(),
		)
	}
	defer m.Disconnect()

	s, err := m.OpenService(serviceName)
	if err != nil {
		return fmt.Errorf("failed to open service: %s", err.Error())
	}
	defer s.Close()

	status, err := s.Control(svc.Stop)
	if err != nil && err != windows.ERROR_SERVICE_NOT_ACTIVE {
		return fmt.Errorf(
			"failed to send stop request to service: %s",
			err.Error(),
		)
	}

	timeout := time.Now().Add(10 * time.Second)
	for status.State != svc.Stopped {
		if timeout.Before(time.Now()) {
			return fmt.Errorf("failed to stop '%s' service", serviceName)
		}
		time.Sleep(300 * time.Millisecond)
		if status, err = s.Query(); err != nil {
			return fmt.Errorf("failed to get service status: %s", err.Error())
		}
	}

	return nil
}

func svcStartupTypeSet() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf("failed to connect to service manager: %s", err.Error())
	}

	defer m.Disconnect()

	s, err := m.OpenService(serviceName)
	if err != nil {
		return fmt.Errorf("failed to open service: %s", err.Error())
	}

	defer s.Close()

	c, err := s.Config()
	if err != nil {
		return fmt.Errorf("failed to retrieve service config: %s", err.Error())
	}

	c.StartType, c.DelayedAutoStart, err = svcStartTypeFlagParse()
	if err != nil {
		return fmt.Errorf("failed to get new startup type: %s", err.Error())
	}

	err = s.UpdateConfig(c)
	if err != nil {
		return fmt.Errorf("failed to update service config: %s", err.Error())
	}

	return nil
}

// Handle service management: install, uninstall, start, stop, set start type
func resolveWindowsService(confPath string) error {
	var msg string
	switch true {
	case svcInstallFlag:
		if err := svcInstall(confPath); err != nil {
			return fmt.Errorf(
				"failed to install %s as service: %s",
				serviceName,
				err,
			)
		}
		msg = fmt.Sprintf("'%s' installed successfully", serviceName)
	case svcUninstallFlag:
		if err := svcUninstall(); err != nil {
			return fmt.Errorf(
				"failed to uninstall %s as service: %s",
				serviceName,
				err,
			)
		}
		msg = fmt.Sprintf("'%s' uninstalled successfully", serviceName)
	case svcStartFlag:
		if err := svcStart(confPath); err != nil {
			return fmt.Errorf(
				"failed to start %s service: %s",
				serviceName,
				err,
			)
		}
		msg = fmt.Sprintf("'%s' started successfully", serviceName)
	case svcStopFlag:
		if err := svcStop(); err != nil {
			return fmt.Errorf("failed to stop %s service: %s", serviceName, err)
		}
		msg = fmt.Sprintf("'%s' stopped successfully", serviceName)
	case svcStartType != "":
		if err := svcStartupTypeSet(); err != nil {
			return fmt.Errorf("failed to set service '%s' startup type: %s", serviceName, err.Error())
		}
		msg = fmt.Sprintf("service '%s' startup type configured successfully", serviceName)
	}

	msg = fmt.Sprintf("jobarg_agentd2 [%d]: %s\n", os.Getpid(), msg)
	fmt.Fprint(os.Stdout, msg)
	// if err := eventLogInfo(msg); err != nil {
	// 	return fmt.Errorf("failed to log to event log: %s", err)
	// }
	return nil
}

func handleWindowsService(confPath string) error {
	if svcInstallFlag || svcUninstallFlag || svcStartFlag || svcStopFlag || svcStartType != "" {
		absPath, err := filepath.Abs(confPath)
		if err != nil {
			return err
		}
		if err := resolveWindowsService(absPath); err != nil {
			return err
		}
		closeEventLog()
		os.Exit(0)
	}

	if winServiceRun {
		fatalStopChan = make(chan bool, 1)
		startChan = make(chan bool)
		go runService()
	}

	return nil
}

func runService() {
	if err := svc.Run(serviceName, &winService{}); err != nil {
		panic(fmt.Errorf("use foreground option to run Zabbix agent as console application: %s", err.Error()))
	}
}

func confirmService() {
	if winServiceRun {
		startChan <- true
	}
}

func sendServiceStop() {
	if winServiceRun {
		winServiceWg.Add(1)
		stopChan <- true
	}
}

func waitServiceClose() {
	if winServiceRun {
		winServiceWg.Done()
		<-CloseChan
	}
}

func fatalCloseOSItems() {
	if winServiceRun {
		sendFatalStopSig()
	}
	closeEventLog()
}

func sendFatalStopSig() {
	fatalStopWg.Add(1)
	select {
	case fatalStopChan <- true:
		fatalStopWg.Wait()
	default:
		fatalStopWg.Done()
	}
}

// Win service and execution method
type winService struct{}

// Execute implements svc.Handler.
func (w *winService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (svcSpecificEC bool, exitCode uint32) {
	changes <- svc.Status{State: svc.StartPending}
	select {
	case <-startChan:
		changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
	case <-fatalStopChan:
		changes <- svc.Status{State: svc.Stopped}
		// This is needed to make sure that windows will receive the status stopped before job arranger agent process ends
		<-time.After(time.Millisecond * 500)
		fatalStopWg.Done()
		return
	}

loop:
	for {
		select {
		case c := <-r:
			switch c.Cmd {
			case svc.Stop, svc.Shutdown:
				changes <- svc.Status{State: svc.StopPending}
				winServiceWg.Add(1)
				CloseChan <- true
				winServiceWg.Wait()
				changes <- svc.Status{State: svc.Stopped}
				// This is needed to make sure that windows will receive the status stopped before job arranger agent process ends
				<-time.After(time.Millisecond * 500)
				CloseChan <- true
				break loop
			default:
				fmt.Printf("unsupported windows service command '%s' received", getCmdName(c.Cmd))
			}
		case <-stopChan:
			fmt.Printf("Stop chan received!!")
			changes <- svc.Status{State: svc.StopPending}
			winServiceWg.Wait()
			changes <- svc.Status{State: svc.Stopped}
			// This is needed to make sure that windows will receive the status stopped before job arranger agent process ends
			<-time.After(time.Millisecond * 500)
			CloseChan <- true
			break loop
		}
	}

	return
}

func getCmdName(cmd svc.Cmd) string {
	switch cmd {
	case svc.Stop:
		return "Stop"
	case svc.Pause:
		return "Pause"
	case svc.Continue:
		return "Continue"
	case svc.Interrogate:
		return "Interrogate"
	case svc.Shutdown:
		return "Shutdown"
	case svc.ParamChange:
		return "ParamChange"
	case svc.NetBindAdd:
		return "NetBindAdd"
	case svc.NetBindRemove:
		return "NetBindRemove"
	case svc.NetBindEnable:
		return "NetBindEnable"
	case svc.NetBindDisable:
		return "NetBindDisable"
	case svc.DeviceEvent:
		return "DeviceEvent"
	case svc.HardwareProfileChange:
		return "HardwareProfileChange"
	case svc.PowerEvent:
		return "PowerEvent"
	case svc.SessionChange:
		return "SessionChange"
	default:
		return "unknown"
	}
}
