#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Linq;
using log4net.Core;
using log4net.Util;

namespace log4net.Appender;

/// <summary>
/// Appender that allows clients to connect via Telnet to receive log messages
/// </summary>
/// <remarks>  
/// <para>
/// The TelnetAppender accepts socket connections and streams logging messages back to the client.
/// The output is provided in a telnet-friendly way so that a log can be monitored over a TCP/IP socket.
/// This allows simple remote monitoring of application logging.
/// </para>
/// <para>
/// The default <see cref="Port"/> is 23 (the telnet port).
/// </para>
/// </remarks>
/// <author>Keith Long</author>
/// <author>Nicko Cadell</author>
public class TelnetAppender : AppenderSkeleton
{
  private SocketHandler? _handler;
  private int _listeningPort = 23;

  /// <summary>
  /// The fully qualified type of the TelnetAppender class.
  /// </summary>
  /// <remarks>
  /// Used by the internal logger to record the Type of the
  /// log message.
  /// </remarks>
  private static readonly Type _declaringType = typeof(TelnetAppender);

  /// <summary>
  /// Gets or sets the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
  /// </summary>
  /// <value>
  /// An integer value in the range <see cref="IPEndPoint.MinPort" /> to <see cref="IPEndPoint.MaxPort" /> 
  /// indicating the TCP port number on which this <see cref="TelnetAppender"/> will listen for connections.
  /// </value>
  /// <remarks>
  /// <para>
  /// The default value is 23 (the telnet port).
  /// </para>
  /// </remarks>
  /// <exception cref="ArgumentOutOfRangeException">The value specified is less than <see cref="IPEndPoint.MinPort" /> 
  /// or greater than <see cref="IPEndPoint.MaxPort" />.</exception>
  public int Port
  {
    get => _listeningPort;
    set
    {
      if (value is < IPEndPoint.MinPort or > IPEndPoint.MaxPort)
      {
        throw SystemInfo.CreateArgumentOutOfRangeException(nameof(value), value,
          $"The value specified for Port is less than {IPEndPoint.MinPort} or greater than {IPEndPoint.MaxPort}.");
      }
      _listeningPort = value;
    }
  }

  /// <summary>
  /// Overrides the parent method to close the socket handler
  /// </summary>
  /// <remarks>
  /// <para>
  /// Closes all the outstanding connections.
  /// </para>
  /// </remarks>
  protected override void OnClose()
  {
    base.OnClose();

    _handler?.Dispose();
    _handler = null;
  }

  /// <summary>
  /// This appender requires a <see cref="Layout"/> to be set.
  /// </summary>
  protected override bool RequiresLayout => true;

  /// <summary>
  /// Create the socket handler and wait for connections
  /// </summary>
  public override void ActivateOptions()
  {
    base.ActivateOptions();
    try
    {
      LogLog.Debug(_declaringType, $"Creating SocketHandler to listen on port [{_listeningPort}]");
      _handler = new SocketHandler(_listeningPort);
    }
    catch (Exception ex)
    {
      LogLog.Error(_declaringType, "Failed to create SocketHandler", ex);
      throw;
    }
  }

  /// <summary>
  /// Writes the logging event to each connected client.
  /// </summary>
  /// <param name="loggingEvent">The event to log.</param>
  protected override void Append(LoggingEvent loggingEvent)
  {
    if (_handler is not null && _handler.HasConnections)
    {
      _handler.Send(RenderLoggingEvent(loggingEvent));
    }
  }

