using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using Webserver.WebAPI;

namespace Webserver.UrlHandlers {
	public class ApiHandler : AbsHandler {
		private readonly Dictionary<string, AbsWebAPI> apis = new CaseInsensitiveStringDictionary<AbsWebAPI> ();

		public ApiHandler () : base (null) {
		}
		
		private static readonly Type[] apiWithParentCtorTypes = { typeof (Web) };
		private static readonly object[] apiWithParentCtorArgs = new object[1];
		private static readonly Type[] apiEmptyCtorTypes = { };
		private static readonly object[] apiEmptyCtorArgs = { };

		public override void SetBasePathAndParent (Web _parent, string _relativePath) {
			base.SetBasePathAndParent (_parent, _relativePath);

			apiWithParentCtorArgs[0] = _parent;

			ReflectionHelpers.FindTypesImplementingBase (typeof (AbsWebAPI), apiFoundCallback);

			// Permissions that don't map to a real API
			addApi (new Null ("viewallclaims"));
			addApi (new Null ("viewallplayers"));
		}

		private void apiFoundCallback (Type _type) {
			ConstructorInfo ctor = _type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
				apiWithParentCtorTypes, null);
			if (ctor != null) {
				AbsWebAPI apiInstance = (AbsWebAPI)ctor.Invoke (apiWithParentCtorArgs);
				addApi (apiInstance);
				return;
			}

			ctor = _type.GetConstructor (BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null,
				apiEmptyCtorTypes, null);
			if (ctor != null) {
				AbsWebAPI apiInstance = (AbsWebAPI)ctor.Invoke (apiEmptyCtorArgs);
				addApi (apiInstance);
			}
		}

		private void addApi (AbsWebAPI _api) {
			apis.Add (_api.Name, _api);
			parent.OpenApiHelpers.LoadOpenApiSpec (_api);
		}

		private static readonly UnityEngine.Profiling.CustomSampler apiHandlerSampler = UnityEngine.Profiling.CustomSampler.Create ("API_Handler");

		private bool HandleCors (RequestContext _context) {
			_context.Request.Headers.TryGetValue ("Origin", out string origin);
			_context.Response.AddHeader ("Access-Control-Allow-Origin", origin ?? "*");

			if (_context.Method != ERequestMethod.OPTIONS) {
				return false;
			}

			if (!_context.Request.Headers.TryGetValue ("Access-Control-Request-Method", out _)) {
				return false;
			}

			_context.Response.AddHeader ("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS, HEAD");
			_context.Response.AddHeader ("Access-Control-Allow-Headers", "X-SDTD-API-TOKENNAME, X-SDTD-API-SECRET");
			_context.Response.AddHeader ("Access-Control-Allow-Credentials", "true");
			return true;
		}

		public override void HandleRequest (RequestContext _context) {

			string apiName;
			string subPath = null;

			int pathSeparatorIndex = _context.RequestPath.IndexOf ('/', urlBasePath.Length);
			if (pathSeparatorIndex >= 0) {
				apiName = _context.RequestPath.Substring (urlBasePath.Length, pathSeparatorIndex - urlBasePath.Length);
				subPath = _context.RequestPath.Substring (pathSeparatorIndex + 1);
			} else {
				apiName = _context.RequestPath.Substring (urlBasePath.Length);
			}
			
			if (!apis.TryGetValue (apiName, out AbsWebAPI api)) {
				Log.Warning ($"[Web] In {nameof(ApiHandler)}.HandleRequest(): No handler found for API \"{apiName}\"");
				_context.Response.StatusCode = (int) HttpStatusCode.NotFound;
				return;
			}

			// CORS specific stuff
			if (HandleCors (_context)) {
				return;
			}
			// CORS end
			
			_context.RequestPath = subPath;

			if (!api.Authorized (_context)) {
				_context.Response.StatusCode = (int) HttpStatusCode.Forbidden;
				if (_context.Connection != null) {
					//Log.Out ($"{nameof(ApiHandler)}: user '{user.SteamID}' not allowed to execute '{apiName}'");
				}

				return;
			}

			try {
				apiHandlerSampler.Begin ();
				api.HandleRequest (_context);
				apiHandlerSampler.End ();
			} catch (Exception e) {
				Log.Error ($"[Web] In {nameof(ApiHandler)}.HandleRequest(): Handler {api.Name} threw an exception:");
				Log.Exception (e);
				_context.Response.StatusCode = (int) HttpStatusCode.InternalServerError;
			}
		}
	}
}