package sd.coordinator;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

import sd.model.Message;
import sd.serialization.MessageSerializer;
import sd.serialization.SerializationException;
import sd.serialization.SerializerFactory;

/**
 * Abstração de cliente TCP para comunicação outbound (de saída) com nós da rede.
 * <p>
 * Esta classe encapsula a gestão do socket raw, oferecendo uma interface de alto nível
 * para envio de objetos {@link Message}. Implementa o protocolo de camada de aplicação
 * proprietário, garantindo a serialização correta e o enquadramento (framing) dos dados
 * na stream TCP.
 * <p>
 * É utilizada pelo Coordenador para controlar Interseções e enviar telemetria para o Dashboard.
 */
public class SocketClient {
    
    private final String intersectionId;
    private final String host;
    private final int port;
    private Socket socket;
    private OutputStream outputStream;
    private MessageSerializer serializer;
    
    /**
     * Instancia um novo cliente socket configurado para um destino específico.
     *
     * @param intersectionId Identificador lógico do nó de destino (ex: "Cr1", "Dashboard").
     * @param host Endereço IP ou hostname do destino.
     * @param port Porta TCP de escuta do destino.
     */
    public SocketClient(String intersectionId, String host, int port) {
        this.intersectionId = intersectionId;
        this.host = host;
        this.port = port;
        this.serializer = SerializerFactory.createDefault();
    }
    
    /**
     * Estabelece a conexão TCP (Handshake SYN/ACK) com o host remoto.
     * * @throws IOException Se o host for inalcançável ou a conexão for recusada.
     */
    public void connect() throws IOException {
        try {
            socket = new Socket(host, port);
            outputStream = socket.getOutputStream();
            System.out.println("Connected to " + intersectionId + " at " + host + ":" + port);
        } catch (IOException e) {
            System.err.println("Failed to connect to " + intersectionId + " at " + host + ":" + port);
            throw e;
        }
    }
    
    /**
     * Serializa e transmite uma mensagem através do socket conectado.
     * <p>
     * <b>Protocolo de Envio (Length-Prefix Framing):</b>
     * <ol>
     * <li>Serializa o objeto {@link Message} para um array de bytes.</li>
     * <li>Calcula o tamanho (N) do array.</li>
     * <li>Escreve um cabeçalho de 4 bytes contendo N (Big-Endian).</li>
     * <li>Escreve os N bytes do payload (corpo da mensagem).</li>
     * <li>Realiza flush no stream para forçar o envio imediato do pacote TCP.</li>
     * </ol>
     * Este mecanismo garante que o recetor saiba exatamente quantos bytes ler,
     * prevenindo problemas de fragmentação ou aglutinação de pacotes TCP.
     *
     * @param message O objeto de domínio a ser enviado.
     * @throws SerializationException Se o objeto não puder ser convertido para bytes.
     * @throws IOException Se houver falha na escrita do socket (ex: conexão resetada).
     */
    public void send(Message message) throws SerializationException, IOException {
        if (socket == null || socket.isClosed()) {
            throw new IOException("Socket is not connected to " + intersectionId);
        }
        
        try {
            byte[] data = serializer.serialize(message);
            
            int length = data.length;
            // Write 4-byte length header (Big Endian)
            outputStream.write((length >> 24) & 0xFF);
            outputStream.write((length >> 16) & 0xFF);
            outputStream.write((length >> 8) & 0xFF);
            outputStream.write(length & 0xFF);
            
            // Write payload
            outputStream.write(data);
            outputStream.flush();
            
        } catch (SerializationException | IOException e) {
            System.err.println("Error sending message to " + intersectionId + ": " + e.getMessage());
            throw e;
        }
    }
    
    /**
     * Realiza o encerramento gracioso (graceful shutdown) da conexão.
     * Liberta os recursos do sistema operativo (descritores de arquivo).
     * <p>
     * Operação idempotente: pode ser chamada múltiplas vezes sem erro.
     */
    public void close() {
        try {
            if (outputStream != null) {
                outputStream.close();
            }
            if (socket != null && !socket.isClosed()) {
                socket.close();
                System.out.println("Closed connection to " + intersectionId);
            }
        } catch (IOException e) {
            System.err.println("Error closing connection to " + intersectionId + ": " + e.getMessage());
        }
    }
    
    /**
     * Verifica o estado atual da conexão.
     * * @return true se o socket estiver instanciado, conectado e aberto; false caso contrário.
     */
    public boolean isConnected() {
        return socket != null && socket.isConnected() && !socket.isClosed();
    }
    
    public String getIntersectionId() {
        return intersectionId;
    }
    
    @Override
    public String toString() {
        return String.format("SocketClient[intersection=%s, host=%s, port=%d, connected=%s]",
            intersectionId, host, port, isConnected());
    }
}