package com.alienvault.otx.connect;

import com.alienvault.otx.connect.internal.HTTPConfig;
import com.alienvault.otx.model.*;
import com.alienvault.otx.model.events.Event;
import com.alienvault.otx.model.events.EventPage;
import com.alienvault.otx.model.indicator.Indicator;
import com.alienvault.otx.model.indicator.IndicatorPage;
import com.alienvault.otx.model.pulse.Pulse;
import com.alienvault.otx.model.pulse.PulsePage;
import com.alienvault.otx.model.user.User;
import com.alienvault.otx.model.user.UserActions;
import com.alienvault.otx.model.user.UserPage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriUtils;

import java.io.UnsupportedEncodingException;
import java.net.*;
import java.util.*;

/**
 * OTXConnextion takes care of the requests made to
 * the OTX service.  The utility methods provided give you
 * the mechanisms necessary to get the Pulses that have
 * been subscribed to in the OTX API.
 * <p/>
 * Construct this class passing in your API key found in the
 * 'Settings' page of the web interface.
 */
public class OTXConnection {

    private RestTemplate restTemplate;
    private RetryTemplate retryTemplate;

    private String otxHost = "otx.alienvault.com";
    private String otxScheme = "https";
    private Integer otxPort = null;
    private static DateTimeFormatter fmt = ISODateTimeFormat.dateTimeNoMillis();
    private Log log = LogFactory.getLog(OTXConnection.class);

    /**
     * Construct the OTX Connection providing full connection details.
     *
     * @param apiKey - API key for your OTX Account
     * @param host   - host of the OTX server (otx.alienvault.com by default)
     * @param scheme - scheme to use for the connection to the server (https by default)
     * @param port   - port for the connection to the server (443 by default)
     */
    public OTXConnection(String apiKey, String host, String scheme, Integer port) {
        if (host != null)
            this.otxHost = host;
        if (scheme != null)
            this.otxScheme = scheme;
        if (port != null)
            otxPort = port;
        configureRestTemplate(apiKey);
    }

    /**
     * Construct the OTX Connection providing full connection details.
     *
     * @param apiKey    - API key for your OTX Account
     * @param otxHost   - host of the OTX server (otx.alienvault.com by default)
     * @param otxScheme - scheme to use for the connection to the server (https by default)
     */
    public OTXConnection(String apiKey, String otxHost, String otxScheme) {
        this.otxHost = otxHost;
        this.otxScheme = otxScheme;
        configureRestTemplate(apiKey);
    }

    /**
     * Construct the OTX Connection providing full connection details.
     *
     * @param apiKey - API key for your OTX Account
     */
    public OTXConnection(String apiKey) {
        configureRestTemplate(apiKey);
    }

    /**
     * Construct the OTX Connection providing full connection details.
     *
     * @param apiKey  - API key for your OTX Account
     * @param otxHost - host of the OTX server (otx.alienvault.com by default)
     */
    public OTXConnection(String apiKey, String otxHost) {
        this.otxHost = otxHost;
        configureRestTemplate(apiKey);
    }

    /**
     * Internal API to configure RestTemplate
     *
     * @param apiKey - API key to configure authorization header
     */
    private void configureRestTemplate(String apiKey) {
        ClientHttpRequestFactory requestFactory = HTTPConfig.createRequestFactory(apiKey);
        restTemplate = new RestTemplate(requestFactory);
        retryTemplate = new RetryTemplate();
        retryTemplate.setBackOffPolicy(new ExponentialBackOffPolicy());
    }

    /**
     * Utility method to access all Pulses subscribed to in the web interface.
     *
     * @return All of the Pulses
     * @throws URISyntaxException
     * @throws MalformedURLException
     */
    public List<Pulse> getAllPulses() throws URISyntaxException, MalformedURLException {
        return getPulses(null);
    }

