using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Channels;
using gatewaySCO.POSBC;
using GatewaySCO;
using Serilog;
using Serilog.Events;

namespace GatewayCHEC.Servidor;



/// <summary>
/// Constructor servidor socket. Usa ip y puerto defualt.
/// El método IniciarAync() activa el servidor.
/// <param name="direccion">Dirección IP servidor, típicamente '127.0.0.1'.</param>
/// <param name="puerto">Número de puerto para el socket, default 11.000</param>
/// <returns>Retorna tipo Task.</returns>
/// </summary>
public class ServidorGatewayCHEC(string ipGateway, int ptoGateway, string ipPOSBC, int ptoPOSBC)
{
    readonly static bool _isDebug = Log.IsEnabled(LogEventLevel.Debug);
    /// <summary>
    /// Dirección ip para vincular el socket.
    /// </summary>
    public string IpGateway { get; private set; } = ipGateway;
    /// <summary>
    /// Puerto a vincular socket.
    /// </summary>
    public Int32 PuertoGateway { get; private set; } = ptoGateway;
    /// <summary>
    /// Dirección ip servidor POSBC.
    /// </summary>
    public string IpPOSBC { get; private set; } = ipPOSBC;
    /// <summary>
    /// Puerto servidro POSBC
    /// </summary>
    public Int32 PuertoPOSBC { get; private set; } = ptoPOSBC;
    /// <summary>
    /// Longitud de la cola de conexión al socket.
    /// </summary>
    public int LongColaConexiones { get; private set; } = 10;
    /// <summary>
    /// Fija timeout usado para enviar / recibir em modo sincrónico, en milisegundos.
    /// Si el timeout se excede, se presenta una excepción. 
    /// Default a 5 segundos.
    /// </summary>
    public int TimeOutSincMs { get; private set; } = 3000;
    /// <summary>
    ///  Servidor POSBC
    /// </summary>
    static ClienteServidorPOSBC _posbc;
    long _numeroConexionesEntrantes = 0;
    static long _nroMsjEntradaCHEC = 0;
    static long _nroMsjEntradaPOSBC = 0;
    static long _nroMsjSalidaCHEC = 0;
    static long _nroMsjSalidaPOSBC = 0;
    static Socket _socketCHEC;

    // Canal de entrada.
    private static readonly Channel<string> _canalEntrada = Channel.CreateUnbounded<string>();
    // Canal de salida.
    private static readonly Channel<string> _canalSalida = Channel.CreateUnbounded<string>();

    /// <summary>
    ///  El POSBC debe ser activado antes de invocar el método Activa.
    /// </summary>
    /// <param name="ip"></param>
    /// <param name="pto"></param>
    private void ActivaPOSBC()
    {
        _posbc = new(IpPOSBC, PuertoPOSBC);
        _posbc.AbreConexion();
    }

    public void Activa()
    {
        IPEndPoint ip = new(IPAddress.Parse(IpGateway), PuertoGateway);
        int cont = 0;
        // Clave usar 'using' para liberar correctamente recursos.
        using Socket tcpSocket = new(
            ip.AddressFamily,
            SocketType.Stream,
            ProtocolType.Tcp);

        // Configuración comportamiento socket tcp.
        // No se permite a otro socket compartir el puerto.
        // TODO - Investigar: cuando servidor y cliente están en la misma instanción de s.o.
        //          no comparten el puerto?.
        // tcpSocket.ExclusiveAddressUse = true;
        // El socket espera los segundos del parámetro para terminar de enviar
        // datos (si hay en el buffer de salida), despues que se llama Socket.Close.
        tcpSocket.LingerState = new LingerOption(true, 3);
        // Desactiva algoritmo Nagle.
        tcpSocket.NoDelay = true;
        // Timeout entrada / salida.
        tcpSocket.ReceiveTimeout = 0;
        tcpSocket.SendTimeout = TimeOutSincMs;
        //tcpSocket.Blocking = false;
        tcpSocket.Bind(ip);
        tcpSocket.Listen(LongColaConexiones);

        // Id proceso. Compilación difiere según .net usado.
        int idProceso = -1;
#if NETFRAMEWORK
                idProceso = Process.GetCurrentProcess().Id;
#elif (NETSTANDARD || NET5_0_OR_GREATER)
        idProceso = Environment.ProcessId;
#endif

        Log.Information("Gateway servidor socket en {ip} : {puerto}, proceso id {id}", IpGateway, PuertoGateway, idProceso);
        if (_isDebug)
        {
            Log.Debug("Versión framework {version} # 13", Environment.Version);
            Log.Debug("Tcp Socket configuración:");
            Log.Debug($"  Blocking  {tcpSocket.Blocking}");
            Log.Debug($"  ExclusiveAddressUse {tcpSocket.ExclusiveAddressUse}");
            Log.Debug($"  LingerState {tcpSocket.LingerState.Enabled}, {tcpSocket.LingerState.LingerTime}");
            Log.Debug($"  NoDelay {tcpSocket.NoDelay}");
            Log.Debug($"  ReceiveBufferSize {tcpSocket.ReceiveBufferSize}");
            Log.Debug($"  ReceiveTimeout {tcpSocket.ReceiveTimeout}");
            Log.Debug($"  SendBufferSize {tcpSocket.SendBufferSize}");
            Log.Debug($"  SendTimeout {tcpSocket.SendTimeout}");
            Log.Debug($"  Ttl {tcpSocket.Ttl}");
            Log.Debug($"  IsBound {tcpSocket.IsBound}");
        }
        try
        {
            //var tareaRecibePOSBC = Task.Run(() => _posbc.Recibe(_canalSalida));

            // Procesa conexiones al Gateway.
            while (true)
            {
                Log.Information("Esperando conexión");
                Socket clienteCHEC = tcpSocket.Accept();
                Log.Information("Conexión remota ip {ip} en puerto {pto}",
                    IPAddress.Parse(((IPEndPoint)clienteCHEC.RemoteEndPoint).Address.ToString()),
                    ((IPEndPoint)clienteCHEC.RemoteEndPoint).Port.ToString());
                // Activa el POSBC
                ActivaPOSBC();
                var tareaAceptaEntradasCHEC = Task.Run(() => AceptaEntradasCHEC(clienteCHEC));
                var tareaRemiteRespuestaPOSBC = Task.Run(() => ProcesaSalidaPOSBC(clienteCHEC, _posbc.Socket));
            }
        }
        finally
        {
            tcpSocket.Close();
            _posbc.CierraConexion();
        }
    }

