using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Net.Security;
using System.Security.Authentication;

namespace StarTrinity
{
	public class CentralProxyMainService
    {
        public readonly Log log;
        public readonly Log dpLog;
		/// <summary>
		/// ordered by email
		/// use lock (proxies)
		/// </summary>
		readonly Dictionary<string, CentralProxy> proxies = new Dictionary<string, CentralProxy>();
		/// <summary>
		/// ordered by connection id
		/// </summary>
		public readonly Dictionary<string, BrowserRequest> pendingRequests = new Dictionary<string, BrowserRequest>();

		int localPortForBrowsers, minLocalPortForBrowsers;
		Socket listenerForDp;
		Settings settings = new Settings();
		public readonly Database database;
		Timer checkTimer;
		const int checkTimerPeriod = 30000;
		const int maxIdleTime = 60 * 60 * 1000;
		public CentralProxyMainService()
		{
#if DEBUG
			var portForDp = 2346;
#else
			var portForDp = 2345;// settings.GetSettingValue(Settings.Names.PortForDp, 2345);
#endif


            var logPath = System.IO.Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath) + "\\logs";
            log = new Log(logPath, "proxy", LogLevel.Debug, true, false, null, null);
            dpLog = new Log(logPath, "dp", LogLevel.Debug, true, false, null, null);
			log.Write(null, null, "log created");

#if DEBUG
			minLocalPortForBrowsers = 20000;// settings.GetSettingValue(Settings.Names.MinLocalPortForBrowsers, 10000);
#else
			minLocalPortForBrowsers = 10000;//settings.GetSettingValue(Settings.Names.MinLocalPortForBrowsers, 10000);
#endif

			localPortForBrowsers = minLocalPortForBrowsers;
			try
			{
				database = new Database(log, settings.GetSettingValue(Settings.Names.MySqlConnection,
					"Network Address=proxy.startrinity.com;Initial Catalog='proxy';Persist Security Info=no;User Name='proxy';Password='123456'"));
				listenerForDp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

				listenerForDp.Bind(new IPEndPoint(IPAddress.Any, portForDp));
				listenerForDp.Listen(-1);

				listenerForDp.BeginAccept(new AsyncCallback(EndAcceptForDp), null);
				log.Write(null, null, "accepting connections from DP at port {0}", portForDp);

				checkTimer = new Timer(new TimerCallback(CheckTimerCallback), null, checkTimerPeriod, checkTimerPeriod);
			}
			catch (Exception exc)
			{
				log.WriteException(null, exc, null);
			}
		}
		void CheckTimerCallback(object o)
		{
			try
			{
				lock (proxies)
				{
					foreach (var proxy in new List<CentralProxy>(proxies.Values))
					{
						proxy.timeSinceLastPing += checkTimerPeriod;
						if (proxy.timeSinceLastPing > maxIdleTime)
                        {
                            log.WriteWarning(null, null, "removing DP {0} on inactivity timeout ({1} ms)", proxy._email, proxy.timeSinceLastPing);

							proxies.Remove(proxy._email);
							database.OnProxyDisconnected(proxy._localPortForBrowsers);
							proxy.Dispose();
						}
					}
					log.Write(null, null, "{0} proxies are online", proxies.Count);
				}

				database.DeleteOldConnectionRecords();
			}
			catch (Exception exc)
			{
				log.WriteException(null, exc, null);
			}
		}

		void EndAcceptForDp(IAsyncResult ar)
		{
			try
			{
				Socket dpSocket = listenerForDp.EndAccept(ar);
                CentralProxyStatistics.OnAcceptedConnectionFromDp(dpSocket);
				log.Write(null, null, "accepted connection from DP at {0}", dpSocket.RemoteEndPoint);
				
				Action a = () =>
					{
						BeginProcessDpConnection(CreateSslStream(new NetworkStream(dpSocket), dpSocket.RemoteEndPoint), dpSocket, null);
				
					};
				a.BeginInvoke((ar2) => { }, null);
				
				
				listenerForDp.BeginAccept(new AsyncCallback(EndAcceptForDp), null);
			}
			catch (Exception exc)
			{
				log.WriteException(null, exc, "acception connection from DP");
				try
				{
					listenerForDp.BeginAccept(new AsyncCallback(EndAcceptForDp), null);
				}
				catch
				{
				}
			}
		}
		public void Dispose()
		{
			if (checkTimer != null) { checkTimer.Dispose(); checkTimer = null; }
			if (listenerForDp != null) { listenerForDp.Close(); listenerForDp.Close(); }
			database.Dispose();
			log.Dispose();
            dpLog.Dispose();
		}
        