    /**
     * Access all indicators for a given pulse id
     *
     * @param pulseId id of pulse
     * @return List<Indicators> indicators contained within pulse
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<Indicator> getAllIndicatorsForPulse(String pulseId) throws MalformedURLException, URISyntaxException {
        return (List<Indicator>) getPagedResults(Collections.singletonMap(OTXEndpointParameters.ID, pulseId), new IndicatorPage(), OTXEndpoints.INDICATORS_FOR_PULSE);
    }

    /**
     * Access all pulses related to a given pulse id
     *
     * @param pulseId id of pulse
     * @return List<Pulse> pulses related to the given pulse id
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<Pulse> getAllRelatedPulses(String pulseId) throws MalformedURLException, URISyntaxException {
        return (List<Pulse>) getPagedResults(Collections.singletonMap(OTXEndpointParameters.ID, pulseId), new PulsePage(), OTXEndpoints.RELATED_TO_PULSE);
    }

    /**
     * Utility method to access all Pulses modified since the date passed to the API.
     *
     * @param lastUpdated - date to cut off the list of pulses
     * @return All of the Pulses modified since the lastUpdated date
     * @throws URISyntaxException
     * @throws MalformedURLException
     */
    public List<Pulse> getPulsesSinceDate(DateTime lastUpdated) throws URISyntaxException, MalformedURLException {
        return getPulses(Collections.singletonMap(OTXEndpointParameters.MODIFIED_SINCE, fmt.print(lastUpdated)));
    }

    /**
     * @param pulseId
     * @return
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public Pulse getPulseDetails(String pulseId) throws MalformedURLException, URISyntaxException {
        return executeGetRequest(OTXEndpoints.PULSE_DETAILS, Collections.singletonMap(OTXEndpointParameters.ID, pulseId), Pulse.class);

    }

    /**
     * @param searchQuery
     * @return
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<Pulse> searchForPulses(String searchQuery) throws MalformedURLException, URISyntaxException {
        return (List<Pulse>) getPagedResults(Collections.singletonMap(OTXEndpointParameters.QUERY, searchQuery), new PulsePage(), OTXEndpoints.SEARCH_PULSES);
    }

    /**
     * @param searchQuery
     * @return
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<User> searchForUsers(String searchQuery) throws MalformedURLException, URISyntaxException {
        return (List<User>) getPagedResults(Collections.singletonMap(OTXEndpointParameters.QUERY, searchQuery), new UserPage(), OTXEndpoints.SEARCH_USERS);
    }

    /**
     * Get the User object representing the authenticated user
     * @return User
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public User getMyDetails() throws MalformedURLException, URISyntaxException {
        return executeGetRequest(OTXEndpoints.USERS_ME, null, User.class);
    }

    /**
     * Create a new pulse.
     *
     * @param newPulse - object representing the pulse to create
     * @return the newly created Pulse object with ID and created meta-data
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public Pulse createPulse(Pulse newPulse) throws MalformedURLException, URISyntaxException {
        return (Pulse) executePostRequest(OTXEndpoints.PULSE_CREATE, newPulse, Pulse.class).getBody();
    }

    /**
     * Allows the ability to follow, subscribe, unfollow, and unsubscribe
     *
     * @param username - NOTE this is case-sensitive
     * @param action - the action to perform
     * @return - the response with a value for the key 'status'
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public Map performUserAction(String username, UserActions action) throws MalformedURLException, URISyntaxException {
        Map<OTXEndpointParameters, String> parameterMap = new HashMap<>();
        parameterMap.put(OTXEndpointParameters.USERNAME, username);
        parameterMap.put(OTXEndpointParameters.ACTION, action.getAction());
        return restTemplate.postForEntity(buildURI(OTXEndpoints.USERS_ACTION, parameterMap), null, Map.class).getBody();
    }

    /**
     * Get all events related to the authenticated account
     * @return List<Event>
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<Event> getAllEvents() throws MalformedURLException, URISyntaxException {
        return (List<Event>) getPagedResults(null, new EventPage(), OTXEndpoints.EVENTS);
    }

    /**
     * Get all events related to the authenticated account since the passed timeframe
     * @param cutoffDate only events after this date will be returned
     * @return List<Event>
     * @throws MalformedURLException
     * @throws URISyntaxException
     */
    public List<Event> getEventsSince(DateTime cutoffDate) throws MalformedURLException, URISyntaxException {
        return (List<Event>) getPagedResults(Collections.singletonMap(OTXEndpointParameters.MODIFIED_SINCE, fmt.print(cutoffDate)), new EventPage(), OTXEndpoints.EVENTS);
    }

    private List<Pulse> getPulses(Map<OTXEndpointParameters, ?> endpointParametersMap) throws URISyntaxException, MalformedURLException {

        return (List<Pulse>) getPagedResults(endpointParametersMap, new PulsePage(), OTXEndpoints.SUBSCRIBED);
    }

