#!/usr/bin/env python3

import fcntl
import errno
import os
import struct
import subprocess
import sys
import tempfile

from posix_parity import cleanup_dir
from posix_parity import compare_calls
from posix_parity import fail
from posix_parity import join
from posix_parity import mergerfs_mount
from posix_parity import temp_dir
from posix_parity import touch


def pack_flock(l_type, l_whence=0, l_start=0, l_len=0, l_pid=0):
    return struct.pack("hhqqi", l_type, l_whence, l_start, l_len, l_pid)


def try_process_lock(path):
    fd = os.open(path, os.O_RDWR)
    try:
        try:
            fcntl.fcntl(fd, fcntl.F_SETLK, pack_flock(fcntl.F_WRLCK))
            return 0
        except OSError as exc:
            return exc.errno
    finally:
        os.close(fd)


def child_lock_errno(path):
    proc = subprocess.run(
        [sys.executable, __file__, "--try-process-lock", path],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        check=False,
    )
    if proc.returncode != 0:
        raise RuntimeError(proc.stderr.strip() or f"child lock helper failed: {proc.returncode}")
    return int(proc.stdout.strip())


def main():
    if len(sys.argv) == 3 and sys.argv[1] == "--try-process-lock":
        print(try_process_lock(sys.argv[2]))
        return 0

    try:
        with mergerfs_mount() as (mount, _):
            with tempfile.TemporaryDirectory() as native:
                merge_base = temp_dir(mount)
                try:
                    native_base = join(native, os.path.basename(merge_base))
                    os.makedirs(native_base, exist_ok=True)

                    merge_file = join(merge_base, "file")
                    native_file = join(native_base, "file")
                    touch(merge_file, b"x")
                    touch(native_file, b"x")

                    mfd1 = os.open(merge_file, os.O_RDWR)
                    mfd2 = os.open(merge_file, os.O_RDWR)
                    nfd1 = os.open(native_file, os.O_RDWR)
                    nfd2 = os.open(native_file, os.O_RDWR)

                    try:
                        err = compare_calls(
                            "flock EX nonblock",
                            lambda: fcntl.flock(mfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
                            lambda: fcntl.flock(nfd1, fcntl.LOCK_EX | fcntl.LOCK_NB),
                        )
                        if err:
                            return fail(err)

                        err = compare_calls(
                            "flock SH nonblock second fd",
                            lambda: fcntl.flock(mfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
                            lambda: fcntl.flock(nfd2, fcntl.LOCK_SH | fcntl.LOCK_NB),
                        )
                        if err:
                            return fail(err)

                        err = compare_calls("flock unlock", lambda: fcntl.flock(mfd1, fcntl.LOCK_UN), lambda: fcntl.flock(nfd1, fcntl.LOCK_UN))
                        if err:
                            return fail(err)
                        err = compare_calls("flock unlock second fd", lambda: fcntl.flock(mfd2, fcntl.LOCK_UN), lambda: fcntl.flock(nfd2, fcntl.LOCK_UN))
                        if err:
                            return fail(err)

                        wrlk = pack_flock(fcntl.F_WRLCK)
                        unlck = pack_flock(fcntl.F_UNLCK)

                        err = compare_calls("fcntl F_SETLK write lock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, wrlk), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, wrlk))
                        if err:
                            return fail(err)

                        m_lock_errno = child_lock_errno(merge_file)
                        n_lock_errno = child_lock_errno(native_file)
                        if m_lock_errno != n_lock_errno:
                            return fail(f"fcntl F_SETLK child contention errno mismatch mergerfs={m_lock_errno} native={n_lock_errno}")
                        if m_lock_errno not in (errno.EACCES, errno.EAGAIN):
                            return fail(f"fcntl F_SETLK child contention expected lock conflict got errno={m_lock_errno}")

                        err = compare_calls("fcntl F_SETLK unlock", lambda: fcntl.fcntl(mfd1, fcntl.F_SETLK, unlck), lambda: fcntl.fcntl(nfd1, fcntl.F_SETLK, unlck))
                        if err:
                            return fail(err)

                        bad_m = os.open(merge_file, os.O_RDONLY)
                        bad_n = os.open(native_file, os.O_RDONLY)
                        os.close(bad_m)
                        os.close(bad_n)
                        err = compare_calls(
                            "fcntl EBADF",
                            lambda: fcntl.fcntl(bad_m, fcntl.F_GETFD),
                            lambda: fcntl.fcntl(bad_n, fcntl.F_GETFD),
                        )
                        if err:
                            return fail(err)
                    finally:
                        os.close(mfd1)
                        os.close(mfd2)
                        os.close(nfd1)
                        os.close(nfd2)

                    return 0
                finally:
                    cleanup_dir(merge_base)
    except RuntimeError as exc:
        print(str(exc), end="")
        return 77


if __name__ == "__main__":
    raise SystemExit(main())
