/*
 * Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
 * 
 * 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.
 */
package monasca.common.middleware;

import static monasca.common.middleware.AuthConstants.TOKEN;
import static monasca.common.middleware.AuthConstants.AUTH_SUBJECT_TOKEN;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.URI;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;

public class HttpAuthClient implements AuthClient {

  private static final Logger logger = LoggerFactory.getLogger(HttpAuthClient.class);

  private static final int DELTA_TIME_IN_SEC = 30;
  private static final String APPLICATION_JSON = "application/json";
  private static SimpleDateFormat expiryFormat;

  static {
    expiryFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    expiryFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
  }

  private final Config appConfig = Config.getInstance();

  private HttpClient client;
  private String adminToken;
  private String adminTokenExpiry;
  private URI uri;

  public HttpAuthClient(HttpClient client, URI uri) {
    this.client = client;
    this.uri = uri;
  }

  @Override
  public String validateTokenForServiceEndpointV3(String token) throws ClientProtocolException {
    String newUri = uri.toString() + "/v3/auth/tokens/";
    Header[] header = new Header[]{new BasicHeader(AUTH_SUBJECT_TOKEN, token)};
    return verifyUUIDToken(token, newUri, header);
  }

  private String verifyUUIDToken(String token, String newUri,
                                 Header[] headers)
      throws ClientProtocolException {

    HttpGet httpGet = new HttpGet(newUri);

    try {

      httpGet.setHeader("Accept", APPLICATION_JSON);
      httpGet.setHeader("Content-Type", APPLICATION_JSON);

      if (headers != null) {
        for (Header header : headers) {
          httpGet.setHeader(header);
        }
      }

      HttpResponse response = sendGet(httpGet);

      HttpEntity entity = response.getEntity();
      int code = response.getStatusLine().getStatusCode();

      InputStream instream;
      try {
        if (code == 404) {
          instream = entity.getContent();
          instream.close();
          throw new AuthException("Authorization failed for user token: " + token);
        }

        if (code != 200) {
          adminToken = null;
          instream = entity.getContent();
          instream.close();
          String reasonPhrase = response.getStatusLine().getReasonPhrase();

          throw new AuthException("Failed to validate via HTTP " + code + " " + reasonPhrase);
        }
      } catch (IOException e) {
        throw new ClientProtocolException("IO Exception: problem closing stream ", e);
      }

      return parseResponse(response);

    } finally {

      httpGet.releaseConnection();

    }
  }

  private HttpResponse sendPost(HttpPost httpPost)
      throws ClientProtocolException {
    HttpResponse response;

    try {
      response = client.execute(httpPost);
      int code = response.getStatusLine().getStatusCode();
      if (!(code == 201 || code == 200 || code == 203)) {
        adminToken = null;
        throw new AdminAuthException(
            "Failed to authenticate admin credentials " + code
            + response.getStatusLine().getReasonPhrase());
      }
    } catch (IOException e) {
      final String message;
      if ((e.getMessage() == null) && (e.getCause() != null)) {
        message = e.getCause().getMessage();
      } else {
        message = e.getMessage();
      }
      logger.error("Failure authenticating adminUser: {}", message);
      httpPost.abort();
      throw new AdminAuthException(
          "Failure authenticating adminUser :" + message, e);
    }
    return response;
  }

  private HttpResponse sendGet(HttpGet httpGet)
      throws ClientProtocolException {
    HttpResponse response;

    if (appConfig.getAdminAuthMethod().equalsIgnoreCase(Config.TOKEN)) {
      httpGet.setHeader(new BasicHeader(TOKEN, appConfig.getAdminToken()));
    } else {
      httpGet.setHeader(new BasicHeader(TOKEN, getAdminToken()));
    }

    try {

      response = client.execute(httpGet);

    } catch (ConnectException c) {
      httpGet.abort();
      throw new ServiceUnavailableException(c.getMessage());
    } catch (IOException e) {
      httpGet.abort();

      throw new ClientProtocolException(
          "IO Exception during GET request ", e);
    }
    return response;
  }