    /// <summary>
    /// Acepta entrada de datos desde CHEC.
    /// Procesa la entrada.
    /// </summary>
    /// <param name="cliente">Socket de conexión a CHEC.</param>
    /// <returns></returns>
    public async Task AceptaEntradasCHEC(Socket cliente)
    {
        var taskId = Task.CurrentId?.ToString() ?? "no-task";
        _socketCHEC = cliente;
        Log.Debug("{tid} Tarea acepta entradas iniciada", taskId);
        while (true)
        {

            // ------------------- LECTURA SINCRONICA

            // Lee longitud mensaje entrante, 4 bytes.
            Log.Information("{tid} Esperando CHEC", taskId);
            var bufferLongitud = new byte[4];
            int bytesLeidos = 0;
            while (bytesLeidos < 4)
            {
                bytesLeidos += cliente.Receive(bufferLongitud, bytesLeidos, bufferLongitud.Length, SocketFlags.None);
            }
            // Lee porción de datos del mensaje, hasta la longitud indicada en los 4 primeros bytes.
            int longitudMensaje = Util.LongitudCodificada(bufferLongitud);
            Log.Debug("{tid} CHEC -> GTWY entra nuevo mensaje, longitud en cabecera {nroBytes}", taskId, longitudMensaje);
            var bufferEntrada = new byte[longitudMensaje];
            bytesLeidos = 0;
            while (bytesLeidos < longitudMensaje)
            {
                bytesLeidos += cliente.Receive(bufferEntrada, bytesLeidos, bufferEntrada.Length, SocketFlags.None);
            }

            // ---------------------- LECTURA ASINCRONICA

            // Lee los primeros 4 bytes de los datos de entrada, los cuales indican 
            // la longitud del resto del mensaje.
            //int nroBytesLeidos = await cliente.ReceiveAsync(new ArraySegment<byte>(bufferLongitud), SocketFlags.None);


            // Si el número de bytes leidos es cero, la conexión se ha cerrado.
            // El método Receive es sincróno, luego si retorna sin datos, es que no hay conexión.
            // Mientras tenga conexión, se queda esperando datos.
            // if (nroBytesLeidos == 0)
            // {
            //     Log.Warning("{tid} CHEC -> GTWY emite 0 bytes - señal cierra conexión.");
            //     break;
            // }

            // int longitudMensaje = Util.LongitudCodificada(bufferLongitud);
            // Log.Debug("{tid} CHEC -> GTWY bytes cabecera {nroBytes}, longitud cuerpo mensaje {longitud}", taskId, nroBytesLeidos, longitudMensaje);
            // // Prepara buffer de entrada según la longitud esperada del mensaje.
            // var bufferMensaje = new byte[longitudMensaje];
            // nroBytesLeidos = await cliente.ReceiveAsync(new ArraySegment<byte>(bufferMensaje), SocketFlags.None);

            _nroMsjEntradaCHEC++;
            Log.Information("{tid} CHEC -> GTWY nuevo mensaje #{nro} - cuerpo {bytes} bytes", taskId, _nroMsjEntradaCHEC, bytesLeidos);

            // Arreglo de bytes de entrada se transforma en string, 
            // constituye el mensaje.
            string mensaje = Encoding.UTF8.GetString(bufferEntrada, 0, bytesLeidos);
            if (bytesLeidos != longitudMensaje)
            {
                Log.Warning("{tid} Longitud en cabecera {longitudMensaje} no corresponde a longitud {bytesLeidos} mensaje leido.", taskId, longitudMensaje, bytesLeidos);
            }
            Log.Verbose("{tid} CHEC -> GTWY mensaje\n{msj}", taskId, mensaje);

            // Se agrega mensaje a la cola de entrada de mensajes.
            // await _canalEntrada.Writer.WriteAsync(mensaje);
            // Log.Debug("{tid} mensaje {nro} -> [cola entrada]", taskId, _nroMsjEntrada);

            // Punto de procesamiento mensajes desde CHEC antes de enviar a  POSBC.
            string mensajeProcesado = ProcesaMensajeCHEC(mensaje);
            await _posbc.Envia(mensajeProcesado);
        }
        cliente.Close();
        Log.Warning("{tid} Tarea acepta entradas CHEC TERMINADA", taskId);
    }

