/*
 * Copyright 2016-2019 Fraunhofer AISEC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *            $$\                           $$\ $$\   $$\
 *            $$ |                          $$ |\__|  $$ |
 *   $$$$$$$\ $$ | $$$$$$\  $$\   $$\  $$$$$$$ |$$\ $$$$$$\    $$$$$$\   $$$$$$\
 *  $$  _____|$$ |$$  __$$\ $$ |  $$ |$$  __$$ |$$ |\_$$  _|  $$  __$$\ $$  __$$\
 *  $$ /      $$ |$$ /  $$ |$$ |  $$ |$$ /  $$ |$$ |  $$ |    $$ /  $$ |$$ |  \__|
 *  $$ |      $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$ |  $$ |$$\ $$ |  $$ |$$ |
 *  \$$$$$$\  $$ |\$$$$$   |\$$$$$   |\$$$$$$  |$$ |  \$$$   |\$$$$$   |$$ |
 *   \_______|\__| \______/  \______/  \_______|\__|   \____/  \______/ \__|
 *
 * This file is part of Clouditor Community Edition.
 */

package io.clouditor.oauth;

import io.clouditor.Engine;
import io.clouditor.auth.AuthenticationService;
import io.clouditor.auth.LoginResponse;
import io.clouditor.auth.User;
import io.clouditor.util.PersistenceManager;
import java.net.URI;
import java.util.Base64;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Path("")
public class OAuthResource {

  private final AuthenticationService service;

  private final Engine engine;

  private static final Logger LOGGER = LoggerFactory.getLogger(OAuthResource.class);

  @Inject
  public OAuthResource(AuthenticationService service, Engine engine) {
    this.service = service;
    this.engine = engine;
  }

  @GET
  @Path("profile")
  public Profile getProfile() {
    var profile = new Profile();
    profile.setEnabled(
        this.engine.getOAuthClientId() != null
            && this.engine.getOAuthClientSecret() != null
            && this.engine.getOAuthAuthUrl() != null
            && this.engine.getOAuthTokenUrl() != null
            && this.engine.getOAuthJwtSecret() != null);

    return profile;
  }

  @GET
  @Path("callback")
  public Response callback(@QueryParam("code") String code) {
    var token = retrieveAccessToken(code);

    var user = token.decode(this.engine.getOAuthJwtSecret(), this.engine.getoAuthJwtIssuer());

    if (user == null) {
      // redirect back to the beginning
      return buildRedirect();
    }

    LOGGER.info("Decoded access token as user {}", user.getUsername());

    // try to find the user
    var ref = PersistenceManager.getInstance().getById(User.class, user.getId());

    if (ref == null) {
      service.createUser(user);
    }

    // issue token for our API
    var payload = new LoginResponse();

    payload.setToken(service.createToken(user));

    // TODO: max age, etc.
    /* angular is particular about the hash! it needs to be included.
    we cannot use UriBuilder, since it will remove the hash */
    var uri = URI.create("/#?token=" + payload.getToken());

    return Response.temporaryRedirect(uri)
        .cookie(new NewCookie("authorization", payload.getToken()))
        .build();
  }

  private TokenResponse retrieveAccessToken(String code) {
    var tokenUrl = this.engine.getOAuthTokenUrl();

    LOGGER.info("Trying to retrieve access token from {}", tokenUrl);

    var uri =
        UriBuilder.fromUri(tokenUrl)
            .queryParam("grant_type", "authorization_code")
            .queryParam("code", code)
            .build();

    var client =
        ClientBuilder.newClient()
            .register(
                (ClientRequestFilter)
                    requestContext -> {
                      var headers = requestContext.getHeaders();
                      headers.add(
                          "Authorization",
                          "Basic "
                              + Base64.getEncoder()
                                  .encodeToString(
                                      (this.engine.getOAuthClientId()
                                              + ":"
                                              + this.engine.getOAuthClientSecret())
                                          .getBytes()));
                    })
            .register(
                (ClientResponseFilter)
                    (requestContext, responseContext) -> {
                      // stupid workaround because some oauth servers incorrectly sends two
                      // Content-Type
                      // headers! fix it!
                      var contentType = responseContext.getHeaders().getFirst("Content-Type");
                      responseContext.getHeaders().putSingle("Content-Type", contentType);
                    });

    return client.target(uri).request().post(null, TokenResponse.class);
  }

  private Response buildRedirect() {
    var nonce = 25;

    var uri =
        UriBuilder.fromUri(this.engine.getOAuthAuthUrl())
            .queryParam("redirect_uri", this.engine.getBaseUrl() + "/oauth2/callback")
            .queryParam("client_id", this.engine.getOAuthClientId())
            .queryParam("response_type", "code")
            .queryParam("scope", "openid email full_name")
            .queryParam("nonce", nonce)
            .build();

    return Response.temporaryRedirect(uri).build();
  }

  @GET
  @Path("login")
  public Response login() {
    return buildRedirect();
  }
}