    private List<?> getPagedResults(Map<OTXEndpointParameters, ?> endpointParametersMap, Page<?> page, OTXEndpoints endpoint) throws MalformedURLException, URISyntaxException {
        if (endpointParametersMap == null || !endpointParametersMap.containsKey(OTXEndpointParameters.LIMIT)){
               Map newParams;
               if (endpointParametersMap != null) {
                   newParams = new HashMap(endpointParametersMap);
               }else{
                   newParams = new HashMap();
               }
               newParams.put(OTXEndpointParameters.LIMIT, 20);
               endpointParametersMap = newParams;
           }
        List pulseList = new ArrayList<>();
        Page<?> firstPage = executeGetRequest(endpoint, endpointParametersMap, page.getClass());
        pulseList.addAll(firstPage.getResults());
        while (firstPage.getNext() != null) {
            String rawQuery = firstPage.getNext().getRawQuery();
            String nextPage = getParameterFromQueryString(rawQuery, OTXEndpointParameters.PAGE);
            // cskellie - passed in new parameter with page count to fetch next page of results.
            Map parametersMap = new HashMap<>();
            if (endpointParametersMap!=null) {
                parametersMap.putAll(endpointParametersMap);
            }
            parametersMap.putAll(Collections.singletonMap(OTXEndpointParameters.PAGE, nextPage));
            firstPage = executeGetRequest(endpoint, parametersMap, page.getClass());
            pulseList.addAll(firstPage.getResults());
        }
        return pulseList;

    }

    private String getParameterFromQueryString(String rawQuery, OTXEndpointParameters page) {
        String[] pairs = rawQuery.split("&");

        Map<String, String> query_pairs = new HashMap<String, String>();
        for (String pair : pairs) {
            int idx = pair.indexOf("=");
            try {
                query_pairs.put(URLDecoder.decode(pair.substring(0, idx), "UTF-8"), URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                log.error("Unexpected issue with encoding", e);
            }
        }
        return query_pairs.get(page.getParameterName());

    }

    private <T> T executeGetRequest(final OTXEndpoints subscribed, final Map<OTXEndpointParameters, ?> endpointParametersMap, final Class<T> classType) throws MalformedURLException, URISyntaxException {

        final URI url = buildURI(subscribed, endpointParametersMap);

        return  retryTemplate.execute(new RetryCallback<T, RestClientException>() {

            public T doWithRetry(RetryContext context) {
                // Do stuff that might fail, e.g. webservice operation
                return restTemplate.getForObject(url, classType);
            }

        });
    }

    private URI buildURI(OTXEndpoints endpoint, Map<OTXEndpointParameters, ?> endpointParametersMap) throws URISyntaxException, MalformedURLException {

        String endpointString = endpoint.getEndpoint();
        if (endpointParametersMap != null) {
            boolean first = true;
            for (Map.Entry<OTXEndpointParameters, ?> otxEndpointParametersEntry : endpointParametersMap.entrySet()) {
                if (otxEndpointParametersEntry.getKey().isRestVariable()) {
                    endpointString = endpointString.replace("{" + otxEndpointParametersEntry.getKey().getParameterName() + "}", otxEndpointParametersEntry.getValue().toString());
                } else {
                    if (first) {
                        endpointString = endpointString + "?";
                        first = false;
                    }
                    try {
                        String parameterName = otxEndpointParametersEntry.getKey().getParameterName();
                        String value = UriUtils.encodeQueryParam(otxEndpointParametersEntry.getValue().toString(), "UTF-8");
                        endpointString = endpointString + String.format("%s=%s&", parameterName, value);
                    } catch (UnsupportedEncodingException e) {
                        log.error("Unpossible");
                    }
                }
            }
        }
        if (otxPort != null) {
            return new URL(otxScheme, otxHost, otxPort, endpointString).toURI();
        } else {
            return new URL(otxScheme, otxHost, endpointString).toURI();
        }
    }


    private ResponseEntity<?> executePostRequest(OTXEndpoints endpoint, Object toPost, Class<Pulse> responseType) throws MalformedURLException, URISyntaxException {
        return restTemplate.postForEntity(buildURI(endpoint, null), toPost, responseType);
    }

}