using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.Reflection.Metadata;
using TestMakerFreeWebApp.ViewModels;
using TestMakerFreeWebApp.Data;
using Microsoft.AspNetCore.Identity;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Net.Http;
using Newtonsoft.Json;

namespace TestMakerFreeWebApp.Controllers
{
   public class TokenController : BaseApiController
   {
      #region Właościwości prywatne
      #endregion

      #region Konstruktor
      public TokenController(
          ApplicationDbContext context,
          RoleManager<IdentityRole> roleManager,
          UserManager<ApplicationUser> userManager,
          SignInManager<ApplicationUser> signInManager,
          IConfiguration configuration
          )
          : base(context, roleManager, userManager, configuration)
      {
         SignInManager = signInManager;
      }
      #endregion

      #region Właściwości
      protected SignInManager<ApplicationUser> SignInManager
      {
         get; private set;
      }
      #endregion

      [HttpPost("Auth")]
      public async Task<IActionResult> Auth([FromBody]TokenRequestViewModel model)
      {
         // Zwróć ogólny kod statusu HTTP 500,
         // jeśli dane przesłane przez klienta są niepoprawne
         if (model == null) return new StatusCodeResult(500);

         switch (model.grant_type)
         {
            case "password":
               return await GetToken(model);
            case "refresh_token":
               return await RefreshToken(model);
            default:
               // Nieobsługiwane, zwróć kod statusu HTTP 401
               return new UnauthorizedResult();
         }
      }

      [HttpPost("Facebook")]
      public async Task<IActionResult> Facebook([FromBody]ExternalLoginRequestViewModel model)
      {
         try
         {
            var fbAPI_url = "https://graph.facebook.com/v2.12/";
            var fbAPI_queryString = String.Format(
                "me?scope=email&access_token={0}&fields=id,name,email",
                model.access_token);
            string result = null;

            // Pobierz informacje o użytkowniku z Facebook Graph v2.12
            using (var c = new HttpClient())
            {
               c.BaseAddress = new Uri(fbAPI_url);
               var response = await c
                   .GetAsync(fbAPI_queryString);
               if (response.IsSuccessStatusCode)
               {
                  result = await response.Content.ReadAsStringAsync();
               }
               else throw new Exception("Błąd uwierzytelniania");
            };

            // Wczytaj wynikowe dane JSON to słownika
            var epInfo = JsonConvert.DeserializeObject<Dictionary<string, string>>(result);
            var info = new UserLoginInfo("facebook", epInfo["id"], "Facebook");

            // Sprawdź, czy użytkownik zarejestrował się wcześniej przy użyciu zewnętrznego dostawcy
            var user = await UserManager.FindByLoginAsync(
                info.LoginProvider, info.ProviderKey);
            if (user == null)
            {
               // Jeśli jesteśmy w tym miejscu, użytkownik nigdy nie próbował logować się
               // przy użyciu tego zewnętrznego dostawcy. Mógł jednak użyć innego dostawcy
               // lub konta lokalnego, więc spróbuj go poszukać po adresie e-mail.

               // Sprawdź, czy istnieje w bazie użytkownik o danym adresie e-mail.
               user = await UserManager.FindByEmailAsync(epInfo["email"]);
               if (user == null)
               {
                  // Użytkownika nie znaleziono, więc zarejestruj nowego
                  // na podstawie danych otrzymanych od Facebooka
                  DateTime now = DateTime.Now;
                  var username = String.Format("FB{0}{1}",
                          epInfo["id"],
                          Guid.NewGuid().ToString("N")
                      );
                  user = new ApplicationUser()
                  {
                     SecurityStamp = Guid.NewGuid().ToString(),
                     // Zapewnij unikatowość nazwy użytkownika
                     UserName = username,
                     Email = epInfo["email"],
                     DisplayName = epInfo["name"],
                     CreatedDate = now,
                     LastModifiedDate = now
                  };

                  // Dodaj użytkownika do bazy danych z losowym hasłem
                  await UserManager.CreateAsync(user,
                      DataHelper.GenerateRandomPassword());

                  // Przypisz użytkownika do roli 'ZarejestrowanyUżytkownik'
                  await UserManager.AddToRoleAsync(user, "ZarejestrowanyUżytkownik");

                  // Usuń potwierdzanie e-maila i blokadę
                  user.EmailConfirmed = true;
                  user.LockoutEnabled = false;

                  // Zapisz wszystko w bazie danych
                  DbContext.SaveChanges();
               }
               // Zarejestruj zewnętrznego dostawcę dla użytkownika 
               var ir = await UserManager.AddLoginAsync(user, info);
               if (ir.Succeeded)
               {
                  // Zapisz zmiany w bazie danych
                  DbContext.SaveChanges();
               }
               else throw new Exception("Błąd uwierzytelniania");
            }

            // Utwórz token odświeżenia
            var rt = CreateRefreshToken(model.client_id, user.Id);

            // Dodaj token odświeżenia do bazy danych
            DbContext.Tokens.Add(rt);
            DbContext.SaveChanges();

            // Utwórz i zwróć token dostępowy
            var t = CreateAccessToken(user.Id, rt.Value);
            return Json(t);
         }
         catch (Exception ex)
         {
            // Zwróć klientowi kod statusu HTTP 400
            return BadRequest(new { Error = ex.Message });
         }
      }

