using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using SpaceWizards.HttpListener;
using UnityEngine;
using Webserver.FileCache;
using Webserver.Permissions;
using Webserver.UrlHandlers;
using Cookie = System.Net.Cookie;
using HttpStatusCode = System.Net.HttpStatusCode;
using IPEndPoint = System.Net.IPEndPoint;

namespace Webserver {
	public class Web : IConsoleServer {
		public static event Action<Web> ServerInitialized;
		
		private const string indexPageUrl = "/app";
		
		private readonly List<AbsHandler> handlers = new List<AbsHandler> ();
		public readonly List<WebMod> webMods = new List<WebMod> ();
		public readonly ConnectionHandler ConnectionHandler;

		private readonly HttpListener listener = new HttpListener ();
		private readonly Version httpProtocolVersion = new Version(1, 1);

		private readonly AsyncCallback handleRequestDelegate;

		public Web (string _modInstancePath) {
			try {
				bool dashboardEnabled = GamePrefs.GetBool (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.WebDashboardEnabled)));
				if (!dashboardEnabled) {
					Log.Out ($"[Web] Webserver not started, {nameof (EnumGamePrefs.WebDashboardEnabled)} set to false");
					return;
				}
				
				int webPort = GamePrefs.GetInt (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.WebDashboardPort)));
				if (webPort < 1 || webPort > 65533) {
					Log.Out ($"[Web] Webserver not started ({nameof (EnumGamePrefs.WebDashboardPort)} not within 1-65535)");
					return;
				}

				if (!HttpListener.IsSupported) {
					Log.Out ("[Web] Webserver not started (HttpListener.IsSupported returned false)");
					return;
				}

				if (string.IsNullOrEmpty (GamePrefs.GetString (EnumUtils.Parse<EnumGamePrefs> (nameof (EnumGamePrefs.WebDashboardUrl))))) {
					Log.Warning ($"[Web] {nameof (EnumGamePrefs.WebDashboardUrl)} not set. Recommended to set it to the public URL pointing to your dashboard / reverse proxy");
				}

				ConnectionHandler = new ConnectionHandler ();
				
				RegisterDefaultHandlers (_modInstancePath);
				
				// Allow other code to add their stuff
				ServerInitialized?.Invoke (this);

				listener.Prefixes.Add ($"http://+:{webPort}/");
				listener.Start ();
				handleRequestDelegate = HandleRequest;
				listener.BeginGetContext (handleRequestDelegate, listener);

				SdtdConsole.Instance.RegisterServer (this);

				Log.Out ($"[Web] Started Webserver on port {webPort}");
			} catch (Exception e) {
				Log.Error ("[Web] Error in Web.ctor: ");
				Log.Exception (e);
			}
		}

		private void RegisterDefaultHandlers (string _modInstancePath) {
			// TODO: Read from config
			bool useCacheForStatic = StringParsers.ParseBool ("false");

			string webfilesFolder = DetectWebserverFolder (_modInstancePath);

			RegisterPathHandler ("/", new RewriteHandler ("/files/"));

			// React virtual routing
			RegisterPathHandler ("/app", new RewriteHandler ("/files/index.html", true));
				
			// Do mods relatively early as they should not be requested a lot, unlike the later registrations, especially for API and map tiles
			RegisterWebMods (useCacheForStatic);

			RegisterPathHandler ("/session/", new SessionHandler (ConnectionHandler));
			RegisterPathHandler ("/userstatus", new UserStatusHandler ());
			RegisterPathHandler ("/sse/", new SseHandler ());
			RegisterPathHandler ("/files/", new StaticHandler (
				webfilesFolder,
				useCacheForStatic ? new SimpleCache () : new DirectAccess (),
				false)
			);
			RegisterPathHandler ("/itemicons/", new ItemIconHandler (true));
			RegisterPathHandler ("/api/", new ApiHandler ());
		}

		private static string DetectWebserverFolder (string _modInstancePath) {
			string webserverFolder = $"{_modInstancePath}/webserver";

			foreach (Mod mod in ModManager.GetLoadedMods ()) {
				string modServerFolder = $"{mod.Path}/webserver";
				
				if (Directory.Exists (modServerFolder)) {
					webserverFolder = modServerFolder;
				}
			}
			
			Log.Out ($"[Web] Serving basic webserver files from {webserverFolder}");

			return webserverFolder;
		}

		public void RegisterPathHandler (string _urlBasePath, AbsHandler _handler) {
			foreach (AbsHandler handler in handlers) {
				if (handler.UrlBasePath != _urlBasePath) {
					continue;
				}

				Log.Error ($"[Web] Handler for relative path {_urlBasePath} already registerd.");
				return;
			}
			
			handlers.Add (_handler);
			_handler.SetBasePathAndParent (this, _urlBasePath);
		}

		private void RegisterWebMods (bool _useStaticCache) {
			foreach (Mod mod in ModManager.GetLoadedMods ()) {
				try {
					try {
						WebMod webMod = new WebMod (this, mod, _useStaticCache);
						webMods.Add (webMod);
					} catch (InvalidDataException e) {
						Log.Error ($"[Web] Could not load webmod from mod {mod.Name}: {e.Message}");
					}
				} catch (Exception e) {
					Log.Error ($"[Web] Failed loading web mods from mod {mod.Name}");
					Log.Exception (e);
				}
			}
		}

		public void Disconnect () {
			try {
				listener.Stop ();
				listener.Close ();
			} catch (Exception e) {
				Log.Out ($"[Web] Error in Web.Disconnect: {e}");
			}
		}

		public void Shutdown () {
			foreach (AbsHandler handler in handlers) {
				handler.Shutdown ();
			}
		}

		public void SendLine (string _line) {
			ConnectionHandler.SendLine (_line);
		}

		public void SendLog (string _formattedMessage, string _plainMessage, string _trace, LogType _type, DateTime _timestamp, long _uptime) {
			// Do nothing, handled by LogBuffer internally
		}

		private readonly UnityEngine.Profiling.CustomSampler getContextSampler = UnityEngine.Profiling.CustomSampler.Create ("GetCtx");
		private readonly UnityEngine.Profiling.CustomSampler authSampler = UnityEngine.Profiling.CustomSampler.Create ("Auth");
		private readonly UnityEngine.Profiling.CustomSampler cookieSampler = UnityEngine.Profiling.CustomSampler.Create ("ConCookie");
		private readonly UnityEngine.Profiling.CustomSampler handlerSampler = UnityEngine.Profiling.CustomSampler.Create ("Handler");

		private void HandleRequest (IAsyncResult _result) {
			HttpListener listenerInstance = (HttpListener)_result.AsyncState;
			if (!listenerInstance.IsListening) {
				return;
			}

#if ENABLE_PROFILER
			UnityEngine.Profiling.Profiler.BeginThreadProfiling ("AllocsMods", "WebRequest");
			getContextSampler.Begin ();
			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
			getContextSampler.End ();
			try {
#else
			HttpListenerContext ctx = listenerInstance.EndGetContext (_result);
			listenerInstance.BeginGetContext (HandleRequest, listenerInstance);
#endif
			try {
				HttpListenerRequest request = ctx.Request;
				HttpListenerResponse response = ctx.Response;
				response.SendChunked = false;

				response.ProtocolVersion = httpProtocolVersion;

				// No game yet -> fail request
				if (GameManager.Instance.World == null) {
					response.StatusCode = (int) HttpStatusCode.ServiceUnavailable;
					return;
				}

				if (request.Url == null) {
					response.StatusCode = (int) HttpStatusCode.BadRequest;
					return;
				}

				authSampler.Begin ();
				int permissionLevel = DoAuthentication (request, out WebConnection conn);
				authSampler.End ();

				//Log.Out ("Login status: conn!=null: {0}, permissionlevel: {1}", conn != null, permissionLevel);

				cookieSampler.Begin ();
				if (conn != null) {
					Cookie cookie = new Cookie ("sid", conn.SessionID, "/") {
						Expired = false,
						Expires = DateTime.MinValue,
						HttpOnly = true,
						Secure = false
					};
					response.AppendCookie (cookie);
				}
				cookieSampler.End ();

				string requestPath = request.Url.AbsolutePath;

				if (requestPath.Length < 2) {
					response.Redirect (indexPageUrl);
					return;
				}

				RequestContext context = new RequestContext (requestPath, request, response, conn, permissionLevel);

				if (context.Method == ERequestMethod.Other) {
					context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
					return;
				}
				
				ApplyPathHandler (context);

			} catch (IOException e) {
				if (e.InnerException is SocketException) {
					Log.Out ($"[Web] Error in Web.HandleRequest(): Remote host closed connection: {e.InnerException.Message}");
				} else {
					Log.Out ($"[Web] Error (IO) in Web.HandleRequest(): {e}");
				}
			} catch (Exception e) {
				Log.Error ("[Web] Error in Web.HandleRequest(): ");
				Log.Exception (e);
			} finally {
				if (!ctx.Response.SendChunked) {
					ctx.Response.Close ();
				}
			}
#if ENABLE_PROFILER
			} finally {
				listenerInstance.BeginGetContext (handleRequestDelegate, listenerInstance);
				UnityEngine.Profiling.Profiler.EndThreadProfiling ();
			}
#endif
		}

		public void ApplyPathHandler (RequestContext _context) {
			for (int i = handlers.Count - 1; i >= 0; i--) {
				AbsHandler handler = handlers [i];

				if (!_context.RequestPath.StartsWith (handler.UrlBasePath)) {
					continue;
				}

				if (!handler.IsAuthorizedForHandler (_context.Connection, _context.PermissionLevel)) {
					_context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
					if (_context.Connection != null) {
						//Log.Out ("Web.HandleRequest: user '{0}' not allowed to access '{1}'", _con.SteamID, handler.ModuleName);
					}
				} else {
					handlerSampler.Begin ();
					handler.HandleRequest (_context);
					handlerSampler.End ();
				}

				return;
			}

			// Not really relevant for non-debugging purposes:
			//Log.Out ("Error in Web.HandleRequest(): No handler found for path \"" + _requestPath + "\"");
			_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
		}

		private int DoAuthentication (HttpListenerRequest _req, out WebConnection _con) {
			_con = null;

			string sessionId = _req.Cookies ["sid"]?.Value;

			IPEndPoint reqRemoteEndPoint = _req.RemoteEndPoint;
			if (reqRemoteEndPoint == null) {
				Log.Warning ("[Web] No RemoteEndPoint on web request");
				return AdminWebModules.PermissionLevelGuest;
			}
			
			if (!string.IsNullOrEmpty (sessionId)) {
				_con = ConnectionHandler.IsLoggedIn (sessionId, reqRemoteEndPoint.Address);
				if (_con != null) {
					int level1 = GameManager.Instance.adminTools.Users.GetUserPermissionLevel (_con.UserId);
					int level2 = int.MaxValue;
					if (_con.CrossplatformUserId != null) {
						level2 = GameManager.Instance.adminTools.Users.GetUserPermissionLevel (_con.CrossplatformUserId);
					}

					return Math.Min (level1, level2);
				}
			}

			if (!_req.Headers.TryGetValue ("X-SDTD-API-TOKENNAME", out string apiTokenName) ||
			    !_req.Headers.TryGetValue ("X-SDTD-API-SECRET", out string apiTokenSecret)) {
				return AdminWebModules.PermissionLevelGuest;
			}

			int adminLevel = AdminApiTokens.Instance.GetPermissionLevel (apiTokenName, apiTokenSecret);
			if (adminLevel < int.MaxValue) {
				return adminLevel;
			}

			Log.Warning ($"[Web] Invalid Admintoken used from {reqRemoteEndPoint}");

			return AdminWebModules.PermissionLevelGuest;
		}
	}
}