# This code is part of the Ansible collection community.docker, but is an independent component.
# This particular file, and this file only, is based on the Docker SDK for Python (https://github.com/docker/docker-py/)
#
# Copyright (c) 2016-2022 Docker, Inc.
#
# It is licensed under the Apache 2.0 license (see LICENSES/Apache-2.0.txt in this collection)
# SPDX-License-Identifier: Apache-2.0

# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
# Do not use this from other collections or standalone plugins/modules!

from __future__ import annotations

import functools
import io
import time
import traceback
import typing as t

PYWIN32_IMPORT_ERROR: str | None  # pylint: disable=invalid-name
try:
    import pywintypes
    import win32api
    import win32event
    import win32file
    import win32pipe
except ImportError:
    PYWIN32_IMPORT_ERROR = traceback.format_exc()  # pylint: disable=invalid-name
else:
    PYWIN32_IMPORT_ERROR = None  # pylint: disable=invalid-name

if t.TYPE_CHECKING:
    from collections.abc import Buffer, Callable

    _Self = t.TypeVar("_Self")
    _P = t.ParamSpec("_P")
    _R = t.TypeVar("_R")


ERROR_PIPE_BUSY = 0xE7
SECURITY_SQOS_PRESENT = 0x100000
SECURITY_ANONYMOUS = 0

MAXIMUM_RETRY_COUNT = 10


