context: using System;
  using System.Collections.Generic;
  using System.Linq;
  using System.Net.Http;
  using System.Net.Http.Headers;
  using System.Security.Authentication;
  using System.Security.Claims;
  using System.Threading.Tasks;
  using Machete.Data;
  using Machete.Data.Identity;
  using Machete.Web.Helpers.Api;
  using Machete.Web.Helpers.Api.Identity;
  using Machete.Web.ViewModel.Api.Identity;
  using Microsoft.AspNetCore.Authentication;
  using Microsoft.AspNetCore.Authentication.Cookies;
  using Microsoft.AspNetCore.Authorization;
  using Microsoft.AspNetCore.Http;
  using Microsoft.AspNetCore.Identity;
  using Microsoft.AspNetCore.Mvc;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.Options;
  using Microsoft.IdentityModel.Tokens;
  using Newtonsoft.Json;
  using RestSharp;
  
  namespace Machete.Web.Controllers.Api.Identity
  {
      #region [Summary]
      ///<summary>
      /// 
      /// This controller exists outside the Machete.Api.Controllers because it serves a different purpose and because the
      /// routes are defined separately. Its fields and dependencies were created to replicate the functionality of
      /// IdentityServer3 without having to modify the https://github.com/SavageLearning/machete-ui project beyond port
      /// mappings and domain names. As such, the namespace division exists to call out that this could be (and once was)
      /// a separate project.
      ///
      /// After going down the path of implementing full-on JWT auth using the AngularJS oidc-client library, we decided
      /// to use cookie auth to save time. So currently, we only issue a JWT token for a user that is already authenticated
      /// using cookies. There's a branch with the working JWT implementation here:
      ///
      /// https://github.com/chaim1221/Machete/blob/feature/api-jwt/Api/Identity/Controllers/IdentityController.cs#L82
      ///
      /// Sources used:
      /// https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
      /// https://github.com/mmacneil/AngularASPNETCore2WebApiAuth/blob/master/src/Controllers/AccountsController.cs (MIT)
      /// https://github.com/mmacneil/AngularASPNETCore2WebApiAuth/blob/master/src/Controllers/AuthController.cs (MIT)
      /// https://github.com/IdentityServer/IdentityServer3 (Apache-2.0)
      /// https://github.com/IdentityServer/IdentityServer4 (Apache-2.0)
      /// 
      /// </summary>
      #endregion [Summary]
      [Route("id")]
      public class IdentityController : Controller
      {
          private readonly UserManager<MacheteUser> _userManager;
          private readonly SignInManager<MacheteUser> _signinManager;
  
          private readonly IJwtFactory _jwtFactory;
          private readonly JwtIssuerOptions _jwtOptions;
          private readonly List<MacheteRole> _roles;
          private readonly IConfiguration _configuration;
  
          const string CookieName = ".AspNetCore.Identity.Application";
          const string CookieAuthenticationType = "Identity.Application";
  
          public IdentityController(
              UserManager<MacheteUser> userManager,
              SignInManager<MacheteUser> signInManager,
              RoleManager<MacheteRole> roleManager,
              IJwtFactory jwtFactory,
              IOptions<JwtIssuerOptions> jwtOptions,
              IConfiguration configuration
          )
          {
              ThrowIfInvalidOptions(jwtOptions.Value);
  
              _userManager = userManager;
              _signinManager = signInManager;
  
              _roles = roleManager.Roles.ToList();
              _jwtFactory = jwtFactory;
              _jwtOptions = jwtOptions.Value;
              _configuration = configuration;
          }
          
          [HttpGet]
          [Route("authorize")]
          public async Task<IActionResult> Authorize()
          {
              if (!User.Identity.IsAuthenticated) return await Task.FromResult(new UnauthorizedResult());
              
              var verifiedUser = await _userManager.FindByNameAsync(User.Identity.Name);
              foreach (var role in _roles)
              {
                  var hasRole = await _userManager.IsInRoleAsync(verifiedUser, role.Name);
                  if (hasRole) verifiedUser.UserRoles.Add(new MacheteUserRole { Role = role, User = verifiedUser }); // TODO completely untested
              }
              _jwtOptions.Issuer = Routes.GetHostFrom(Request).IdentityRoute();
              _jwtOptions.Nonce = Guid.NewGuid().ToString(); // corners were cut; this is supposed to identify the client
              var claimsIdentity = await _jwtFactory.GenerateClaimsIdentity(verifiedUser, _jwtOptions);
              var jwt = await _jwtFactory.GenerateEncodedToken(claimsIdentity, _jwtOptions);
              
              return await Task.FromResult<IActionResult>(
                  new OkObjectResult(new { access_token = jwt })
              );
          }
  
          [HttpPost("login")]
          public async Task<IActionResult> Login([FromBody] CredentialsViewModel model)
          {
              if (!ValidateLogin(model)) return BadRequest(ModelState);
              
              await VerifyClaimsExistFor(model.UserName);
              
              var result = await _signinManager.PasswordSignInAsync(model.UserName, model.Password, model.Remember, false);
  
              if (result?.Succeeded == true) return await Task.FromResult(new OkResult());
  
              ModelState.TryAddModelError("login_failure", "Invalid username or password.");
              return BadRequest(ModelState);
          }
          
          // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
          // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/index?view=aspnetcore-2.2#environment-variables-configuration-provider
          [HttpGet]
          [Route("signin-facebook")]
          public async Task<IActionResult> FacebookLogin([FromQuery] ExternalLoginViewModel viewModel)
          {
              var host = Routes.GetHostFrom(Request);
  
              if (viewModel.State == _configuration["Authentication:State"])
              {
                  var httpClient = new HttpClient();
                  var appId = _configuration["Authentication:Facebook:AppId"];
                  var redirectUri = Routes.FacebookSignin(host);
                  var appSecret = _configuration["Authentication:Facebook:AppSecret"];
  
                  var tokenResponse = await httpClient.GetAsync(
                      $"https://graph.facebook.com/v3.2/oauth/access_token?" +
                      $"client_id={appId}&" +
                      $"redirect_uri={redirectUri}&" +
                      $"client_secret={appSecret}&" +
                      $"code={viewModel.Code}");
  
                  await SigninByEmailAsync(tokenResponse, httpClient, "facebook", redirectUri);
              }
              // They're either logged in now, or they aren't.
              return await Task.FromResult<IActionResult>(new RedirectResult(host.V2AuthorizationEndpoint()));
          }
  
          // https://developers.google.com/identity/protocols/OAuth2WebServer
          // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/index?view=aspnetcore-2.2#environment-variables-configuration-provider
          [HttpGet]
          [Route("signin-google")]
          public async Task<IActionResult> GoogleLogin([FromQuery] ExternalLoginViewModel viewModel)
          {
              var host = Routes.GetHostFrom(Request);
  
              if (viewModel.State == _configuration["Authentication:State"])
              {
                  var httpClient = new HttpClient();
                  var appId = _configuration["Authentication:Google:ClientId"];
                  var redirectUri = $"{host}id/signin-google"; // TODO
                  var appSecret = _configuration["Authentication:Google:ClientSecret"];
  
                  var content = new StringContent($@"
  {{
    ""code"": ""{viewModel.Code}"",
    ""client_id"": ""{appId}"",
    ""client_secret"": ""{appSecret}"",
    ""redirect_uri"": ""{redirectUri}"",
    ""grant_type"": ""authorization_code""
  }}
  "
                  );
  
                  // TODO we *should* get this URL from https://accounts.google.com/.well-known/openid-configuration
                  var tokenResponse = await httpClient.PostAsync("https://oauth2.googleapis.com/token", content);
                  
                  await SigninByEmailAsync(tokenResponse, httpClient, "google", redirectUri);
              }
              // They're either logged in now, or they aren't.
              return await Task.FromResult<IActionResult>(new RedirectResult(host.V2AuthorizationEndpoint()));
          }
  
          private async Task SigninByEmailAsync(HttpResponseMessage tokenResponse, HttpClient httpClient, string provider, string redirectUri)
          {
              var tokenResponseContent = tokenResponse.Content.ReadAsStringAsync();
              if (tokenResponse.IsSuccessStatusCode)
              {
                  var tokenObject = JsonConvert.DeserializeObject<ExternalLoginAccessToken>(tokenResponseContent.Result);
  
                  httpClient.DefaultRequestHeaders.Authorization =
                      new AuthenticationHeaderValue("Bearer", tokenObject.access_token);
  
                  string profileResponseUrl;
                  switch (provider)
                  {
                      case "facebook":
                          profileResponseUrl = $"https://graph.facebook.com/me?" +
                                               $"fields=name,email&" +
                                               $"access_token={tokenObject.access_token}";
                          break;
                      case "google":
                          // TODO we *should* get this URL from https://accounts.google.com/.well-known/openid-configuration
                          profileResponseUrl = "https://openidconnect.googleapis.com/v1/userinfo";
                          break;
                      default:
                          throw new Exception("Unable to parse provider.");
                  }
                  
                  var profileResponse = await httpClient.GetAsync(profileResponseUrl);
                  var profileResponseContent = profileResponse.Content.ReadAsStringAsync();
                  var profile = JsonConvert.DeserializeObject<ExternalLoginProfile>(profileResponseContent.Result);
                  var user = await _userManager.FindByEmailAsync(profile.email);
                  if (user == null)
                  {
                      var name = profile.name?.Split(' ');
                      if (string.IsNullOrEmpty(profile.name))
                          name = new[] { "", "" };
                         
                      var macheteUser = new MacheteUser
                      {
                          UserName = profile.email,
                          Email = profile.email, // Machete pls
  
gt:                         FirstName = name[0],