      [HttpGet("ExternalLogin/{provider}")]
      public IActionResult ExternalLogin(string provider, string returnUrl = null)
      {
         switch (provider.ToLower())
         {
            case "facebook":
               // case "google":
               // case "twitter":
               // NA PRZYSZŁOŚC: dodaj wszystkich obsługiwanych dostawców

               // Redirect the request to the external provider.
               var redirectUrl = Url.Action(
                   nameof(ExternalLoginCallback),
                   "Token",
                   new { returnUrl });
               var properties =
                   SignInManager.ConfigureExternalAuthenticationProperties(
                       provider,
                       redirectUrl);
               return Challenge(properties, provider);
            default:
               // Dostawca nie jest obsługiwany
               return BadRequest(new
               {
                  Error = String.Format("Dostawca '{0}' nie jest obsługiwany.", provider)
               });
         }
      }

      [HttpGet("ExternalLoginCallback")]
      public async Task<IActionResult> ExternalLoginCallback(
    string returnUrl = null, string remoteError = null)
      {
         if (!String.IsNullOrEmpty(remoteError))
         {
            // NA POŹNIEJ: obsługa błędów dostawcy
            throw new Exception(String.Format("Błąd u zewnętrznego dostawcy: {0}", remoteError));
         }

         // Pobierz dane uzyskane od zewnętrznego dostawcy
         var info = await SignInManager.GetExternalLoginInfoAsync();
         if (info == null)
         {
            // Jeśli nie ma danych, zgłoś błąd
            throw new Exception("BŁĄD: brak danych z logowania");
         }

         // Sprawdź, czy użytkownik zarejestrował się wcześniej przy użyciu zewnętrznego dostawcy
         var user = await UserManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey);
         if (user == null)
         {
            // Jeśli jesteśmy w tym miejscu, użytkownik nigdy nie próbował logować się
            // przy użyciu tego zewnętrznego dostawcy. Mógł jednak użyć innego dostawcy
            // lub konta lokalnego, więc spróbuj go poszukać po adresie e-mail

            // Pobierz roszczenie 'emailaddress'
            var emailKey = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
            var email = info.Principal.FindFirst(emailKey).Value;

            // Sprawdź, czy istnieje w bazie użytkownik o danym adresie e-mail
            user = await UserManager.FindByEmailAsync(email);
            if (user == null)
            {
               // Użytkownika nie znaleziono, więc zarejestruj nowego
               // na podstawie danych otrzymanych od zewnętrznego dostawcy
               DateTime now = DateTime.Now;

               // Utwórz unikatową nazwę użytkownika na podstawie roszczenia 'nameidentifier'
               var idKey = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
               var username = String.Format("{0}{1}{2}",
                   info.LoginProvider,
                   info.Principal.FindFirst(idKey).Value,
                   Guid.NewGuid().ToString("N")
                   );

               user = new ApplicationUser()
               {
                  SecurityStamp = Guid.NewGuid().ToString(),
                  UserName = username,
                  Email = email,
                  CreatedDate = now,
                  LastModifiedDate = now
               };

               // Dodaj użytkownika do bazy danych z losowym hasłem
               await UserManager.CreateAsync(
                   user,
                   DataHelper.GenerateRandomPassword());

               // Przypisz użytkownika do roli 'ZarejestrowanyUżytkownik'
               await UserManager.AddToRoleAsync(user, "ZarejestrowanyUżytkownik");

               // Usuń potwierdzanie e-maila i blokadę
               user.EmailConfirmed = true;
               user.LockoutEnabled = false;

               // Zapisz wszystko w bazie danych
               await DbContext.SaveChangesAsync();
            }
            // Zarejestruj zewnętrznego dostawcę dla użytkownika
            var ir = await UserManager.AddLoginAsync(user, info);
            if (ir.Succeeded)
            {
               // Zapisz zmiany w bazie danych
               DbContext.SaveChanges();
            }
            else throw new Exception("Błąd uwierzytelniania");
         }

         // Utwórz token odświeżenia
         var rt = CreateRefreshToken("TestMakerFree", user.Id);

         // Dodaj token odświeżenia do bazy danych
         DbContext.Tokens.Add(rt);
         DbContext.SaveChanges();

         // Utwórz i zwróć token dostępowy
         var t = CreateAccessToken(user.Id, rt.Value);

