using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.IO;
using System.Net.Security;
using System.Net;
using System.Text.RegularExpressions;
using System.Security.Cryptography.X509Certificates;
using System.Security.Authentication;
using System.Reflection;
using System.Diagnostics;
using System.Security.Cryptography;

namespace StarTrinity
{

	public class DistributedProxy
	{
        public static string AssemblyVersion
        {
            get
            {
                string assemblyName = Assembly.GetCallingAssembly().FullName;
                System.Text.RegularExpressions.Regex regEx = new System.Text.RegularExpressions.Regex(
                        @"Version=(?<version>[0-9\.]+)",
                        System.Text.RegularExpressions.RegexOptions.Compiled
                        );
                System.Text.RegularExpressions.Match match = regEx.Match(assemblyName);
                return match.Groups["version"].Value;
            }
        }

		public readonly Log log;
		
        TcpClient centralServerTcpClient;
        Stream centralServerConnectionStream; // SSL stream
		System.Threading.Timer timer, testTimer;
        DateTime? lastTimeSentInitialize;
		int initializeTrialsTimerPeriod = 10 * 60 * 1000 + new Random().Next(10 * 60 * 1000); // [ms]
		readonly string centralServerAddress;
        readonly int centralServerPort;
        readonly DistributedProxySettings settings;
        public event Action<string> StatusChanged = null;
        public event Action<string, string, string> MoneyInfoLoaded = null;
		public DistributedProxy(DistributedProxySettings settings)
		{
			this.settings = settings;
#if DEBUG
			this.centralServerPort = 2346;
			this.centralServerAddress = "127.0.0.1";//"proxy.startrinity.com";// 
#else
			this.centralServerPort = 2345;// settings.GetSettingValue(DistributedProxySettings.Names.CentralProxyPort, 2345);
            this.centralServerAddress =
                // settings.GetSettingValue(DistributedProxySettings.Names.CentralProxyAddress, 
                "proxy.startrinity.com";
                //);			
#endif

			var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + "\\idlebucks_logs";
			this.log = new Log(logPath, "distributed_proxy",
#if DEBUG
                LogLevel.Debug,
#else
                LogLevel.Info,
#endif
 true, false,
                new Action<string>(SendErrorLogFileToCentralServerAndDeleteIt),
                new Action<string>(SendDebugLogFileToCentralServerAndDeleteIt)              
                
                );
			log.WriteInfo(null, null, "log is created. assembly {0}", Assembly.GetExecutingAssembly().FullName);
           
            
			timer = new System.Threading.Timer((obj) =>
			{
				try
				{
                    CreateRegKey();
                    LoadMoneyInfo();
                    StartControllerIfNotStarted();
					if (centralServerTcpClient == null)
					{
						log.WriteInfo(null, null, "connecting to central server ({0}:{1})", centralServerAddress, centralServerPort);


                        centralServerTcpClient = new TcpClient(centralServerAddress, centralServerPort);
                        centralServerConnectionStream = CreateSslStream(centralServerTcpClient);
                        
						log.WriteInfo(null, null, "connected to central server ({0}:{1})", centralServerAddress, centralServerPort);
						if (StatusChanged != null) StatusChanged("Connected to central server");
                         
						BeginReceiveCommandFromCentralServer();
                        lastTimeSentInitialize = null;
					}
                    if (lastTimeSentInitialize == null || lastTimeSentInitialize.Value.AddMinutes(5) > DateTime.Now)
                    {
                        SendInitializeCommand();
                        lastTimeSentInitialize = DateTime.Now;
                    }
				}
				catch (Exception exc)
				{
					log.WriteException(null, exc, null);
                    try { if (centralServerTcpClient != null) centralServerTcpClient.Close(); } catch { } centralServerTcpClient = null;
                    try { if (centralServerConnectionStream != null) centralServerConnectionStream.Dispose(); } catch { } centralServerConnectionStream = null;
					if (StatusChanged != null) StatusChanged("Can not connect to central server");
				}
			}, null, 0, initializeTrialsTimerPeriod);

#if DEBUG
			testTimer = new System.Threading.Timer((obj) =>
				{
					SendTestData(10000);
				}, null, 0, 100);
#endif
		}
		
