"""Cross-platform locked file for the database (no separate .lock files).

Unix:    open() + fcntl.flock (advisory, cooperative among processes that flock).
Windows: CreateFileW with FILE_SHARE_READ (OS-enforced, allows readers, blocks writers).

A single file descriptor is opened once for both reading and writing.
The lock is acquired atomically (on Windows) or immediately after open (on Unix),
and the same descriptor is used for the lifetime of the process: first to read
the existing content, then to append new writes.
"""

import logging
import os
import sys
from pathlib import Path

_logger = logging.getLogger(__name__)


def _fatal(msg: str) -> None:
    """Log a fatal error and exit immediately, bypassing exception handlers."""
    _logger.critical(msg)
    os._exit(1)


if sys.platform == "win32":
    import ctypes
    from ctypes import wintypes

    _kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

    _GENERIC_READ = 0x80000000
    _GENERIC_WRITE = 0x40000000
    _FILE_SHARE_READ = 0x00000001
    _OPEN_EXISTING = 3
    _OPEN_ALWAYS = 4
    _FILE_ATTRIBUTE_NORMAL = 0x80
    _FILE_BEGIN = 0
    _FILE_END = 2
    _ERROR_SHARING_VIOLATION = 32
    _INVALID_FILE_SIZE = 0xFFFFFFFF

    _kernel32.CreateFileW.restype = wintypes.HANDLE
    _kernel32.CreateFileW.argtypes = [
        wintypes.LPCWSTR,
        wintypes.DWORD,
        wintypes.DWORD,
        ctypes.c_void_p,
        wintypes.DWORD,
        wintypes.DWORD,
        wintypes.HANDLE,
    ]
    _kernel32.ReadFile.restype = wintypes.BOOL
    _kernel32.ReadFile.argtypes = [
        wintypes.HANDLE,
        ctypes.c_void_p,
        wintypes.DWORD,
        ctypes.POINTER(wintypes.DWORD),
        ctypes.c_void_p,
    ]
    _kernel32.WriteFile.restype = wintypes.BOOL
    _kernel32.WriteFile.argtypes = [
        wintypes.HANDLE,
        ctypes.c_void_p,
        wintypes.DWORD,
        ctypes.POINTER(wintypes.DWORD),
        ctypes.c_void_p,
    ]
    _kernel32.GetFileSize.restype = wintypes.DWORD
    _kernel32.GetFileSize.argtypes = [
        wintypes.HANDLE,
        ctypes.POINTER(wintypes.DWORD),
    ]
    _kernel32.SetFilePointer.restype = wintypes.DWORD
    _kernel32.SetFilePointer.argtypes = [
        wintypes.HANDLE,
        wintypes.LONG,
        ctypes.POINTER(wintypes.LONG),
        wintypes.DWORD,
    ]
    _kernel32.CloseHandle.restype = wintypes.BOOL
    _kernel32.CloseHandle.argtypes = [wintypes.HANDLE]

    def _is_invalid_handle(handle) -> bool:
        return ctypes.c_void_p(handle).value == ctypes.c_void_p(-1).value

else:
    import fcntl