        #region SSL
        public static X509Certificate SelectLocalServerCertificate(
            object sender,
            string targetHost,
            X509CertificateCollection localCertificates,
            X509Certificate remoteCertificate,
            string[] acceptableIssuers)
        {
            return (X509Certificate)new X509Certificate2(Path.Combine(Settings.BaseDirectory, "proxy.startrinity.com.pfx"), "145210");
        }

        static bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }
        Stream CreateSslStream(NetworkStream stream ,EndPoint remoteEndpoint)
        {
			log.Write(null, null, "creating SSL stream for {0}", remoteEndpoint);
            SslStream sslStream = new SslStream(stream, false, new RemoteCertificateValidationCallback(CertificateValidationCallback), new LocalCertificateSelectionCallback(SelectLocalServerCertificate));

			log.Write(null, null, "created SSL stream for {0}. authenticating...", remoteEndpoint);
            sslStream.AuthenticateAsServer(new X509Certificate2(Path.Combine(Settings.BaseDirectory, "proxy.startrinity.com.pfx"), "145210"), false, SslProtocols.Default, false);

			log.Write(null, null, "authenticated SSL stream for {0}", remoteEndpoint);

            return sslStream;
        }
        #endregion
		void BeginProcessDpConnection(Stream dpConnection, Socket dpSocket, Action disconnectHandler)
		{     
			// receive 1st command
			byte[] command = new byte[1024];
			dpConnection.BeginRead(command, 0, command.Length,(ar) =>
			{
				try
				{
					int receivedCount = dpConnection.EndRead(ar);
					var commandStr = Encoding.ASCII.GetString(command, 0, receivedCount);

					bool needToReceiveAgain = false;
					foreach (var c in commandStr.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
					{
						// log.Write(null, dpConnection.RemoteEndPoint.ToString(), "received command '{0}'", commandText);
						try
						{
							if (BeginProcessDpCommand(dpConnection, dpSocket, c, out disconnectHandler))
								needToReceiveAgain = true;
						}
						catch (Exception exc)
						{
							log.WriteException(dpSocket.RemoteEndPoint.ToString(), exc, "processing command from DP");
							dpConnection.Close();
						}
					}
					if (needToReceiveAgain)
                        BeginProcessDpConnection(dpConnection, dpSocket, disconnectHandler); // receive next command
					
				}
				catch (Exception exc)
				{
                    try
                    {
                        if (disconnectHandler != null) disconnectHandler();
                    }
                    catch (Exception exc2)
                    {
					    log.WriteException(null, exc2, "handling disconnect");
                    }
                    if (exc.InnerException is SocketException)
                    {
                        var ne = (SocketException)exc.InnerException;
                        if (ne.ErrorCode == 10054)
                        {
                            CentralProxyStatistics.OnForciblyClosedConnectionByDp(dpSocket);
                            return;
                        }
                    }
                    log.WriteException(dpSocket.RemoteEndPoint.ToString(), exc, "receiving message from DP");
				}
			}, null);
		}

		/// <returns>true if next command is expected</returns>
		bool BeginProcessDpCommand(Stream dpConnection, Socket dpSocket, string command, out Action disconnectHandler)
        {
            var dpRemoteEndpoint = ((IPEndPoint)dpSocket.RemoteEndPoint);
            log.Write("mainservice", dpRemoteEndpoint.ToString(), ">> BeginProcessDpCommand(command = '{0}')", command);
			disconnectHandler = null;
			var args = command.Split(' ');
			if (args.Length == 0) throw new ArgumentException();
			switch (args[0])
			{
				case "initialize":
					{
						if (args.Length < 4) throw new ArgumentException();

						var email = args[1];
						var password = args[2];
						var mac = args[3];
                        var dpVersion = args.Length > 4 ? args[4] : "";
                        var dpIpAddress = dpRemoteEndpoint.Address.ToString();
                        if (!database.CheckInitializeFromDp(email, password, mac, dpIpAddress, dpVersion))
						{
							dpConnection.Close();					
							return false;
						}

						CentralProxy proxy = null;
						lock (proxies)
						{
							if (proxies.ContainsKey(email))
							{
								proxy = proxies[email];
								proxy.OnReceivedInitializeFromDp(dpConnection);
								proxy.timeSinceLastPing = 0;

                                database.OnProxyPingReceived(proxy._localPortForBrowsers, dpIpAddress, mac, email);
							}
							else
							{
								bool reject;
								proxy = new CentralProxy(this, minLocalPortForBrowsers, ref localPortForBrowsers, dpConnection, email, dpIpAddress);
								try
								{
									database.OnProxyConnected(proxy._localPortForBrowsers, dpIpAddress, mac, email, "todo", dpVersion, "todo", out reject);
									if (reject)
									{
										proxy.Dispose();
										dpConnection.Close();
										return false;
									}
								}
								catch (Exception exc)
								{
									proxy.Dispose();
									dpConnection.Close();
									throw exc;
								}

								proxies.Add(email, proxy);

							}
						}

						if (proxy != null)
							disconnectHandler = () =>
							{
                                log.Write("mainservice", dpRemoteEndpoint.ToString(), "DP is disconnected (email '{0}')", email);
								bool removed;
								lock (proxies)
								{
									removed = proxies.Remove(email);
								}
								if (removed) database.OnProxyDisconnected(proxy._localPortForBrowsers);
								proxy.Dispose();
							};
					}
					return true;
				case "connect":
					{
						if (args.Length != 2) throw new ArgumentException();
						var connectionId = args[1];						
						OnReceivedConnect(dpConnection, dpSocket, connectionId);
					}
					return false;
				case "log":
				case "errorlog":
					{ // save log report from DP
						if (args.Length < 4) throw new ArgumentException();

						var email = args[1];
						var password = args[2];
						var dpVersion = args[3];
						var logLength = args[4];
						var timestamp = args[5];
						var dpIpAddress = dpRemoteEndpoint.Address.ToString();
						var timestampDate = Database.UnixTimeStampToDateTime(int.Parse(timestamp));

						var logData = new byte[int.Parse(logLength)];
						dpConnection.Read(logData, 0, logData.Length);

						var logString = Encoding.ASCII.GetString(logData);

						dpLog.Write(null, null,
							String.Format("received DP log report: IP {0} email {1}, version {2} utc date {3}\r\n{4}", dpIpAddress, email, dpVersion, timestampDate, logString),
							args[0] == "log" ? LogLevel.Info : LogLevel.Error);

					}
					return false;
				case "test":
					{
						if (args.Length < 4) throw new ArgumentException();

						var email = args[1];
						var password = args[2];
						var dpVersion = args[3];
						var logLength = args[4];
						var timestamp = args[5];
						var dpIpAddress = dpRemoteEndpoint.Address.ToString();
						var timestampDate = Database.UnixTimeStampToDateTime(int.Parse(timestamp));

						var logData = new byte[int.Parse(logLength)];
						dpConnection.Read(logData, 0, logData.Length);

						var logString = Encoding.ASCII.GetString(logData);

						dpLog.Write(null, null, "received test data, {0} bytes", logLength);

					}
					return false;
				default:
					throw new Exception("invalid command");
			}
        }
        
		void OnReceivedConnect(Stream dpConnection, Socket dpSocket, string connectionId)
		{
            log.Write(null, connectionId, ">> OnReceivedConnect({0}, {1})", dpSocket.RemoteEndPoint, connectionId);
			
			BrowserRequest request = null;
			lock (pendingRequests)
			{
				if (pendingRequests.TryGetValue(connectionId, out request))
					pendingRequests.Remove(connectionId);
			}
			
			if (request != null)
			{
				try
				{
					log.Write(null, connectionId, "executing pending action");
					request.PendingAction(dpConnection, (IPEndPoint)dpSocket.RemoteEndPoint);
				}
				catch (Exception exc)
				{
					log.WriteException(null, exc, connectionId, "executing pending action", true);
				}
			}

		}
        
		//void DumpRequests()
		//{
		//    var sb = new StringBuilder();
		//    lock (pendingRequests)
		//    {
		//        sb.AppendFormat("pending requests: count = {0}--------------------------------------------\r\n", pendingRequests.Count);
		//        foreach (var r in pendingRequests.Values)
		//            sb.AppendFormat("{0}\r\n", r.Report);
		//    }
		//    sb.Append("------------------------------------------------------------------------------");
		//    log.Write(null, name, sb.ToString());
		//}
	}
}
