/*
** 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 utils

import (
	"fmt"
	"jobarranger2/src/libs/golibs/common"
	"jobarranger2/src/libs/golibs/logger/logger"
	"os"
	"time"

	"golang.org/x/sys/windows"
)

const (
	CreateIfNotExist    = true
	OpenOnly            = false
	MoveFileWithLock    = true
	MoveFileWithoutLock = false

	LOCKFILE_EXCLUSIVE_LOCK = 0x00000002
	LOCKFILE_SHARE_LOCK     = 0x00000000
	LOCKFILE_FAIL_ON_LOCK   = 0x00000001
)

func getWindowsErrno(err error) (windows.Errno, bool) {
	switch e := err.(type) {
	case windows.Errno:
		return e, true

	case *os.PathError:
		if errno, ok := e.Err.(windows.Errno); ok {
			return errno, true
		}

	case *os.LinkError:
		if errno, ok := e.Err.(windows.Errno); ok {
			return errno, true
		}
	}

	return 0, false
}

// File open function with retry if the file is locked by another process
func OpenFileWithLockRetry(path string, retryCount int, createIfNotExist bool) (*os.File, error) {
	funcName := "OpenFileWithLockRetry"

	var lastErr error
	var lastErrno windows.Errno

	// Base open flags
	openFlags := os.O_RDWR
	if createIfNotExist {
		openFlags |= os.O_CREATE
	}

	for i := 1; i <= retryCount; i++ {
		f, err := os.OpenFile(path, openFlags, 0644)
		if err == nil {
			return f, nil
		}

		lastErr = err
		if errno, ok := getWindowsErrno(err); ok {
			lastErrno = errno

			// ERROR_SHARING_VIOLATION
			if errno == windows.ERROR_SHARING_VIOLATION {
				logger.JaLog(
					"JAUTILS400007",
					logger.Logging{},
					funcName, path, i, retryCount,
				)

				time.Sleep(1 * time.Second)
				continue
			}

			// Other Windows errno → fail fast
			return nil, fmt.Errorf(
				"failed to open file. errno: %d (%s), path: %s, error: %w",
				int(errno), errno.Error(), path, err,
			)
		}
	}

	return nil, fmt.Errorf(
		"failed to open file after retries. path: %s, retryCount: %d, last errno: %d, last error: %w",
		path, retryCount, int(lastErrno), lastErr,
	)
}

// File move function with retry if the file is locked by another process
func MoveFileWithLockRetry(src, dst string, retryCount int) error {
	funcName := "MoveFileWithLockRetry"

	var lastErr error
	var lastErrno windows.Errno

	for i := 1; i <= retryCount; i++ {
		err := os.Rename(src, dst)
		if err == nil {
			return nil
		}

		lastErr = err
		if errno, ok := getWindowsErrno(err); ok {
			lastErrno = errno

			// ERROR_SHARING_VIOLATION
			if errno == windows.ERROR_SHARING_VIOLATION {
				logger.JaLog(
					"JAUTILS400008",
					logger.Logging{},
					funcName, src, dst, i, retryCount,
				)

				time.Sleep(1 * time.Second)
				continue
			}

			// Other Windows error → fail fast
			return fmt.Errorf(
				"failed to rename file. errno: %d (%s), src: %s, dst: %s, error: %w",
				int(errno), errno.Error(), src, dst, err,
			)
		}
	}

	return fmt.Errorf(
		"failed to rename file after retries. src: %s, dst: %s, retryCount: %d, last errno: %d, last error: %w",
		src, dst, retryCount, int(lastErrno), lastErr,
	)
}

// Lock the lock file without retry
func TryLockFile(file *os.File, flags uint32) error {
	handle := windows.Handle(file.Fd())
	var overlapped windows.Overlapped

	// Lock 1 byte from offset 0 — enough to block others
	return windows.LockFileEx(
		handle,
		flags,
		0,    // Reserved
		1, 0, // Lock 1 byte starting at offset 0
		&overlapped,
	)
}

// Lock the lock file with retry
func LockFile(flags uint32) (*os.File, error) {
	funcName := "LockFile"

	for {
		// Open or create the lock file
		file, err := OpenFileWithLockRetry(common.LockFilePath, 30, CreateIfNotExist)
		if err != nil {
			return nil, err
		}

		handle := windows.Handle(file.Fd())
		var overlapped windows.Overlapped

		// Try to acquire the lock (lock 1 byte at offset 0)
		err = windows.LockFileEx(
			handle,
			flags,
			0,    // Reserved
			1, 0, // Lock 1 byte
			&overlapped,
		)

		if err == nil {
			// Lock acquired successfully
			return file, nil
		}

		// Lock failed → close before retry
		file.Close()

		// Retry only if the file is already locked
		if errno, ok := err.(windows.Errno); ok {
			if errno == windows.ERROR_LOCK_VIOLATION ||
				errno == windows.ERROR_SHARING_VIOLATION {
				if common.Manager.Name == common.AgentManagerProcess || common.Manager.Name == common.JobRunClientProcess {
					logger.WriteLog("JAUTILS400009", funcName, flags)
				} else {
					logger.WriteLog("JAUTILS400009", logger.Logging{}, funcName, flags)
				}

				time.Sleep(1 * time.Second)
				continue
			}
		}

		// Any other error → return immediately
		return nil, err
	}
}

// Releases the lock from lock file
func UnlockFile(file *os.File) error {
	handle := windows.Handle(file.Fd())
	var overlapped windows.Overlapped

	// Unlock the first byte (same one used for locking)
	if err := windows.UnlockFileEx(handle, 0, 1, 0, &overlapped); err != nil {
		return err
	}

	// Close the file descriptor
	return file.Close()
}