class LockedFile:
    """A file opened with an exclusive write lock.

    Usage::

        f = LockedFile()
        f.open(path)          # open + lock (read+write)
        content = f.read()    # read entire content
        f.write(data)         # append data (seeks to end first)
        f.close()             # release lock + close fd

    Unix:    fcntl.flock (advisory) — read-only callers that don't flock are unaffected.
    Windows: CreateFileW with FILE_SHARE_READ — OS blocks other writers.
    """

    def __init__(self) -> None:
        self._fd: int | None = None  # Unix fd or Windows HANDLE

    def open(self, path: Path, *, create: bool = False) -> None:
        """Open *path* for read+write with an exclusive lock.

        Args:
            path:   File to open and lock.
            create: If True, create the file if it doesn't exist (bootstrap).

        Raises:
            SystemExit: If the file is locked by another process or not found.
        """
        if self._fd is not None:
            return  # Already open (idempotent)

        if sys.platform == "win32":
            self._open_win32(path, create)
        else:
            self._open_unix(path, create)

    def open_and_read(self, path: Path) -> bytes:
        """Open *path* with exclusive lock and read all content.

        Combined operation for efficient use with asyncio.to_thread().
        """
        self.open(path)
        return self.read()

    def read(self) -> bytes:
        """Read the entire file content from the beginning."""
        if self._fd is None:
            raise RuntimeError("LockedFile.read() called on a closed file")

        if sys.platform == "win32":
            return self._read_win32()
        else:
            return self._read_unix()

    def write(self, data: bytes) -> None:
        """Append *data* to the end of the file."""
        if self._fd is None:
            raise RuntimeError("LockedFile.write() called on a closed file")

        if sys.platform == "win32":
            self._write_win32(data)
        else:
            self._write_unix(data)

    def close(self) -> None:
        """Release the lock and close the file."""
        if self._fd is None:
            return
        if sys.platform == "win32":
            _kernel32.CloseHandle(self._fd)
        else:
            os.close(self._fd)
        self._fd = None

    @property
    def is_open(self) -> bool:
        return self._fd is not None

    # -- Unix ----------------------------------------------------------------

    def _open_unix(self, path: Path, create: bool) -> None:
        flags = os.O_RDWR | (os.O_CREAT if create else 0)
        try:
            fd = os.open(path, flags, 0o666)
        except FileNotFoundError:
            _fatal(f"Database file not found: {path.resolve()}")
        try:
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except OSError:
            os.close(fd)
            _fatal(f"🛑 {path.resolve()}: database already locked by another instance")
        self._fd = fd

    def _read_unix(self) -> bytes:
        os.lseek(self._fd, 0, os.SEEK_SET)
        chunks = []
        while True:
            chunk = os.read(self._fd, 1 << 20)  # 1 MiB
            if not chunk:
                break
            chunks.append(chunk)
        return b"".join(chunks)

    def _write_unix(self, data: bytes) -> None:
        os.lseek(self._fd, 0, os.SEEK_END)
        os.write(self._fd, data)

    # -- Windows -------------------------------------------------------------

    def _open_win32(self, path: Path, create: bool) -> None:
        disposition = _OPEN_ALWAYS if create else _OPEN_EXISTING
        handle = _kernel32.CreateFileW(
            str(path),
            _GENERIC_READ | _GENERIC_WRITE,
            _FILE_SHARE_READ,
            None,
            disposition,
            _FILE_ATTRIBUTE_NORMAL,
            None,
        )
        if _is_invalid_handle(handle):
            err = ctypes.get_last_error()
            if err == _ERROR_SHARING_VIOLATION:
                _fatal(
                    f"🛑 {path.resolve()}: database already locked by another instance"
                )
            _fatal(f"Failed to open database {path.resolve()}: Windows error {err}")
        self._fd = handle

    def _read_win32(self) -> bytes:
        _kernel32.SetFilePointer(self._fd, 0, None, _FILE_BEGIN)
        size = _kernel32.GetFileSize(self._fd, None)
        if size == _INVALID_FILE_SIZE:
            raise OSError(
                f"GetFileSize failed: Windows error {ctypes.get_last_error()}"
            )
        if size == 0:
            return b""
        buf = ctypes.create_string_buffer(size)
        bytes_read = wintypes.DWORD()
        ok = _kernel32.ReadFile(self._fd, buf, size, ctypes.byref(bytes_read), None)
        if not ok:
            raise OSError(f"ReadFile failed: Windows error {ctypes.get_last_error()}")
        return buf.raw[: bytes_read.value]

    def _write_win32(self, data: bytes) -> None:
        _kernel32.SetFilePointer(self._fd, 0, None, _FILE_END)
        written = wintypes.DWORD()
        ok = _kernel32.WriteFile(
            self._fd,
            data,
            len(data),
            ctypes.byref(written),
            None,
        )
        if not ok:
            raise OSError(f"WriteFile failed: Windows error {ctypes.get_last_error()}")
