package serverUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;

public class ConnectionManager {
    private static final String ACK_MESSAGE_PREFIX = "<%s>Ack";
    private static final int BUFFER_SIZE = 1024 * 1024;

    ServerSocketChannel serverSocketChannel;
    int port;

    public ConnectionManager(int port) {
        this.port = port;
    }

    public void initialize() throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        System.out.println("Server started on port " + port);
        System.out.println("============ * ============");
    }

    public void registerSelector(Selector selector) throws ClosedChannelException {
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    public void processQuery(Selector selector, SelectionKey key) throws IOException {
        if (!key.isValid()) {
            return;
        }
        if (key.isAcceptable()) {
            acceptConnection(key, selector);
        } else if (key.isReadable()) {
            handleRead(key);
        }
    }
    private void acceptConnection(SelectionKey key, Selector selector) throws IOException {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientSocketChannel = serverSocketChannel.accept();
        clientSocketChannel.configureBlocking(false);
        clientSocketChannel.register(selector, SelectionKey.OP_READ);

        System.out.println("Accepted connection from " + clientSocketChannel.getRemoteAddress());
    }

    private void handleRead(SelectionKey key) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
        SocketChannel clientSocketChannel = (SocketChannel) key.channel();
        int bytesRead;
        while ((bytesRead = clientSocketChannel.read(buffer)) > 0) {
            buffer.flip();

            // проверяем наличие данных в буфере
            if (buffer.remaining() < 8) {
                continue;
            }

            // читаем длину имени файла в байтах
            int nameLength = buffer.getInt();

            // проверяем наличие данных в буфере
            if (buffer.remaining() < nameLength + 4) {
                buffer.position(buffer.position() - 4);
                buffer.compact();
                continue;
            }

            // читаем имя файла
            byte[] nameBytes = new byte[nameLength];
            buffer.get(nameBytes);
            String fileName = new String(nameBytes, StandardCharsets.UTF_8);

            // читаем длину файла в байтах
            int fileLength = buffer.getInt();

            // проверяем наличие данных в буфере
            if (buffer.remaining() < fileLength + 8) {
                buffer.position(buffer.position() - 8);
                buffer.compact();
                continue;
            }

            // читаем содержимое файла
            byte[] fileContent = new byte[fileLength];
            buffer.get(fileContent, 0, fileLength);

            // проверяем разделитель
            long separator = buffer.getLong();
            if (separator != 0) {
                throw new RuntimeException("Invalid separator");
            }

            // очищаем буфер
            buffer.clear();

            // сохраняем файл
            saveFile(fileName, fileContent);

            // отправляем подтверждение о сохранении файла
            sendAck(clientSocketChannel, fileName);
        }

        if (bytesRead == -1) {
            System.out.println("Closed connection with " + clientSocketChannel.getRemoteAddress());
            clientSocketChannel.close();
            key.cancel();
            System.out.println("============ * ============");
        }
    }


    private void saveFile(String filename, byte[] fileContent) throws IOException {
        File file = new File("files\\" + filename);

        System.out.println("Saving file " + filename);

        try (FileOutputStream outputStream = new FileOutputStream(file)) {
            outputStream.write(fileContent);
        }
        System.out.println("File " + filename + " saved successfully");
    }

    private void sendAck(SocketChannel clientSocketChannel, String filename) throws IOException {
        String ackMessage = String.format(ACK_MESSAGE_PREFIX, filename);
        byte[] ackMessageBytes = ackMessage.getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(ackMessageBytes.length + Integer.BYTES);
        buffer.putInt(ackMessageBytes.length);
        buffer.put(ackMessageBytes);
        buffer.flip();

        while (buffer.hasRemaining()) {
            clientSocketChannel.write(buffer);
        }

        System.out.println("Sent acknowledgement for " + filename);
        buffer.clear(); // Очищаем буфер перед чтением новых данных
    }
}

1. Необходимо выполнить главное условие: одновременную обработку нескольких клиентов. Например, если один из клиентов остановился на передаче файла где-то на половине, и чтобы не крутиться в холостом цикле, в это время необходимо обрабатывать запросы других клиентов. Для достижения данного условия необходимо создать контекст соединения и связать его с конкретным соединением (Selection Key) и получать его через метод attachment. В этом же контексте можно хранить название файла, текущий состояние буффера, этап передачи файла. 

2. Так как сокет в неблокирующем режиме, то он не будет блокироваться при считывании данных в буффер и будет просто крутиться в холостом цикле.

3. Ну и лучше не хранить в памяти файл, так как он может не влезть полностью в оперативку, нужно считывать батчами и загружать в файл.