using inutralia.Abstractions;
using inutralia.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace inutralia.API
{
	/// <summary>
	/// Interfaz con el WebService de peticiones de la API de iNutralia
	/// </summary>
	public class WebService : IWebService
	{
		/// <summary>
		/// Procesa las peticiones
		/// </summary>
		private HttpClient _client;

		/// <summary>
		/// Nombre del usuario actual
		/// </summary>
		private string _username;

		private JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, NullValueHandling = NullValueHandling.Ignore };

		public WebService()
		{
			Reset();
		}

		#region IDataPersistenceService

		/// <summary>
		/// Recupera de forma asíncrona los datos de una entidad a partir de su
		/// identificador
		/// </summary>
		/// <typeparam name="T">Tipo de dato de cada entidad</typeparam>
		/// <param name="id">Identificador de la entidad</param>
		/// <returns>Instancia de T correspondiente al identificador</returns>
		public async Task<T> GetItemAsync<T>(int id) where T : IIdentifiableEntity
		{
			// Preparar el path de acceso al elemento
			string fullPath = string.Format("{0}/{1}", GetDataPath<T>(), id);

			// Crear uri a partir de la URL base y el path
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, fullPath));

            try
            {
                // Hacer la petición
                var response = await _client.GetAsync (uri);
                if (response.IsSuccessStatusCode)
                {
                    // Si es correcta, traer los datos y deserializarlos
                    var content = await response.Content.ReadAsStringAsync ();
                    return JsonConvert.DeserializeObject<T> (content);
                } //endif
            }
            catch(Exception)
            { }

			// En caso contrario retornar error
			return default(T);
		}

		/// <summary>
		/// Obtiene de forma asíncrona una lista de elementos de cierto tipo
		/// </summary>
		/// <typeparam name="T">Tipo de datos de cada elemento</typeparam>
		/// <returns>Lista de elementos obtenidos</returns>
		public async Task<List<T>> RefreshListAsync<T>() where T : IIdentifiableEntity
		{
			// Crear uri a partir de la URL base y el path
			return await RefreshListAsync<T>(GetDataPath<T>());
		}

		/// <summary>
		/// Recupera de forma asíncrona los datos de una entidad
		/// </summary>
		/// <typeparam name="T">Tipo de dato de la entidad</typeparam>
		/// <param name="item">La entidad a actualizar</param>
		/// <returns>La entidad actualizada</returns>
		public async Task<bool> RefreshItemAsync<T>(T item) where T : IIdentifiableEntity
		{
			// Preparar el path de acceso al elemento
			string fullPath = string.Format("{0}/{1}", GetDataPath(item.GetType() ), item.Id);

			// Crear uri a partir de la URL base y el path
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, fullPath));

            try
            {
                // Hacer la petición
                var response = await _client.GetAsync (uri);
                if (response.IsSuccessStatusCode)
                {
                    // Si es correcta, traer los datos y deserializarlos
                    var responseContent = await response.Content.ReadAsStringAsync ();
                    try
                    {
                        JsonConvert.PopulateObject (responseContent, item, _serializerSettings);
                    }
                    catch (Exception e)
                    {
                        return false;
                    }

                    return true;
                } //endif
            }
            catch(Exception)
            { }

			// En caso contrario retornar error
			return false;
		}

		/// <summary>
		/// Actualiza de forma asíncrona los datos de una entidad
		/// </summary>
		/// <typeparam name="T">Tipo de datos de cada entidad</typeparam>
		/// <param name="dataPath">El path dentro del API</param>
		/// <param name="item">Los datos de la entidad a actualizar</param>
		/// <param name="isNew">Indica si hay que crear una entidad nueva o modificar una existente</param>
		/// <returns>El ID de la entidad. Null en caso de error</returns>
		public async Task<int?> UpdateItemAsync<T>(T item, bool isNew = false) where T : IIdentifiableEntity
		{
			// Preparar el path de acceso al elemento
			string dataPath = GetDataPath(item.GetType());
			string fullPath = isNew ? dataPath : string.Format("{0}/{1}", dataPath, item.Id);

			// Crear uri a partir de la URL base y el path
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, fullPath));

			try
            { 
                // Serializar el objeto
			    var json = JsonConvert.SerializeObject(item);
			    var content = new StringContent(json, Encoding.UTF8, "application/json");

			    if (isNew)
			    {
				    // Utilizamos POST cuando el objeto es nuevo
				    var response = await _client.PostAsync(uri, content);

				    // Si todo fue bien, recibimos el objeto de vuelta (con el id con el que fue creado)
				    if (response.IsSuccessStatusCode)
				    {
					    var responseContent = await response.Content.ReadAsStringAsync();
					    JsonConvert.PopulateObject(responseContent, item, _serializerSettings);
					    return item.Id;
				    } //endif
			    }
			    else
			    {
				    // Utilizamos PUT cuando el objeto no es nuevo
				    var response = await _client.PutAsync(uri, content);

				    // Si todo fue bien, el servidor suele responder con 204 No Content
				    if (response.IsSuccessStatusCode)
					    return item.Id;
			    } //endif
            }
            catch (Exception)
            { }


            // Si llegamos aquí, es que la petición devolvió error
            return null;
		}

		/// <summary>
		/// Elimina de forma asíncrona una entidad
		/// </summary>
		/// <typeparam name="T">Tipo de dato de la entidad</typeparam>
		/// <param name="item">La entidad a eliminar</param>
		/// <returns>Si la operación se realizó correctamente</returns>
		public async Task<bool> DeleteItemAsync<T>(T item) where T : IIdentifiableEntity
		{
			// Preparar el path de acceso al elemento
			string fullPath = string.Format("{0}/{1}", GetDataPath(item.GetType()), item.Id);

			// Crear uri a partir de la URL base y el path
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, fullPath));

            try
            { 
			    // Hacer la petición
			    var response = await _client.DeleteAsync(uri);

			    // Indicar si todo fue correcto
			    return response.IsSuccessStatusCode;
            }
            catch (Exception)
            { }

            return false;
        }

        #endregion

        #region IWebService

        /// <summary>
        /// Borra la información relativa a las credenciales
        /// </summary>
        public void Reset()
		{
			_client = new HttpClient();
			_username = string.Empty;
		}

		/// <summary>
		/// Establece las credenciales de acceso a la API
		/// </summary>
		/// <param name="username">Nombre del usuario accediendo a la API</param>
		/// <param name="password">Contraseña del usuario</param>
		public void CredentialsSet(string username, string password)
		{
			// Nos guardamos el nombre del usuario
			_username = username;

			// Creamos el string de autenticación (base64(user:pass) )
			var authData = string.Format("{0}:{1}", username, password);
			var authHeaderValue = Convert.ToBase64String(Encoding.UTF8.GetBytes(authData));

			// Lo establecemos como cabecera por defecto para todas las peticiones que hagamos a partir de ahora
			_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authHeaderValue);
		}

		/// <summary>
		/// Cambia la contraseña del usuario actual
		/// </summary>
		/// <param name="currPass">Contraseña actual (para verificación)</param>
		/// <param name="newPass">Nueva contraseña</param>
		public async Task<bool> PasswordChange(string currPass, string newPass)
		{
			// Creamos el string de cambio de autenticación
			var newAuth = string.Format("{0}:{1}:{2}", _username, currPass, newPass);
			var param = Convert.ToBase64String(Encoding.UTF8.GetBytes(newAuth));

			// Crear uri a partir de la URL base y el path
			var path = string.Format("password/{0}", param);
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, path));

            bool retVal = false;
            try
            { 
			    // Realizamos la petición de cambio
			    var response = await _client.GetAsync(uri);

			    // Miramos si es correcta
			    retVal = response.IsSuccessStatusCode;

			    // Si es correcta actualizamos las credenciales
			    if (retVal)
				    CredentialsSet(_username, newPass);
            }
            catch (Exception)
            { }

            return retVal;
		}

		/// <summary>
		/// Petición directa al WebService (GET)
		/// </summary>
		/// <param name="resourcePath">Path del recurso REST al cual acceder</param>
		/// <param name="data">Objeto a serializar en la petición</param>
		/// <returns>Respuesta del servidor</returns>
		public async Task<HttpResponseMessage> RawMessage(string resourcePath, object data = null)
		{
			return await this.RawMessage(HttpMethod.Get, resourcePath, data);
		}

		/// <summary>
		/// Petición directa al WebService
		/// </summary>
		/// <param name="method">Método HTTP de la petición</param>
		/// <param name="resourcePath">Path del recurso REST al cual acceder</param>
		/// <param name="data">Objeto a serializar en la petición</param>
		/// <returns>Respuesta del servidor</returns>
		public async Task<HttpResponseMessage> RawMessage(HttpMethod method, string resourcePath, object data = null)
		{
			string fullPath = string.Format(Constants.ApiUrlTemplate, resourcePath);

			var req = new HttpRequestMessage(method, fullPath);
			if (data != null)
			{
				// Serializar el objeto
				var json = JsonConvert.SerializeObject(data);
				var content = new StringContent(json, Encoding.UTF8, "application/json");
				req.Content = content;
			} //endif

			return await _client.SendAsync(req);
		}

		/// <summary>
		/// Procesa la respuesta de una petición directa
		/// </summary>
		/// <typeparam name="T">Tipo de datos en el que convertir los datos de respuesta</typeparam>
		/// <param name="response">Respuesta devuelta por RawMessage</param>
		/// <param name="data">Objeto a poblar con los datos de respuesta. Se creará un nuevo objeto si este parámetro es null</param>
		/// <returns>El objeto poblado / creado. Si el status de la petición no es 2XX se devuelve null</returns>
		public async Task<T> ResponseProcess<T>(HttpResponseMessage response, T data = null) where T : class
		{
			if (response.IsSuccessStatusCode)
			{
				var content = await response.Content.ReadAsStringAsync();

				if (data == null)
					return JsonConvert.DeserializeObject<T>(content);

				JsonConvert.PopulateObject(content, data, _serializerSettings);
				return data;
			} //endif

			return null;
		}

		/// <summary>
		/// Realiza un petición directa y procesa su respuesta
		/// </summary>
		/// <typeparam name="T">Tipo de datos en el que convertir los datos de respuesta</typeparam>
		/// <param name="method">Método HTTP de la petición</param>
		/// <param name="resourcePath">Path del recurso REST al cual acceder</param>
		/// <param name="input">Objeto a serializar en la petición</param>
		/// <param name="output">Objeto a poblar con los datos de respuesta. Se creará un nuevo objeto si este parámetro es null</param>
		/// <returns>El objeto poblado / creado. Si el status de la petición no es 2XX se devuelve null</returns>
		public async Task<T> RawMessage<T>(HttpMethod method, string resourcePath, object input = null, T output = null) where T : class
		{
			return await ResponseProcess(await RawMessage(method, resourcePath, input), output);
		}

		/// <summary>
		/// Obtiene de forma asíncrona una lista de elementos de cierto tipo
		/// </summary>
		/// <typeparam name="T">Tipo de datos de cada elemento</typeparam>
		/// <param name="path">Path de acceso al recurso de la API</param>
		/// <returns>Lista de elementos obtenidos</returns>
		public async Task<List<T>> RefreshListAsync<T>(string path)
		{
			// Hacer la petición
			var response = await _client.GetAsync(string.Format(Constants.ApiUrlTemplate, path));
			if (response.IsSuccessStatusCode)
			{
				// Si es correcta, traer los datos y deserializarlos
				var content = await response.Content.ReadAsStringAsync();
				return JsonConvert.DeserializeObject<List<T>>(content);
			} //endif

			// La respuesta no es correcta, distinguir código de error
			switch (response.StatusCode)
			{
				// El usuario y contraseña son incorrectos
				case HttpStatusCode.Unauthorized:
					throw new UnauthorizedAccessException();

				// Cualquier otro
				default:
					throw new Exception(response.ReasonPhrase);
			} //endswitch
		}

        #endregion

        /// <summary>
        /// Retorna el path dentro del api para acceder a cierto tipo de entidad
        /// </summary>
        /// <typeparam name="T">Tipo de dato de la entidad</typeparam>
        /// <returns>El path correspondiente dentro del API</returns>
        private static string GetDataPath<T> ()
        {
            return GetDataPath (typeof (T));
        }

        /// <summary>
        /// Retorna el path dentro del api para acceder a cierto tipo de entidad
        /// </summary>
        /// <param name="t">Tipo de dato de la entidad</typeparam>
        /// <returns>El path correspondiente dentro del API</returns>
        private static string GetDataPath(Type t)
        { 
			// Si tiene el atributo DataPath, lo utilizamos
			DataPathAttribute attr = t.GetTypeInfo().GetCustomAttribute<DataPathAttribute>();
			if (attr != null)
				return attr.DataPath;

			// En caso contrario devolvemos el nombre de la clase en minúsculas y en plural
			return t.Name.ToLowerInvariant() + "s";
		}

		public async Task<HttpStatusCode?> RegisterUser(string userName, string passWord, string companyCode)
		{
			var dataChain = string.Format("{0}:{1}:{2}", userName, passWord, companyCode);
			var param = Convert.ToBase64String(Encoding.UTF8.GetBytes(dataChain));

			var path = string.Format("register/{0}", param);
			var uri = new Uri(string.Format(Constants.ApiUrlTemplate, path));

            try
            { 
			    var response = await _client.GetAsync(uri);

			    if (response.IsSuccessStatusCode)
				    return null;

                var content = await response.Content.ReadAsStringAsync();
                var respuesta = JsonConvert.DeserializeObject<dataResponse>(content);

                var desc_error = respuesta.error;

                return response.StatusCode;
            }
            catch (Exception)
            { }

            return HttpStatusCode.InternalServerError;
        }

        public class dataResponse
		{
			public string error { get; set; }
		}

	}
}