         // Zwróć znacznik<SCRIPT> wywołujący funkcję JS
         // zarejestrowaną w globalnej przestrzeni okna nadrzędnego
         return Content(
             "<script type=\"text/javascript\">" +
             "window.opener.externalProviderLogin(" +
                 JsonConvert.SerializeObject(t, JsonSettings) +
             ");" +
             "window.close();" +
             "</script>",
             "text/html"
             );
      }

      private async Task<IActionResult> GetToken(TokenRequestViewModel model)
      {
         try
         {
            // Sprawdź, czy istnieje użytkownik o podanej nazwie
            var user = await UserManager.FindByNameAsync(model.username);
            // Dopóść użycie adresu e-mail w zastępstwie nazwy użytkownika
            if (user == null && model.username.Contains("@"))
               user = await UserManager.FindByEmailAsync(model.username);

            if (user == null
                || !await UserManager.CheckPasswordAsync(user, model.password))
            {
               // Użytkownik nie istnieje lub hasła nie pasują do siebie
               return new UnauthorizedResult();
            }

            // Nazwa użytkownika i hasło jest prawidłowe - utwórz tokeny
            var rt = CreateRefreshToken(model.client_id, user.Id);

            // Dodaj nowy tokoen odświeżania do bazy danych
            DbContext.Tokens.Add(rt);
            DbContext.SaveChanges();
            
            // Utwórz i zwróć token dostępowy
            var t = CreateAccessToken(user.Id, rt.Value);
            return Json(t);

         }
         catch (Exception ex)
         {
            return new UnauthorizedResult();
         }
      }

      private async Task<IActionResult> RefreshToken(TokenRequestViewModel model)
      {
         try
         {
            // Sprawdź, czy otrzymany token oświeżenia istnieje dla danego clientId
            var rt = DbContext.Tokens
                .FirstOrDefault(t =>
                t.ClientId == model.client_id
                && t.Value == model.refresh_token);

            if (rt == null)
            {
               // Token nie istnieje lub jest niepoprawny (albo przekazano złe clientId)
               return new UnauthorizedResult();
            }

            // Sprawdź, czy istnieje użytkownik o userId z tokena odświeżenia
            var user = await UserManager.FindByIdAsync(rt.UserId);

            if (user == null)
            {
               // Użytkownika nie odnaleziono lub UserId jest nieprawidłowe
               return new UnauthorizedResult();
            }

            // Wygeneruj nowy token odświeżania
            var rtNew = CreateRefreshToken(rt.ClientId, rt.UserId);

            // Unieważnij stary token odświeżania (poprzez jego usunięcie)
            DbContext.Tokens.Remove(rt);

            // Dodaj nowy token odświeżania
            DbContext.Tokens.Add(rtNew);

            // Zapisz zmiany w bazie danych
            DbContext.SaveChanges();

            // Utwórz nowy token dostępowy
            var response = CreateAccessToken(rtNew.UserId, rtNew.Value);

            // ... i wyślij go do klienta
            return Json(response);
         }
         catch (Exception ex)
         {
            return new UnauthorizedResult();
         }
      }

      private Token CreateRefreshToken(string clientId, string userId)
      {
         return new Token()
         {
            ClientId = clientId,
            UserId = userId,
            Type = 0,
            Value = Guid.NewGuid().ToString("N"),
            CreatedDate = DateTime.UtcNow
         };
      }

      private TokenResponseViewModel CreateAccessToken(string userId, string refreshToken)
      {
         DateTime now = DateTime.UtcNow;

         // Dodaj odpowiednie roszczenia do JWT (RFC7519).
         // Więcej informacji znajdziesz pod adresem https://tools.ietf.org/html/rfc7519#section-4.1
         var claims = new[] {
                new Claim(JwtRegisteredClaimNames.Sub, userId),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                new Claim(JwtRegisteredClaimNames.Iat,
                    new DateTimeOffset(now).ToUnixTimeSeconds().ToString())
                // NA-POZNIEJ: dodaj dodatkowe roszczenia
            };

         var tokenExpirationMins =
             Configuration.GetValue<int>("Auth:Jwt:TokenExpirationInMinutes");
         var issuerSigningKey = new SymmetricSecurityKey(
             Encoding.UTF8.GetBytes(Configuration["Auth:Jwt:Key"]));

         var token = new JwtSecurityToken(
             issuer: Configuration["Auth:Jwt:Issuer"],
             audience: Configuration["Auth:Jwt:Audience"],
             claims: claims,
             notBefore: now,
             expires: now.Add(TimeSpan.FromMinutes(tokenExpirationMins)),
             signingCredentials: new SigningCredentials(
                 issuerSigningKey, SecurityAlgorithms.HmacSha256)
         );
         var encodedToken = new JwtSecurityTokenHandler().WriteToken(token);

         return new TokenResponseViewModel()
         {
            token = encodedToken,
            expiration = tokenExpirationMins,
            refresh_token = refreshToken
         };
      }
   }
}