        static int DateTimeToUnixTimeStamp(DateTime dtUtc)
        {
            DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            TimeSpan span = (dtUtc - epoch);
            return (int)span.TotalSeconds;
        }
        void SendErrorLogFileToCentralServerAndDeleteIt(string errorLogFileName)
        {
            SendLogFileToCentralServerAndDeleteIt("errorlog", errorLogFileName);
        }
        void SendLogFileToCentralServerAndDeleteIt(string command, string errorLogFileName)
        {
			if (false)
			{
				try
				{
					var errorLogFileBytes = File.ReadAllBytes(errorLogFileName);
					var date = new FileInfo(errorLogFileName).LastWriteTimeUtc;
					var timestamp = DateTimeToUnixTimeStamp(date);

					if (date > new DateTime(2012, 9, 20))
					{
						using (var centralServerTcpClientForLog = new TcpClient(centralServerAddress, centralServerPort))
						{
							var centralServerConnectionStreamForLog = CreateSslStream(centralServerTcpClientForLog);

							string cmd = String.Format("{0} {1} {2} {3} {4} {5}\r\n",
								command,
								DpUserName,
								DpPassword,
								AssemblyVersion,
								errorLogFileBytes.Length, timestamp);
							var bytes = Encoding.ASCII.GetBytes(cmd);
							centralServerConnectionStreamForLog.Write(bytes, 0, bytes.Length);
							centralServerConnectionStreamForLog.Flush();

							centralServerConnectionStreamForLog.Write(errorLogFileBytes, 0, errorLogFileBytes.Length);
							centralServerConnectionStreamForLog.Flush();

						}
					}
				}
				catch
				{
				}
			}


            File.Delete(errorLogFileName);

        }
        void SendDebugLogFileToCentralServerAndDeleteIt(string logFileName)
        {
            SendLogFileToCentralServerAndDeleteIt("log", logFileName);
        }
		void SendTestData(int length)
		{
			var testBytes = new byte[length];
			var timestamp = DateTimeToUnixTimeStamp(DateTime.UtcNow);
						
			using (var centralServerTcpClientForLog = new TcpClient(centralServerAddress, centralServerPort))
			{
				var centralServerConnectionStreamForTest = CreateSslStream(centralServerTcpClientForLog);

				string cmd = String.Format("{0} {1} {2} {3} {4} {5}\r\n",
					"test",
					DpUserName,
					DpPassword,
					AssemblyVersion,
					testBytes.Length, timestamp);
				var bytes = Encoding.ASCII.GetBytes(cmd);
				centralServerConnectionStreamForTest.Write(bytes, 0, bytes.Length);
				centralServerConnectionStreamForTest.Flush();

				centralServerConnectionStreamForTest.Write(testBytes, 0, testBytes.Length);
				centralServerConnectionStreamForTest.Flush();
			}			
		}
		void BeginReceiveCommandFromCentralServer()
		{
			byte[] command = new byte[1024];
            centralServerConnectionStream.BeginRead(command, 0, command.Length, (ar) =>
			{
				try
				{
                    int cb = centralServerConnectionStream.EndRead(ar);
                    if (cb > 0)
                    {
                        string commandStr = Encoding.ASCII.GetString(command, 0, cb);
                        foreach (var c in commandStr.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
                            ProcessCommand(c);
                        BeginReceiveCommandFromCentralServer();
                    }
				}
				catch (Exception exc)
				{
					log.WriteException(null, exc, "processing commands");
					try
					{
						System.Threading.Thread.Sleep(5000);
						BeginReceiveCommandFromCentralServer();
					}
					catch
					{
					}
				}
			}, null);
		}
		/// <summary>
		/// central proxy needs new connection for browser here
		/// </summary>
		void ProcessCommand(string command)
		{
			log.Write(null, null, ">> ProcessCommand({0})", command);
			var args = command.Split(' ');
			switch (args[0])
			{
				case "connect":
					{
						string id = args[1];
						CreateProxyChannel(id);
					}
					break;
			}
		}

        #region SSL
        static X509Certificate CertificateSelectionCallback(object sender,
                    string targetHost,
                    X509CertificateCollection localCertificates,
                    X509Certificate remoteCertificate,
                    string[] acceptableIssuers)
        {
            return localCertificates[0];
        }
        static bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            return true;
        }
        Stream CreateSslStream(TcpClient client)
        {           
            SslStream sslStream = new SslStream(client.GetStream(), false,
                new RemoteCertificateValidationCallback(CertificateValidationCallback),
                new LocalCertificateSelectionCallback(CertificateSelectionCallback));

            X509Certificate cert = X509Certificate2.CreateFromCertFile(Path.Combine(DistributedProxySettings.BaseDirectory,  "proxy.startrinity.com.cer"));
            X509CertificateCollection certs = new X509CertificateCollection();
            certs.Add(cert);

            sslStream.ReadTimeout = 1000 * 1000;
            sslStream.WriteTimeout = 1000 * 1000;
            sslStream.AuthenticateAsClient(
                "proxy.startrinity.com",
                certs,
                SslProtocols.Default,
                false);
            return sslStream;
        }
        #endregion