    /// <summary>
    /// Procesa los mensajes d entrada CHEC.
    /// Punto de tratamiento de mensajes CHEC si hay lugar a procesarlos.
    /// Retorna mensaje procesado para remitir a POSBC.
    /// </summary>
    /// <param name="mensaje"></param>
    /// <returns></returns>
    public string ProcesaMensajeCHEC(string mensaje)
    {
        Log.Debug("GTWY procesador mensaje CHEC para POSBC invocado.");
        var copiaMensaje = new String(mensaje);
        return copiaMensaje;
    }


    /// <summary>
    /// Toma mensajes de la cola de mensaje de POSBC, los procesa y los remite a CHEC. 
    /// </summary>
    /// <param name="clienteCHEC">Socket de conexión de CHEC</param>
    /// <returns></returns>
    private async Task ProcesaSalidaPOSBC(Socket clienteCHEC, Socket clientePOSBC)
    {
        var taskId = Task.CurrentId?.ToString() ?? "no-task";
        Log.Debug("{tid} Tarea procesa salida POSBC inicia", taskId);
        while (true)
        {
            // Lee longitud mensaje entrante, 4 bytes.
            Log.Information("{tid} Esperando POSBC", taskId);
            var bufferLongitud = new byte[4];
            int bytesLeidos = 0;
            while (bytesLeidos < 4)
            {
                bytesLeidos += clientePOSBC.Receive(bufferLongitud, bytesLeidos, bufferLongitud.Length, SocketFlags.None);
            }
            // Lee porción de datos del mensaje, hasta la longitud indicada en los 4 primeros bytes.
            int longitudMensaje = Util.LongitudCodificada(bufferLongitud);
            Log.Debug("{tid} GTWY <- POSBC entra nuevo mensaje, longitud en cabecera {nroBytes}", taskId, longitudMensaje);
            var bufferEntrada = new byte[longitudMensaje];
            bytesLeidos = 0;
            while (bytesLeidos < longitudMensaje)
            {
                bytesLeidos += clientePOSBC.Receive(bufferEntrada, bytesLeidos, bufferEntrada.Length, SocketFlags.None);
            }
            _nroMsjEntradaPOSBC++;
            Log.Information("{tid} GTWY <- POSBC {nro} - {bytes} bytes cuerpo", taskId, _nroMsjEntradaPOSBC, bytesLeidos);

            // Arreglo de bytes de entrada se transforma en string, 
            // constituye el mensaje.
            string mensaje = Encoding.UTF8.GetString(bufferEntrada, 0, bytesLeidos);
            Log.Verbose("{tid} GTWY <- POSBC mensaje\n{msj}", taskId, mensaje);

            // PROCESA MENSAJE POSBC --> CHEC
            String mensajeProcesado = ProcesaMensajePOSBC(mensaje);
            if (mensajeProcesado.Length > 0)
            {
                byte[] bufferSalida = Util.ConvierteEnBufferBytes(mensajeProcesado);
                await clienteCHEC.SendAsync(bufferSalida);
                _nroMsjSalidaCHEC++;
                Log.Information("{tid} CHEC <- GTWY {nro} - {bytes} bytes total", taskId, _nroMsjSalidaCHEC, bufferSalida.Length);
                Log.Verbose("{tid} CHEC <- GTWY mensaje\n{msj}", taskId, mensaje);
            }
            else
            {
                Log.Warning("GTWY mensaje longitud 0 - no se remite a CHEC");

            }

        }
        Log.Debug("{tid} Tarea procesa salida POSBC TERMINADA", taskId);
    }

    /// <summary>
    /// Procesa los mensajes de respuesta de POSBC.
    /// Punto de tratamiento de mensajes POSBC si hay lugar a procesarlos.
    /// Retorna mensaje procesado para remitir a CHEC.
    /// </summary>
    /// <param name="mensaje"></param>
    /// <returns></returns>
    public string ProcesaMensajePOSBC(string mensaje)
    {
        Log.Debug("GTWY procesador mensaje POSBC para CHEC invocado.");
        var copiaMensaje = new String(mensaje);
        return copiaMensaje;
    }
}