def check_closed(
    f: Callable[t.Concatenate[_Self, _P], _R],
) -> Callable[t.Concatenate[_Self, _P], _R]:
    @functools.wraps(f)
    def wrapped(self: _Self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
        if self._closed:  # type: ignore
            raise RuntimeError("Can not reuse socket after connection was closed.")
        return f(self, *args, **kwargs)

    return wrapped


class NpipeSocket:
    """Partial implementation of the socket API over windows named pipes.
    This implementation is only designed to be used as a client socket,
    and server-specific methods (bind, listen, accept...) are not
    implemented.
    """

    def __init__(self, handle: t.Any | None = None) -> None:
        self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT
        self._handle = handle
        self._address: str | None = None
        self._closed = False
        self.flags: int | None = None

    def accept(self) -> t.NoReturn:
        raise NotImplementedError()

    def bind(self, address: t.Any) -> t.NoReturn:
        raise NotImplementedError()

    def close(self) -> None:
        if self._handle is None:
            raise ValueError("Handle not present")
        self._handle.Close()
        self._closed = True

    @check_closed
    def connect(self, address: str, retry_count: int = 0) -> None:
        try:
            handle = win32file.CreateFile(
                address,
                win32file.GENERIC_READ | win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_EXISTING,
                (
                    SECURITY_ANONYMOUS
                    | SECURITY_SQOS_PRESENT
                    | win32file.FILE_FLAG_OVERLAPPED
                ),
                0,
            )
        except win32pipe.error as e:
            # See Remarks:
            # https://msdn.microsoft.com/en-us/library/aa365800.aspx
            if e.winerror == ERROR_PIPE_BUSY:
                # Another program or thread has grabbed our pipe instance
                # before we got to it. Wait for availability and attempt to
                # connect again.
                retry_count = retry_count + 1
                if retry_count < MAXIMUM_RETRY_COUNT:
                    time.sleep(1)
                    return self.connect(address, retry_count)
            raise e

        self.flags = win32pipe.GetNamedPipeInfo(handle)[0]  # type: ignore

        self._handle = handle
        self._address = address

    @check_closed
    def connect_ex(self, address: str) -> None:
        self.connect(address)

    @check_closed
    def detach(self) -> t.Any:
        self._closed = True
        return self._handle

    @check_closed
    def dup(self) -> NpipeSocket:
        return NpipeSocket(self._handle)

    def getpeername(self) -> str | None:
        return self._address

    def getsockname(self) -> str | None:
        return self._address

    def getsockopt(
        self, level: t.Any, optname: t.Any, buflen: t.Any = None
    ) -> t.NoReturn:
        raise NotImplementedError()

    def ioctl(self, control: t.Any, option: t.Any) -> t.NoReturn:
        raise NotImplementedError()

    def listen(self, backlog: t.Any) -> t.NoReturn:
        raise NotImplementedError()

    def makefile(self, mode: str, bufsize: int | None = None) -> t.IO[bytes]:
        if mode.strip("b") != "r":
            raise NotImplementedError()
        rawio = NpipeFileIOBase(self)
        if bufsize is None or bufsize <= 0:
            bufsize = io.DEFAULT_BUFFER_SIZE
        return io.BufferedReader(rawio, buffer_size=bufsize)

    @check_closed
    def recv(self, bufsize: int, flags: int = 0) -> str:
        if self._handle is None:
            raise ValueError("Handle not present")
        dummy_err, data = win32file.ReadFile(self._handle, bufsize)
        return data

    @check_closed
    def recvfrom(self, bufsize: int, flags: int = 0) -> tuple[str, str | None]:
        data = self.recv(bufsize, flags)
        return (data, self._address)

    @check_closed
    def recvfrom_into(
        self, buf: Buffer, nbytes: int = 0, flags: int = 0
    ) -> tuple[int, str | None]:
        return self.recv_into(buf, nbytes), self._address

    @check_closed
    def recv_into(self, buf: Buffer, nbytes: int = 0) -> int:
        if self._handle is None:
            raise ValueError("Handle not present")
        readbuf = buf if isinstance(buf, memoryview) else memoryview(buf)

        event = win32event.CreateEvent(None, True, True, None)
        try:
            overlapped = pywintypes.OVERLAPPED()
            overlapped.hEvent = event
            dummy_err, dummy_data = win32file.ReadFile(  # type: ignore
                self._handle, readbuf[:nbytes] if nbytes else readbuf, overlapped
            )
            wait_result = win32event.WaitForSingleObject(event, self._timeout)
            if wait_result == win32event.WAIT_TIMEOUT:
                win32file.CancelIo(self._handle)
                raise TimeoutError
            return win32file.GetOverlappedResult(self._handle, overlapped, 0)
        finally:
            win32api.CloseHandle(event)

    @check_closed
    def send(self, string: Buffer, flags: int = 0) -> int:
        if self._handle is None:
            raise ValueError("Handle not present")
        event = win32event.CreateEvent(None, True, True, None)
        try:
            overlapped = pywintypes.OVERLAPPED()
            overlapped.hEvent = event
            win32file.WriteFile(self._handle, string, overlapped)  # type: ignore
            wait_result = win32event.WaitForSingleObject(event, self._timeout)
            if wait_result == win32event.WAIT_TIMEOUT:
                win32file.CancelIo(self._handle)
                raise TimeoutError
            return win32file.GetOverlappedResult(self._handle, overlapped, 0)
        finally:
            win32api.CloseHandle(event)

    @check_closed
    def sendall(self, string: Buffer, flags: int = 0) -> int:
        return self.send(string, flags)

    @check_closed
    def sendto(self, string: Buffer, address: str) -> int:
        self.connect(address)
        return self.send(string)

    def setblocking(self, flag: bool) -> None:
        if flag:
            return self.settimeout(None)
        return self.settimeout(0)

    def settimeout(self, value: int | float | None) -> None:
        if value is None:
            # Blocking mode
            self._timeout = win32event.INFINITE
        elif not isinstance(value, (float, int)) or value < 0:
            raise ValueError("Timeout value out of range")
        else:
            # Timeout mode - Value converted to milliseconds
            self._timeout = int(value * 1000)

    def gettimeout(self) -> int | float | None:
        return self._timeout

    def setsockopt(self, level: t.Any, optname: t.Any, value: t.Any) -> t.NoReturn:
        raise NotImplementedError()

    @check_closed
    def shutdown(self, how: t.Any) -> None:
        return self.close()


class NpipeFileIOBase(io.RawIOBase):
    def __init__(self, npipe_socket: NpipeSocket | None) -> None:
        self.sock = npipe_socket

    def close(self) -> None:
        super().close()
        self.sock = None

    def fileno(self) -> int:
        if self.sock is None:
            raise RuntimeError("socket is closed")
        # TODO: This is definitely a bug, NpipeSocket.fileno() does not exist!
        return self.sock.fileno()  # type: ignore

    def isatty(self) -> bool:
        return False

    def readable(self) -> bool:
        return True

    def readinto(self, buf: Buffer) -> int:
        if self.sock is None:
            raise RuntimeError("socket is closed")
        return self.sock.recv_into(buf)

    def seekable(self) -> bool:
        return False

    def writable(self) -> bool:
        return False
