namespace inutralia.API
{
    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;

    /// <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)
                    {
                        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; }
        }
    }
}