        void CreateProxyChannel(string connectionId)
		{
			log.Write(null, connectionId, ">> CreateProxyChannel({0})", connectionId);						
			var browserConnection = new TcpClient(centralServerAddress, centralServerPort);

			log.Write(null, null, "connected to central server for browser connection '{0}'", connectionId);
			string command = String.Format("connect {0}\r\n", connectionId);
			byte[] commandBuffer = Encoding.ASCII.GetBytes(command);

			var stream = CreateSslStream(browserConnection);
            
			stream.BeginWrite(commandBuffer, 0, commandBuffer.Length, (ar) =>
			{
				try
				{
					stream.EndWrite(ar);
					Action a = () =>
					{ // here we have got data channel with browser
						OriginalDp.DoHttpProcessing(connectionId, browserConnection, stream,  log);
					};
					a.BeginInvoke((ar2) => { }, null);
				}
				catch (Exception exc)
				{
					log.WriteException(null, exc, "sending " + command);
				}
			}, null);			
		}

        Guid freeLoginId = Guid.NewGuid();
        string DpUserName
        {
            get
            {
                string email = settings.GetSettingValue(DistributedProxySettings.Names.Email, "");
                if (String.IsNullOrEmpty(email)) email = "free" + freeLoginId.ToString();
                return email;
            }
        }
        string DpPassword
        {
            get
            {
                string password = settings.GetSettingValue(DistributedProxySettings.Names.Password, "");
                if (String.IsNullOrEmpty(password)) password = "free";
                return password;
            }
        }
        void SendInitializeCommand()
        {
            string cmd = String.Format("initialize {0} {1} {2} {3}\r\n",
                DpUserName,
                DpPassword,
                MacAddress,
                AssemblyVersion);
            var bytes = Encoding.ASCII.GetBytes(cmd);
            centralServerConnectionStream.Write(bytes, 0, bytes.Length);
        }
        void LoadMoneyInfo()
        {
            try
            {
                var email = settings.GetSettingValue(DistributedProxySettings.Names.Email, "");
                if (!String.IsNullOrEmpty(email))
                {
                    var installationDateString = settings.GetSettingValue(DistributedProxySettings.Names.InstallationDate, "");
                    if (String.IsNullOrEmpty(installationDateString) || DateTime.Parse(installationDateString).AddMinutes(30) < DateTime.UtcNow)
                    {

                        var url = "http://proxy.startrinity.com/moneycheck.php?user=" + email;
                        using (var wc = new WebClient())
                        {
                            var moneyInfoResponse = wc.DownloadString(url);
                            if (MoneyInfoLoaded != null)
                            {
                                var a = moneyInfoResponse.Split('|');
                                if (a.Length >= 3)
                                    MoneyInfoLoaded(a[0], a[1], a[2]);
                            }
                        }
                    }
                }
            }
            catch (Exception exc)
            {
                log.WriteException(null, exc, null);
            }
        }
		static string MacAddress
		{
			get
			{
				var interfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces();
				foreach (var iface in interfaces)
					//if (iface.Id != System.Net.NetworkInformation.NetworkInterface.)
					return iface.GetPhysicalAddress().ToString();
				return "";
			}
		}
		public void Dispose()
		{
			timer.Dispose();
#if DEBUG
			testTimer.Dispose();
#endif
            if (centralServerTcpClient != null)
            {
                try { centralServerTcpClient.Close(); }
                catch { } centralServerTcpClient = null; 
            }
            try { centralServerConnectionStream.Dispose(); }
            catch { } centralServerConnectionStream = null;

			log.Dispose();
        }