  /// <summary>
  /// Helper class to manage connected clients
  /// </summary>
  /// <remarks>
  /// <para>
  /// The SocketHandler class is used to accept connections from clients.
  /// It is threaded so that clients can connect/disconnect asynchronously.
  /// </para>
  /// </remarks>
  [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
  protected class SocketHandler : IDisposable
  {
    private const int MaxConnections = 20;

    private readonly Socket _serverSocket;
    private readonly List<SocketClient> _clients = [];
    private readonly object _syncRoot = new();
    private bool _wasDisposed;

    /// <summary>
    /// Class that represents a client connected to this handler
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
    protected class SocketClient : IDisposable
    {
      private readonly Socket _socket;
      private readonly StreamWriter _writer;

      /// <summary>
      /// Create this <see cref="SocketClient"/> for the specified <see cref="Socket"/>
      /// </summary>
      /// <param name="socket">the client's socket</param>
      /// <remarks>
      /// <para>
      /// Opens a stream writer on the socket.
      /// </para>
      /// </remarks>
      public SocketClient(Socket socket)
      {
        _socket = socket;
        try
        {
          _writer = new(new NetworkStream(socket));
        }
        catch (Exception e) when (!e.IsFatal())
        {
          Dispose();
          throw;
        }
      }

      /// <summary>
      /// Writes a string to the client.
      /// </summary>
      /// <param name="message">string to send</param>
      public void Send(string message)
      {
        _writer.Write(message);
        _writer.Flush();
      }

      /// <summary>
      /// Cleans up the client connection.
      /// </summary>
      [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly")]
      public void Dispose()
      {
        try
        {
          _writer.Dispose();
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // Ignore
        }

        try
        {
          _socket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // Ignore
        }

        try
        {
          _socket.Dispose();
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // Ignore
        }
      }
    }

    /// <summary>
    /// Opens a new server port on <paramref ref="port"/>
    /// </summary>
    /// <param name="port">the local port to listen on for connections</param>
    /// <remarks>
    /// <para>
    /// Creates a socket handler on the specified local server port.
    /// </para>
    /// </remarks>
    public SocketHandler(int port)
    {
      _serverSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      _serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
      _serverSocket.Listen(5);
      AcceptConnection();
    }

    private void AcceptConnection() => _serverSocket.BeginAccept(OnConnect, null);

    /// <summary>
    /// Sends a string message to each of the connected clients.
    /// </summary>
    /// <param name="message">the text to send</param>
    public void Send(string message)
    {
      List<SocketClient> localClients;
      lock (_syncRoot)
      {
        localClients = _clients.ToList();
      }

      // Send outside lock.
      foreach (SocketClient client in localClients)
      {
        try
        {
          client.Send(message);
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // The client has closed the connection, remove it from our list
          client.Dispose();
          RemoveClient(client);
        }
      }
    }

    /// <summary>
    /// Add a client to the internal clients list
    /// </summary>
    /// <param name="client">client to add</param>
    private void AddClient(SocketClient client)
    {
      lock (_syncRoot)
      {
        _clients.Add(client);
      }
    }

    /// <summary>
    /// Remove a client from the internal clients list
    /// </summary>
    /// <param name="client">client to remove</param>
    private void RemoveClient(SocketClient client)
    {
      lock (_syncRoot)
      {
        _clients.Remove(client);
      }
    }

    /// <summary>
    /// Test if this handler has active connections
    /// </summary>
    public bool HasConnections => _clients.Count > 0;
    // clients.Count is an atomic read that can be done outside the lock.

    /// <summary>
    /// Callback used to accept a connection on the server socket
    /// </summary>
    /// <param name="asyncResult">The result of the asynchronous operation</param>
    /// <remarks>
    /// <para>
    /// On connection adds to the list of connections 
    /// if there are too many open connections you will be disconnected
    /// </para>
    /// </remarks>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")]
    private void OnConnect(IAsyncResult asyncResult)
    {
      if (_wasDisposed)
      {
        return;
      }

      try
      {
        // Block until a client connects
        Socket socket = _serverSocket.EndAccept(asyncResult);
        LogLog.Debug(_declaringType, $"Accepting connection from [{socket.RemoteEndPoint}]");
        SocketClient client = new(socket);

        // clients.Count is an atomic read that can be done outside the lock.
        int currentActiveConnectionsCount = _clients.Count;
        if (currentActiveConnectionsCount < MaxConnections)
        {
          try
          {
            client.Send($"TelnetAppender v1.0 ({currentActiveConnectionsCount + 1} active connections)\r\n\r\n");
            AddClient(client);
          }
          catch (Exception e) when (!e.IsFatal())
          {
            client.Dispose();
          }
        }
        else
        {
          client.Send("Sorry - Too many connections.\r\n");
          client.Dispose();
        }
      }
      catch (Exception e) when (!e.IsFatal())
      {
        // Ignore
      }
      finally
      {
        if (!_wasDisposed)
        {
          AcceptConnection();
        }
      }
    }

    /// <summary>
    /// Closes all network connections
    /// </summary>
    public void Dispose()
    {
      if (_wasDisposed)
      {
        return;
      }

      _wasDisposed = true;

      lock (_syncRoot)
      {
        foreach (SocketClient client in _clients)
        {
          client.Dispose();
        }
        _clients.Clear();

        try
        {
          _serverSocket.Shutdown(SocketShutdown.Both);
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // Ignore
        }

        try
        {
          _serverSocket.Dispose();
        }
        catch (Exception e) when (!e.IsFatal())
        {
          // Ignore
        }
      }
    }
  }
}