  private String parseResponse(HttpResponse response) {
    StringBuffer json = new StringBuffer();
    HttpEntity entity = response.getEntity();
    if (entity != null) {
      InputStream instream;
      try {
        instream = entity.getContent();

        BufferedReader reader = new BufferedReader(
            new InputStreamReader(instream));
        String line = reader.readLine();
        while (line != null) {
          json.append(line);
          line = reader.readLine();
        }
        instream.close();
        reader.close();
      } catch (Exception e) {
        throw new AuthException("Failed to parse Http Response ", e);
      }
    }

    return json.toString();
  }

  private String getAdminToken() throws ClientProtocolException {

    if (adminTokenExpiry != null) {
      if (isExpired(adminTokenExpiry)) {
        adminToken = null;
      }
    }

    if (adminToken == null) {

      String authUri = uri + "/v3/auth/tokens";
      HttpPost httpPost = new HttpPost(authUri);

      try {

        StringEntity params = getUnscopedV3AdminTokenRequest();
        httpPost.setHeader("Accept", APPLICATION_JSON);
        httpPost.setHeader("Content-Type", APPLICATION_JSON);
        httpPost.setEntity(params);
        HttpResponse response = sendPost(httpPost);
        adminToken = response.getFirstHeader(AUTH_SUBJECT_TOKEN).getValue();
        String json = parseResponse(response);
        JsonObject
            token =
            new JsonParser().parse(json).getAsJsonObject().get("token").getAsJsonObject();
        adminTokenExpiry = token.get("expires_at").getAsString();

      } finally {

        httpPost.releaseConnection();

      }
    }
    return adminToken;
  }

  private String buildAuth(final String userName, final String password,
                           final String projectId, final String projectName) {
    final JsonObject domain = new JsonObject();
    domain.addProperty("id", "default");

    final JsonObject user = new JsonObject();
    user.addProperty("name", userName);
    user.addProperty("password", password);
    user.add("domain", domain);

    final JsonObject passwordHolder = new JsonObject();
    passwordHolder.add("user", user);

    final JsonArray methods = new JsonArray();
    methods.add(new JsonPrimitive("password"));

    final JsonObject identity = new JsonObject();
    identity.add("methods", methods);
    identity.add("password", passwordHolder);

    boolean scopeDefined = false;
    final JsonObject project = new JsonObject();
    // If project id is available, it is preferred
    if (!projectId.isEmpty()) {
      project.addProperty("id", projectId);
      scopeDefined = true;

    } else if (!projectName.isEmpty()) {
      project.add("domain", domain);
      project.addProperty("name", projectName);
      scopeDefined = true;
    }

    final JsonObject auth = new JsonObject();
    auth.add("identity", identity);
    if (scopeDefined) {
      final JsonObject scope = new JsonObject();
      scope.add("project", project);
      auth.add("scope", scope);
    }

    final JsonObject outer = new JsonObject();
    outer.add("auth", auth);

    return outer.toString();
  }

  private StringEntity getUnscopedV3AdminTokenRequest() {
    final String body;
    if (appConfig.getAdminAuthMethod().equalsIgnoreCase(Config.PASSWORD)) {
      body = buildAuth(appConfig.getAdminUser(), appConfig.getAdminPassword(),
                       appConfig.getAdminProjectId(), appConfig.getAdminProjectName());
    } else {
      String
          msg =
          String.format("Admin auth method %s not supported", appConfig.getAdminAuthMethod());
      throw new AdminAuthException(msg);
    }
    try {
      return new StringEntity(body);
    } catch (UnsupportedEncodingException e) {
      throw new AdminAuthException("Invalid V3 authentication request " + e);
    }
  }

  private boolean isExpired(String expires) {
    Date tokenExpiryDate;
    try {
      // The date looks like: 2014-11-13T02:34:59.953729Z
      // SimpleDateFormat can't handle the microseconds so take them off
      final String tmp = expires.replaceAll("\\.[\\d]+Z", "Z");
      tokenExpiryDate = expiryFormat.parse(tmp);
    } catch (ParseException e) {
      logger.warn("Failure parsing Admin Token expiration date: {}", e.getMessage());
      return true;
    }
    Date current = new Date();
    return tokenExpiryDate.getTime() < (current.getTime() + DELTA_TIME_IN_SEC * 1000);
  }

  public void reset() {
  }

}