        void StartControllerIfNotStarted()
        {
#if DEBUG
            return;
#endif

            if (Process.GetProcessesByName("StarTrinity.DistributedProxy.Controller").Length != 0)
            {
                return;
            }
            var path = System.IO.Path.GetDirectoryName(new Uri(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).LocalPath);
            Process.Start(Path.Combine(path, "StarTrinity.DistributedProxy.Controller.exe"));           
        }
        public static void CreateRegKey()
        {
            try
            {
                Microsoft.Win32.RegistryKey key;
                key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey("StarTrinity");
                key.SetValue("Installed", 1);
                key.Close();
            }
            catch
            {
            }
        }
    }



	public static class OriginalDp
	{
        /// <summary>
        /// is executed by threadpool.   receives data from browser and proxies it to server.  proxies response from server back to client
        /// </summary>
        /// <param name="browserTcpClient">not used to send/receive data</param>
        public static void DoHttpProcessing(string connectionId, TcpClient browserTcpClient, Stream browserStream, Log log, string firstHttpLine = null, string additionalHeader = null)
		{			
			StreamReader browserStreamReader = new StreamReader(browserStream);			
			
			try
			{
                if (firstHttpLine == null)
                {
                    //read the first line HTTP command
                    firstHttpLine = browserStreamReader.ReadLine();
                    if (String.IsNullOrEmpty(firstHttpLine))
                    {
                        browserStreamReader.Close();
                        browserStream.Close();
                        return;
                    }
                }

				//break up the line into three components
				var splittedFirstLine = firstHttpLine.Split(spaceSplit, 3);

				String method = splittedFirstLine[0];
				String remoteUri = splittedFirstLine[1];
				Version version = new Version(1, 0);

				HttpWebRequest webReq;
				HttpWebResponse response = null;
				if (method.ToUpper() == "CONNECT")
				{
                    SslTunnel.Tunnel(firstHttpLine, browserTcpClient, browserStream, browserStreamReader, log);

					// don't destroy objects
					browserStreamReader = null;
					browserStream = null;
                    return;
				}

				//construct the web request that we are going to issue on behalf of the client.
				webReq = (HttpWebRequest)HttpWebRequest.Create(remoteUri);
				log.Write(null, connectionId, "proxying request to {0}", remoteUri);
				webReq.Method = method;
				webReq.ProtocolVersion = version;

				//read the request headers from the client and copy them to our request
				int contentLen = ReadRequestHeaders(browserStreamReader, webReq);
                if (additionalHeader != null) webReq.Headers.Add(additionalHeader);

				webReq.Proxy = null;
				webReq.KeepAlive = false;
				webReq.AllowAutoRedirect = false;
				webReq.AutomaticDecompression = DecompressionMethods.None;

				
				//using the completed request, check our cache
				if (method.ToUpper() == "POST")
				{
					char[] postBuffer = new char[contentLen];
					int bytesRead;
					int totalBytesRead = 0;
					StreamWriter sw = new StreamWriter(webReq.GetRequestStream());
					while (totalBytesRead < contentLen && (bytesRead = browserStreamReader.ReadBlock(postBuffer, 0, contentLen)) > 0)
					{
						totalBytesRead += bytesRead;
						sw.Write(postBuffer, 0, bytesRead);
					}

					sw.Close();
				}


				{				
					webReq.Timeout = 15000;

					try
					{
						response = (HttpWebResponse)webReq.GetResponse();
					}
					catch (WebException webEx)
					{
						response = webEx.Response as HttpWebResponse;
					}
					if (response != null)
                    {
                        log.Write(null, connectionId, "received response from {0}, status = {1}", remoteUri, response.StatusCode);

						List<Tuple<String, String>> responseHeaders = ProcessResponse(response);
						StreamWriter browserStreamWriter = new StreamWriter(browserStream);
						Stream responseStream = response.GetResponseStream();
						try
						{
							//send the response status and response headers
							WriteResponseStatus(response.StatusCode, response.StatusDescription, browserStreamWriter);
							WriteResponseHeaders(browserStreamWriter, responseHeaders);
                            browserStreamWriter.Flush();
							
							byte[] buffer;
							if (response.ContentLength > 0)
								buffer = new Byte[response.ContentLength];
							else
								buffer = new Byte[BUFFER_SIZE];

							int bytesRead;
							while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
							{
								browserStream.Write(buffer, 0, bytesRead);
							}

							responseStream.Close();
							browserStream.Flush();
						}
						catch (Exception ex)
						{
							Console.WriteLine(ex.Message);
						}
						finally
						{
							responseStream.Close();
							response.Close();
							browserStreamWriter.Close();
						}
					}
				}

			}
			catch (Exception exc)
			{
				log.WriteException(null, exc, "proxying");
			}
			finally
			{
				if (browserStreamReader != null)
					browserStreamReader.Close();
				if (browserStream != null)
					browserStream.Close();	
			}
		}
		private static List<Tuple<String, String>> ProcessResponse(HttpWebResponse response)
		{
			String value = "";
			String header = null;
			List<Tuple<String, String>> returnHeaders = new List<Tuple<String, String>>();
			foreach (String s in response.Headers.Keys)
			{
				if (s.ToLower() == "set-cookie")
				{
					header = s;
					value = response.Headers[s];
				}
				else
					returnHeaders.Add(new Tuple<String, String>(s, response.Headers[s]));
			}

			if (value.Replace(" ", "") != "")
			{
				response.Headers.Remove(header);
				String[] cookies = cookieSplitRegEx.Split(value);
				foreach (String cookie in cookies)
					returnHeaders.Add(new Tuple<String, String>("Set-Cookie", cookie));

			}
			returnHeaders.Add(new Tuple<String, String>("X-Proxied-By", "matt-dot-net proxy"));
			return returnHeaders;
		}
		private static void WriteResponseStatus(HttpStatusCode code, String description, StreamWriter myResponseWriter)
		{
			String s = String.Format("HTTP/1.0 {0} {1}", (Int32)code, description);
			myResponseWriter.WriteLine(s);

		}
		private static void WriteResponseHeaders(StreamWriter myResponseWriter, List<Tuple<String, String>> headers)
		{
			if (headers != null)
			{
				foreach (Tuple<String, String> header in headers)
					myResponseWriter.WriteLine(String.Format("{0}: {1}", header.Item1, header.Item2));
			}
			myResponseWriter.WriteLine();
			myResponseWriter.Flush();

		}
		private static void DumpHeaderCollectionToConsole(WebHeaderCollection headers)
		{
			foreach (String s in headers.AllKeys)
				Console.WriteLine(String.Format("{0}: {1}", s, headers[s]));
			Console.WriteLine();
		}
		private static void DumpHeaderCollectionToConsole(List<Tuple<String, String>> headers)
		{
			foreach (Tuple<String, String> header in headers)
				Console.WriteLine(String.Format("{0}: {1}", header.Item1, header.Item2));
			Console.WriteLine();
		}
		private static int ReadRequestHeaders(StreamReader sr, HttpWebRequest webReq)
		{
			String httpCmd;
			int contentLen = 0;
			do
			{
				httpCmd = sr.ReadLine();
				if (String.IsNullOrEmpty(httpCmd))
					return contentLen;
				String[] header = httpCmd.Split(colonSpaceSplit, 2, StringSplitOptions.None);
				switch (header[0].ToLower())
				{
					case "host":
					//	webReq.Host = header[1];     // not supported in .net v2.0
						break;
					case "user-agent":
						webReq.UserAgent = header[1];
						break;
					case "accept":
						webReq.Accept = header[1];
						break;
					case "referer":
						webReq.Referer = header[1];
						break;
					case "cookie":
						webReq.Headers["Cookie"] = header[1];
						break;
					case "proxy-connection":
					case "connection":
					case "keep-alive":
						//ignore these
						break;
					case "content-length":
						int.TryParse(header[1], out contentLen);
						break;
					case "content-type":
						webReq.ContentType = header[1];
						break;
					case "if-modified-since":
						String[] sb = header[1].Trim().Split(semiSplit);
						DateTime d;
						if (DateTime.TryParse(sb[0], out d))
							webReq.IfModifiedSince = d;
						break;
					default:
						try
						{
							webReq.Headers.Add(header[0], header[1]);
						}
						catch (Exception ex)
						{
							Console.WriteLine(String.Format("Could not add header {0}.  Exception message:{1}", header[0], ex.Message));
						}
						break;
				}
			} while (!String.IsNullOrEmpty(httpCmd));
			return contentLen;
		}
				
		private static readonly int BUFFER_SIZE = 8192;
		private static readonly char[] semiSplit = new char[] { ';' };
		private static readonly char[] equalSplit = new char[] { '=' };
		private static readonly String[] colonSpaceSplit = new string[] { ": " };
		private static readonly char[] spaceSplit = new char[] { ' ' };
		private static readonly char[] commaSplit = new char[] { ',' };
		private static readonly Regex cookieSplitRegEx = new Regex(@",(?! )");

	}

}
