package io.cattle.platform.iaas.api.auth.integration.azure;

import io.cattle.platform.api.auth.Identity;
import io.cattle.platform.json.JsonMapper;
import io.github.ibuildthecloud.gdapi.context.ApiContext;
import io.github.ibuildthecloud.gdapi.exception.ClientVisibleException;
import io.github.ibuildthecloud.gdapi.util.ResponseCodes;
import io.cattle.platform.util.type.CollectionUtils;

import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;

import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;
import org.apache.http.entity.ContentType;
import org.apache.http.util.EntityUtils;

public class AzureRESTClient extends AzureConfigurable{

    @Inject
    private JsonMapper jsonMapper;
    
    @Inject
    private AzureTokenUtil azureTokenUtil;

    private static final Log logger = LogFactory.getLog(AzureRESTClient.class);

    private Identity getUserIdentity(String azureAccessToken) {
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        try {
            logger.debug("getUserIdentity for logged in user");
            HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USER, ""));
            if(hasTokenExpired(response)) {
                //refresh token
                refreshAccessToken();
                //retry the request
                azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USER, ""));
            }
            
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                noAzure(response);
            }
            
            Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
            return jsonToAzureAccountInfo(jsonData).toIdentity(AzureConstants.USER_SCOPE, true);
        } catch (IOException e) {
            logger.error("Failed to get Azure user account info.", e);
            throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.AZURE_CLIENT,
                    "Failed to get Azure user account info.", null);
        } catch(ClientVisibleException ex) {
            logger.error("Failed to get Azure user account info.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to get Azure user account info.", ex);
            throw new RuntimeException(ex);
        }
    }

    private List<Identity> getGroupIdentities(String azureAccessToken) {
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        try {
            logger.debug("getGroupIdentities for logged in user");
            HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUP, ""));
            if(hasTokenExpired(response)) {
                //refresh token
                refreshAccessToken();
                //retry the request
                azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUP, ""));
            }
            
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                noAzure(response);
            }

            Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
            List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);

            List<Identity> groupIdentities = new ArrayList<Identity>();

            if(searchResponseList != null && !searchResponseList.isEmpty()){
                for(AzureAccountInfo userOrGroup : searchResponseList){
                    groupIdentities.add(userOrGroup.toIdentity(AzureConstants.GROUP_SCOPE, false));
                }
            }

            return groupIdentities;
        } catch (IOException e) {
            logger.error("Failed to get Azure group memberships.", e);
            throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.AZURE_CLIENT,
                    "Failed to get Azure group memberships.", null);
        } catch(ClientVisibleException ex) {
            logger.error("Failed to get Azure group memberships.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to get Azure group memberships.", ex);
            throw new RuntimeException(ex);
        }
    }


    public String getAccessToken(String code) {
        if (!isConfigured()) {
            throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.CONFIG, "Azure Client and Tenant Id not configured", null);
        }
        
        logger.debug("getAccessToken from Azure");
    
        String[] split = code.split(":", 2);
        if (split.length != 2) {
            throw new ClientVisibleException(ResponseCodes.BAD_REQUEST, "MalformedCode");
        }
        String username = split[0];
        String domain = AzureConstants.AZURE_DOMAIN.get();
        //check if the username has a domain, if not then append the domain.
        if(domain != null && domain != "" && !username.endsWith("@"+domain)) {
            username = username + "@"+ domain;
        }
        
        //testuser1%40ZTAD.onmicrosoft.com&password=testpwd1%21";

        String accessToken, refreshToken;
        try{   
            StringBuilder body = new StringBuilder("scope=openid&grant_type=password&resource=https%3A%2F%2Fgraph.windows.net");
            body.append("&client_id=");
            body.append(URLEncoder.encode(AzureConstants.AZURE_CLIENT_ID.get(), "UTF-8"));
            body.append("&username=");
            body.append(URLEncoder.encode(username, "UTF-8"));
            body.append("&password=");
            body.append(URLEncoder.encode(split[1], "UTF-8")); 
            
            HttpResponse response = Request.Post(AzureConstants.AUTHORITY)
                    .addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_FORM_URL_ENCODED)
                    .bodyString(body.toString(), ContentType.APPLICATION_FORM_URLENCODED).execute().returnResponse();
            int statusCode = response.getStatusLine().getStatusCode();
            if(statusCode >= 300) {
                noAzure(response);
            }

            Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
            accessToken = ObjectUtils.toString(jsonData.get("access_token"));
            refreshToken = ObjectUtils.toString(jsonData.get("refresh_token"));

            //Also validate tenantID by issuing a search request for the user, if it errors we error out else login is successful
            String filter = "$filter=userPrincipalName%20eq%20'" + URLEncoder.encode(username, "UTF-8") + "'";
            HttpResponse newResponse = getFromAzure(accessToken, getURL(AzureClientEndpoints.USERS, "") + "&"+ filter);
            statusCode = newResponse.getStatusLine().getStatusCode();
            if(statusCode >= 300) {
                noAzure(newResponse);
            }

            ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_ACCESS_TOKEN, accessToken);
            ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_REFRESH_TOKEN, refreshToken);

        } catch(ClientVisibleException ex) {
            throw ex;
        } catch(Exception ex) {
            throw new RuntimeException(ex);
        }

        return accessToken;

    }
    
    
    public void refreshAccessToken() {
        if (!isConfigured()) {
            throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR, AzureConstants.CONFIG, "Azure Client and Tenant Id not configured", null);
        }
        
        logger.debug("Azure accessToken expired, refreshing the token");
        
        String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        
        String azureRefreshToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_REFRESH_TOKEN);
        if (StringUtils.isEmpty(azureRefreshToken)) {
            noRefreshToken();
        }
        try{
            //post to the microsoft azure authority
            HttpResponse response = Request.Post(AzureConstants.AUTHORITY)
                    .addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_FORM_URL_ENCODED)
                    .bodyString("grant_type=refresh_token&refresh_token="+azureRefreshToken, ContentType.APPLICATION_FORM_URLENCODED)
                    .execute().returnResponse();
            int statusCode = response.getStatusLine().getStatusCode();
            if(statusCode >= 300) {
                noAzure(response);
            }
            
            Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
            
            String newAccessToken = ObjectUtils.toString(jsonData.get("access_token"));
            String newRefreshToken = ObjectUtils.toString(jsonData.get("refresh_token"));
            ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_ACCESS_TOKEN, newAccessToken);
            ApiContext.getContext().getApiRequest().setAttribute(AzureConstants.AZURE_REFRESH_TOKEN, newRefreshToken);
            logger.debug("Storing the new Azure access token to the Account");
            azureTokenUtil.refreshAccessToken();
    
        } catch(ClientVisibleException ex) {
            logger.error("Failed to refreshAccessToken for Azure user.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to refreshAccessToken for Azure user.", ex);
            throw new RuntimeException(ex);
        }

    }

    public List<AzureAccountInfo> parseSearchResponseList(Map<String, Object> jsonData) {
        List<?> azureValueList = CollectionUtils.toList(jsonData.get("value"));
        List<AzureAccountInfo> azureUserOrGroupList = new ArrayList<AzureAccountInfo>();
        if (azureValueList != null && !azureValueList.isEmpty())
        {
            for(Object azureValue : azureValueList) {
                Map<String, Object> result = CollectionUtils.toMap(azureValue);
                String objectType = ObjectUtils.toString(result.get("objectType"));
                if(objectType != null && (objectType.equalsIgnoreCase("User") || objectType.equalsIgnoreCase("Group"))) {
                    AzureAccountInfo userOrGroupInfo = jsonToAzureAccountInfo(result);
                    azureUserOrGroupList.add(userOrGroupInfo);
                }
            }
        }
        return azureUserOrGroupList;
    }

    public AzureAccountInfo jsonToAzureAccountInfo(Map<String, Object> jsonData) {
        String objectId = ObjectUtils.toString(jsonData.get("objectId"));
        String objectType = ObjectUtils.toString(jsonData.get("objectType"));
        String userPrincipalName = ObjectUtils.toString(jsonData.get("userPrincipalName"));
        String accountName = ObjectUtils.toString(jsonData.get("mailNickname"));

        if("Group".equalsIgnoreCase(objectType)) {
            accountName = ObjectUtils.toString(jsonData.get("displayName"));
        }

        String name = ObjectUtils.toString(jsonData.get("displayName"));
        if (StringUtils.isBlank(name)) {
            name = accountName;
        }
        //String profilePicture = ObjectUtils.toString(jsonData.get(GithubConstants.PROFILE_PICTURE));
        return new AzureAccountInfo(objectId, accountName, null, userPrincipalName, name);
    }

    public boolean hasTokenExpired(HttpResponse response) throws IOException {
        int statusCode = response.getStatusLine().getStatusCode();
        if(statusCode == 401) {
            //if token expired, refresh token.
            Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
            Map<String, Object> azureError = CollectionUtils.toMap(jsonData.get("odata.error"));
            String azureCode = ObjectUtils.toString(azureError.get("code"));
            if("Authentication_ExpiredToken".equalsIgnoreCase(azureCode)) {
                return true;
            }
         }
        return false;
    }

    public HttpResponse getFromAzure(String azureAccessToken, String url) throws IOException {

        logger.trace("URL for Azure API call: "+ url);
        HttpResponse response = Request.Get(url).addHeader(AzureConstants.AUTHORIZATION, "Bearer " +
                "" + azureAccessToken).addHeader(AzureConstants.ACCEPT, AzureConstants.APPLICATION_JSON).execute().returnResponse();
        logger.debug("Response from Azure API: "+ response.getStatusLine());
        logger.trace("Complete Response from Azure: "+ EntityUtils.toString(response.getEntity()));

        return response;
    }    

    private void noAccessToken() {
        throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
                "AzureAccessToken", "No Azure Access token", null);
    }
    
    private void noRefreshToken() {
        throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
                "AzureRefreshToken", "No Azure Refresh token", null);
    }
    
    public void noAzure(HttpResponse response) {
        int statusCode = response.getStatusLine().getStatusCode();

        try {
            Map<String, Object> jsonData = jsonMapper.readValue(response.getEntity().getContent());
            String azureError = (String)jsonData.get("error");
            String azureErrorDesc = (String)jsonData.get("error_description");
            if("invalid_grant".equalsIgnoreCase(azureError) || "unauthorized_client".equalsIgnoreCase(azureError)) {
                throw new ClientVisibleException(ResponseCodes.UNAUTHORIZED);
            }
            logger.error("Error received from Azure: " + azureError);
            logger.error("Error Description received from Azure: " + azureErrorDesc);
            throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, AzureConstants.AZURE_ERROR,
                    "Error from Azure: " + Integer.toString(statusCode), "Details: " +azureError + ", Description: "+azureErrorDesc);
        } catch(ClientVisibleException ex) {
            logger.error("Got error from Azure.", ex);
            throw ex;
        } catch(Exception ex) {
            throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, AzureConstants.AZURE_ERROR,
                    "Error Response from Azure", "Status code from Azure: " + Integer.toString(statusCode));
        } 
    }

    public String getURL(AzureClientEndpoints val, String objectId) {
        String apiEndpoint = AzureConstants.GRAPH_API_ENDPOINT;
        String tenantId = AzureConstants.AZURE_TENANT_ID.get();

        String toReturn;
        switch (val) {
            case USERS:
                toReturn = apiEndpoint + tenantId + "/users/" + objectId;
                break;
            case GROUPS:
                toReturn = apiEndpoint + tenantId  + "/groups/" + objectId;
                break;
            case USER:
                toReturn = apiEndpoint + "me";
                break;
            case GROUP:
                toReturn = apiEndpoint + "me/memberOf";
                break;
            default:
                throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
                        "AzureClient", "Attempted to get invalid Api endpoint.", null);
        }
        return toReturn + AzureConstants.GRAPH_API_VERSION;
    }    
    

    public AzureAccountInfo getAzureUserByName(String username) {
        String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        logger.debug("getAzureUserByName: "+ username);
        try {
            if (StringUtils.isEmpty(username)) {
                throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
                        "getAzureUser", "No azure username specified.", null);
            }
            String domain = AzureConstants.AZURE_DOMAIN.get();
            //check if the username has a domain, if not then append the domain.
            if(domain != null && domain != "" && !username.endsWith("@"+domain)) {
                username = username + "@"+ domain;
            }
            
            username = URLEncoder.encode(username, "UTF-8");
            
            String filter = "$filter=userPrincipalName%20eq%20'" + username + "'";
            //String filter = "$filter=startswith(userPrincipalName,'" + username+ "')";

            
            HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, "") + "&"+ filter);
            if(hasTokenExpired(response)) {
                //refresh token
                refreshAccessToken();
                //retry the request
                azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, "") + "&"+ filter);
            }
            
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                noAzure(response);
            }

            
            Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
            List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);
            if(searchResponseList != null && !searchResponseList.isEmpty()) {
                return searchResponseList.get(0);
            }
            return null;
        } catch (IOException e) {
            logger.error(e);
            throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve User by name from Azure", null);
        } catch(ClientVisibleException ex) {
            logger.error("Failed to get Azure user account info by name.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to get Azure user account info by name.", ex);
            throw new RuntimeException(ex);
        }

    }


    public AzureAccountInfo getAzureGroupByName(String org) {
        String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        logger.debug("getAzureGroupByName: "+ org);
        try {
            if (StringUtils.isEmpty(org)) {
                throw new ClientVisibleException(ResponseCodes.INTERNAL_SERVER_ERROR,
                        "noAzureGroupName", "No group name specified when retrieving from Azure.", null);
            }
            org = URLEncoder.encode(org, "UTF-8");
            String filter = "$filter=displayName%20eq%20'" + org + "'";
            //String filter = "$filter=startswith(displayName,'" + org+ "')";
            
            HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, "") + "&"+ filter);
            if(hasTokenExpired(response)) {
                //refresh token
                refreshAccessToken();
                //retry the request
                azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, "") + "&"+ filter);
            }
            
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                noAzure(response);
            }  

            Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent
                    (), Map.class));
            List<AzureAccountInfo> searchResponseList = parseSearchResponseList(jsonData);
            if(searchResponseList != null && !searchResponseList.isEmpty()) {
                return searchResponseList.get(0);
            }
            return null;
        } catch (IOException e) {
            logger.error(e);
            throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve Group by name from Azure", null);
        } catch(ClientVisibleException ex) {
            logger.error("Failed to get Azure group info by name.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to get Azure group info by name.", ex);
            throw new RuntimeException(ex);
        }
    }
    
    
     public AzureAccountInfo getUserById(String id) {
        String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
        if (StringUtils.isEmpty(azureAccessToken)) {
            noAccessToken();
        }
        logger.debug("getUserById: "+ id);
        try {
            HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, id));
            if(hasTokenExpired(response)) {
                //refresh token
                refreshAccessToken();
                //retry the request
                azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.USERS, id));
            }
            
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode >= 300) {
                noAzure(response);
            }  

            Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
            return jsonToAzureAccountInfo(jsonData);
        } catch (IOException e) {
            throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve User by Id from Azure", null);
        } catch(ClientVisibleException ex) {
            logger.error("Failed to get Azure user account info by Id.", ex);
            throw ex;
        } catch(Exception ex) {
            logger.error("Failed to get Azure user account info by Id.", ex);
            throw new RuntimeException(ex);
        }
    }
     
     public AzureAccountInfo getGroupById(String id) {
         String azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
         if (StringUtils.isEmpty(azureAccessToken)) {
             noAccessToken();
         }
         logger.debug("getGroupById: "+ id);
         try {
             HttpResponse response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, id));
             if(hasTokenExpired(response)) {
                 //refresh token
                 refreshAccessToken();
                 //retry the request
                 azureAccessToken = (String) ApiContext.getContext().getApiRequest().getAttribute(AzureConstants.AZURE_ACCESS_TOKEN);
                 response = getFromAzure(azureAccessToken, getURL(AzureClientEndpoints.GROUPS, id));
             }
             
             int statusCode = response.getStatusLine().getStatusCode();
             if (statusCode >= 300) {
                 noAzure(response);
             } 
             Map<String, Object> jsonData = CollectionUtils.toMap(jsonMapper.readValue(response.getEntity().getContent(), Map.class));
             return jsonToAzureAccountInfo(jsonData);
         } catch (IOException e) {
             throw new ClientVisibleException(ResponseCodes.SERVICE_UNAVAILABLE, "AzureUnavailable", "Could not retrieve Group by Id from Azure", null);
         } catch(ClientVisibleException ex) {
             logger.error("Failed to get Azure group info by id.", ex);
             throw ex;
         } catch(Exception ex) {
             logger.error("Failed to get Azure group info by id.", ex);
             throw new RuntimeException(ex);
         }
     }

 
    public Set<Identity> getIdentities(String accessToken) {
        Set<Identity> identities = new HashSet<>();
        identities.add(getUserIdentity(accessToken));
        identities.addAll(getGroupIdentities(accessToken));

        return identities;
    }

    @Override
    public String getName() {
        return AzureConstants.AZURE_CLIENT;
    }
    
}
