// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

//go:build !windows

package flock

import (
	"context"
	"fmt"
	"io"
	"log"
	"os"
	"syscall"
)

// use fcntl POSIX locks for the most consistent behavior across platforms, and
// hopefully some compatibility over NFS and CIFS.
func Lock(f *os.File) error {
	flock := &syscall.Flock_t{
		Type:   syscall.F_RDLCK | syscall.F_WRLCK,
		Whence: int16(io.SeekStart),
		Start:  0,
		Len:    0,
	}

	return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, flock)
}

// LockBlocking is like Lock except that if the lock is currently contended
// then it blocks until it becomes available.
//
// If the given context is cancelled then it returns early with the cancellation
// error.
func LockBlocking(ctx context.Context, f *os.File) error {
	err := lockBlockingInner(ctx, f)

	// On some platforms (MacOS, Linux, ...?), fcntl tries to detect deadlocks. Unfortunately with multiple
	// processes and multiple goroutines, we may trip the detector erroniously. It thinks that
	// because different processes are installing different providers and have a mix of goroutines
	// (sleeping trying to lock) / (active locked), there must be a deadlock.
	//
	// https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html
	for err == syscall.EDEADLK {
		log.Printf("[INFO] Locking deadlock incorrectly detected by the kernel on %s", f.Name())
		err = lockBlockingInner(ctx, f)
	}

	return err
}
func lockBlockingInner(ctx context.Context, f *os.File) error {
	flock := &syscall.Flock_t{
		Type:   syscall.F_RDLCK | syscall.F_WRLCK,
		Whence: int16(io.SeekStart),
		Start:  0,
		Len:    0,
	}

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	c := make(chan error)
	go func() {
		for {
			err := syscall.FcntlFlock(f.Fd(), syscall.F_SETLKW, flock)
			if err == syscall.EINTR {
				// We'll get here if our process gets any signal at all, but
				// not all signals represent cancellation.
				if ctxErr := ctx.Err(); ctxErr != nil {
					err = ctxErr // return the cancellation error instead of generic EINTR
				} else {
					continue // not cancelled yet
				}
			}
			c <- err
			close(c)
			return
		}
	}()

	for {
		select {
		case err := <-c:
			return err
		case <-ctx.Done():
			// We will get here if the cancellation is caused by anything other
			// than a Unix signal, in which case we'll send a signal ourselves
			// to force the waiting goroutine to exit.
			// We use SIGUSR1 here on the assumption that nothing else in
			// OpenTofu uses it. We're sending this to the current pid
			// explicitly because we might have other processes, such as
			// plugins, also running in our process group (which is what we'd
			// signal if using pid 0 here).
			err := syscall.Kill(syscall.Getpid(), syscall.SIGUSR1)
			if err != nil {
				// This should not fail, but if it does then we'd otherwise
				// get hung up here and so we'll return an error and accept
				// that our background goroutine is going to just hang around
				// until another signal shows up or the program exits.
				return fmt.Errorf("failed canceling lock acquisition: %w", err)
			}
		}
	}
}

func Unlock(f *os.File) error {
	flock := &syscall.Flock_t{
		Type:   syscall.F_UNLCK,
		Whence: int16(io.SeekStart),
		Start:  0,
		Len:    0,
	}

	return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, flock)
}
