from __future__ import annotations

import errno
import os
import stat
import sys
from abc import ABC, abstractmethod
from typing import IO

from shuntly.record import ShuntlyRecord


class Sink(ABC):
    @abstractmethod
    def write(self, record: ShuntlyRecord) -> None: ...

    def close(self) -> None:
        pass


class SinkStream(Sink):
    def __init__(self, stream: IO[str] | None = None):
        self._stream = stream or sys.stderr

    def write(self, record: ShuntlyRecord) -> None:
        self._stream.write(record.to_json() + '\n')
        self._stream.flush()


class SinkFile(Sink):
    def __init__(self, path: str):
        self._path = path
        self._file: IO[str] | None = None

    def _ensure_open(self) -> IO[str]:
        if self._file is None:
            self._file = open(self._path, 'a')
        return self._file

    def write(self, record: ShuntlyRecord) -> None:
        f = self._ensure_open()
        f.write(record.to_json() + '\n')
        f.flush()

    def close(self) -> None:
        if self._file is not None:
            self._file.close()
            self._file = None


class SinkPipe(Sink):
    """A Sink that writes to a named pipe.

    Opens non-blocking to avoid hanging if no reader is connected, but writes
    in a loop to ensure complete records. Fails gracefully if the reader
    disconnects.
    """

    def __init__(self, path: str):
        self._path = path
        self._fd: int | None = None

    def _ensure_open(self) -> int | None:
        if self._fd is not None:
            return self._fd

        if not os.path.exists(self._path):
            os.mkfifo(self._path)
        elif not stat.S_ISFIFO(os.stat(self._path).st_mode):
            raise ValueError(f'{self._path} exists and is not a FIFO')

        try:
            self._fd = os.open(self._path, os.O_WRONLY | os.O_NONBLOCK)
        except OSError as e:
            if e.errno == errno.ENXIO:  # No reader
                return None
            raise

        return self._fd

    def write(self, record: ShuntlyRecord) -> None:
        fd = self._ensure_open()
        if fd is None:
            return

        data = (record.to_json() + '\n').encode()
        offset = 0
        while offset < len(data):
            try:
                written = os.write(fd, data[offset:])
                offset += written
            except OSError as e:
                if e.errno == errno.EAGAIN:
                    # Buffer full — spin until space available
                    continue
                if e.errno == errno.EPIPE:
                    # Reader disconnected
                    self.close()
                    return
                raise

    def close(self) -> None:
        if self._fd is not None:
            os.close(self._fd)
            self._fd = None


class SinkMany(Sink):
    def __init__(self, sinks: list[Sink]):
        self._sinks = sinks

    def write(self, record: ShuntlyRecord) -> None:
        for sink in self._sinks:
            sink.write(record)

    def close(self) -> None:
        for sink in self._sinks:
            sink.close()
