/*
 * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0
 * (the "License"). You may not use this work except in compliance with the License, which is
 * available at www.apache.org/licenses/LICENSE-2.0
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied, as more fully set forth in the License.
 *
 * See the NOTICE file distributed with this work for information regarding copyright ownership.
 */

package alluxio.underfs.swift;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.javaswift.joss.client.factory.AccountConfig;
import org.javaswift.joss.client.factory.AuthenticationMethod.AccessProvider;
import org.javaswift.joss.model.Access;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;

/**
 * Custom {@link AccessProvider} for Keystone V3 authentication.
 */
public class KeystoneV3AccessProvider implements AccessProvider {
  private static final Logger LOG = LoggerFactory.getLogger(KeystoneV3AccessProvider.class);

  private static final String AUTH_METHOD = "password";
  private static final int RESPONSE_OK = 201;

  private AccountConfig mAccountConfig;

  /**
   * Create a new instance of {@link KeystoneV3AccessProvider}.
   *
   * @param accountConfig account credentials
   */
  public KeystoneV3AccessProvider(AccountConfig accountConfig) {
    mAccountConfig = accountConfig;
  }

  @Override
  public Access authenticate() {

    try {
      String requestBody;
      try {
        // Construct request body
        KeystoneV3Request request =
            new KeystoneV3Request(new Auth(
                new Identity(Arrays.asList(AUTH_METHOD),
                    new Password(
                        new User(mAccountConfig.getUsername(), mAccountConfig.getPassword()))),
                new Scope(new Project(mAccountConfig.getTenantName()))));
        requestBody = new ObjectMapper().writeValueAsString(request);
      } catch (JsonProcessingException e) {
        LOG.error("Error processing JSON request: {}", e.getMessage());
        return null;
      }

      try (CloseableHttpClient client = HttpClients.createDefault()) {
        // Send request
        HttpPost post = new HttpPost(mAccountConfig.getAuthUrl());
        post.addHeader("Accept", "application/json");
        post.addHeader("Content-Type", "application/json");
        post.setEntity(new ByteArrayEntity(requestBody.toString().getBytes()));
        try (CloseableHttpResponse httpResponse = client.execute(post)) {
          // Parse response
          int responseCode = httpResponse.getStatusLine().getStatusCode();
          if (responseCode != RESPONSE_OK) {
            LOG.error("Error with response code {} ", responseCode);
            return null;
          }
          String token = httpResponse.getFirstHeader("X-Subject-Token").getValue();

          // Parse response body
          try (BufferedReader bufReader =
              new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent()))) {
            String responseBody = bufReader.readLine();
            KeystoneV3Response response;
            try {
              response =
                  new ObjectMapper().readerFor(KeystoneV3Response.class).readValue(responseBody);
              // Find endpoints
              String internalURL = null;
              String publicURL = null;
              for (Catalog catalog : response.mToken.mCatalog) {
                if (catalog.mName.equals("swift") && catalog.mType.equals("object-store")) {
                  for (Endpoint endpoint : catalog.mEndpoints) {
                    if (endpoint.mRegion.equals(mAccountConfig.getPreferredRegion())) {
                      if (endpoint.mInterface.equals("public")) {
                        publicURL = endpoint.mUrl;
                      } else if (endpoint.mInterface.equals("internal")) {
                        internalURL = endpoint.mUrl;
                      }
                    }
                  }
                }
              }
              // Construct access object
              KeystoneV3Access access = new KeystoneV3Access(internalURL,
                  mAccountConfig.getPreferredRegion(), publicURL, token);
              return access;
            } catch (JsonProcessingException e) {
              LOG.error("Error processing JSON response: {}", e.getMessage());
              return null;
            }
          }
        }
      }
    } catch (IOException e) {
      // Unable to authenticate
      LOG.error("Exception authenticating using KeystoneV3 {}", e.getMessage());
      return null;
    }
  }

  /** Classes for creating authentication JSON request. */
  @JsonPropertyOrder({"auth"})
  private class KeystoneV3Request {
    @JsonProperty("auth")
    public Auth mAuth;

    public KeystoneV3Request(Auth auth) {
      mAuth = auth;
    }
  }

  @JsonPropertyOrder({"identity", "scope"})
  private class Auth {
    @JsonProperty("identity")
    public Identity mIdentity;
    @JsonProperty("scope")
    public Scope mScope;

    public Auth(Identity identity, Scope scope) {
      mIdentity = identity;
      mScope = scope;
    }
  }

  @JsonPropertyOrder({"methods", "password"})
  private class Identity {
    @JsonProperty("methods")
    public List<String> mMethods = null;
    @JsonProperty("password")
    public Password mPassword;

    public Identity(List<String> methods, Password password) {
      mMethods = methods;
      mPassword = password;
    }
  }

  @JsonPropertyOrder({"user"})
  private class Password {
    @JsonProperty("user")
    public User mUser;

    public Password(User user) {
      mUser = user;
    }
  }

  @JsonPropertyOrder({"id"})
  private class Project {
    @JsonProperty("id")
    public String mId;

    public Project(String id) {
      mId = id;
    }
  }

  @JsonPropertyOrder({"project"})
  private class Scope {
    @JsonProperty("project")
    public Project mProject;

    public Scope(Project project) {
      mProject = project;
    }
  }

  @JsonPropertyOrder({"id", "password"})
  private class User {
    @JsonProperty("id")
    public String mId;
    @JsonProperty("password")
    public String mPassword;

    public User(String id, String password) {
      mId = id;
      mPassword = password;
    }
  }

  /** Classes for parsing authentication JSON response. */
  @JsonIgnoreProperties(ignoreUnknown = true)
  private static class KeystoneV3Response {
    public Token mToken;

    @JsonCreator
    public KeystoneV3Response(@JsonProperty("token") Token token) {
      mToken = token;
    }
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  private static class Token {
    public List<Catalog> mCatalog = null;

    @JsonCreator
    public Token(@JsonProperty("catalog") List<Catalog> catalog) {
      mCatalog = catalog;
    }
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  private static class Catalog {
    public List<Endpoint> mEndpoints;
    public String mType;
    public String mName;

    @JsonCreator
    public Catalog(@JsonProperty("endpoints") List<Endpoint> endpoints,
        @JsonProperty("type") String type, @JsonProperty("name") String name) {
      mEndpoints = endpoints;
      mType = type;
      mName = name;
    }
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  private static class Endpoint {
    public String mUrl;
    public String mRegion;
    public String mInterface;

    @JsonCreator
    public Endpoint(@JsonProperty("url") String url, @JsonProperty("region") String region,
        @JsonProperty("interface") String inter) {
      mUrl = url;
      mRegion = region;
      mInterface = inter;
    }
  }
}
