//go:build !windows

package memorysearch

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"syscall"
)

// ownerLockFileName is the cross-process lock the Reconciler acquires at
// Start. A second Desktop instance that cannot acquire it must report
// degraded rather than queue or write concurrently — the spec §5.2 single-
// writer + cross-process file lock requirement.
const ownerLockFileName = "owner.lock"

// OwnerLock is the cross-process flock the Reconciler holds for the
// lifetime of a single Desktop instance owning a binding's derivative.
// It mirrors desktop/internal/profile/flock.go but lives in this package
// so the lock path is bound to the derivative root, not the profile
// config dir.
type OwnerLock struct {
	path string
	file *os.File
}

// TryAcquireOwnerLock opens (creating if absent) the lock file under
// derivativeRoot and attempts a non-blocking exclusive flock. Returns
// ErrOwnerLockBusy if another Desktop instance already holds the lock;
// the caller must report degraded and never block waiting. Blocking here
// would violate the spec: a second instance must fail-closed fast.
//
// The lock file path is fixed under the derivative root; the caller
// cannot supply or override it. This is the trust boundary: a runtime-
// supplied path must never land in the lock file location.
func TryAcquireOwnerLock(derivativeRoot string) (*OwnerLock, error) {
	if derivativeRoot == "" {
		return nil, fmt.Errorf("%w: empty derivative root", ErrInvalidBuildOptions)
	}
	if err := os.MkdirAll(derivativeRoot, 0o755); err != nil {
		return nil, fmt.Errorf("memory search: mkdir derivative root: %w", err)
	}
	lockPath := filepath.Join(derivativeRoot, ownerLockFileName)
	f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600)
	if err != nil {
		return nil, fmt.Errorf("memory search: open owner lock %s: %w", lockPath, err)
	}
	if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
		_ = f.Close()
		return nil, fmt.Errorf("%w: %s", ErrOwnerLockBusy, lockPath)
	}
	return &OwnerLock{path: lockPath, file: f}, nil
}

// Release drops the flock and closes the lock file. Safe to call on a
// nil pointer or call multiple times.
func (l *OwnerLock) Release() {
	if l == nil || l.file == nil {
		return
	}
	_ = syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
	_ = l.file.Close()
	l.file = nil
}

// ErrOwnerLockBusy signals that another Desktop instance holds the
// per-agent owner lock. The caller reports degraded and does not write.
var ErrOwnerLockBusy = errors.New("memory search: owner lock held by another process")
