package com.patreon;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.github.jasminb.jsonapi.*;
import com.patreon.resources.Campaign;
import com.patreon.resources.Pledge;
import com.patreon.resources.RequestUtil;
import com.patreon.resources.User;
import com.patreon.resources.shared.BaseResource;
import com.patreon.resources.shared.Field;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

public class PatreonAPI {
  /**
   * The base URI for requests to the patreon API. This may be overridden (e.g. for testing) by passing
   * -Dpatreon.rest.uri="https://my.other.server.com" as jvm arguments
   */
  public static final String BASE_URI = System.getProperty("patreon.rest.uri", "https://www.patreon.com");

  private static final Logger LOG = LoggerFactory.getLogger(PatreonAPI.class);
  private final String accessToken;
  private final RequestUtil requestUtil;
  private ResourceConverter converter;

  /**
   * Create a new instance of the Patreon API. You only need <b>one</b> of these objects unless you are using the API
   * with multiple tokens
   *
   * @param accessToken The "Creator's Access Token" found on
   *                    <a href="https://www.patreon.com/platform/documentation/clients">the patreon client page</a>
   *                    <b>OR</b> OAuth access token
   */
  public PatreonAPI(String accessToken) {
    this(accessToken, new RequestUtil());
  }

  /**
   * For use in test.
   */
  PatreonAPI(String accessToken, RequestUtil requestUtil) {
    this.accessToken = accessToken;
    this.requestUtil = requestUtil;

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    this.converter = new ResourceConverter(
      objectMapper,
      User.class,
      Campaign.class,
      Pledge.class
    );
    this.converter.enableDeserializationOption(DeserializationFeature.ALLOW_UNKNOWN_INCLUSIONS);
  }

  /**
   * Get the user object of the creator
   *
   * @return the current user
   * @throws IOException Thrown when the GET request failed
   */
  public JSONAPIDocument<User> fetchUser() throws IOException {
    return fetchUser(null);
  }

  /**
   * Get the user object of the creator
   *
   * @param optionalFields A list of optional fields to request, or null.  See {@link User.UserField}
   * @return the current user
   * @throws IOException Thrown when the GET request failed
   */
  public JSONAPIDocument<User> fetchUser(Collection<User.UserField> optionalFields) throws IOException {
    URIBuilder pathBuilder = new URIBuilder()
                    .setPath("current_user")
                    .addParameter("include", "pledges");
    if (optionalFields != null) {
      Set<User.UserField> optionalAndDefaultFields = new HashSet<>(optionalFields);
      optionalAndDefaultFields.addAll(User.UserField.getDefaultFields());
      addFieldsParam(pathBuilder, User.class, optionalAndDefaultFields);
    }

    return converter.readDocument(
      getDataStream(pathBuilder.toString()),
      User.class
    );
  }

  /**
   * Get a list of campaigns the current creator is running - also contains other related data like Goals
   * Note: The first campaign data object is located at index 0 in the data list
   *
   * @return the list of campaigns
   * @throws IOException Thrown when the GET request failed
   */
  public JSONAPIDocument<List<Campaign>> fetchCampaigns() throws IOException {
    String path = new URIBuilder()
                    .setPath("current_user/campaigns")
                    .addParameter("include", "rewards,creator,goals")
                    .toString();
    return converter.readDocumentCollection(
      getDataStream(path),
      Campaign.class
    );
  }

  /**
   * Retrieve pledges for the specified campaign
   *
   * @param campaignId id for campaign to retrieve
   * @param pageSize   how many pledges to return
   * @param pageCursor A cursor retreived from a previous API call, or null for the initial page.
   *                   See {@link #getNextCursorFromDocument(JSONAPIDocument)}
   * @return the page of pledges
   * @throws IOException Thrown when the GET request failed
   */
  public JSONAPIDocument<List<Pledge>> fetchPageOfPledges(String campaignId, int pageSize, String pageCursor) throws IOException {
    return fetchPageOfPledges(campaignId, pageSize, pageCursor, null);
  }

  /**
   * Retrieve pledges for the specified campaign
   *
   * @param campaignId id for campaign to retrieve
   * @param pageSize   how many pledges to return
   * @param pageCursor A cursor retreived from a previous API call, or null for the initial page.
   *                   See {@link #getNextCursorFromDocument(JSONAPIDocument)}
   * @param optionalFields A list of optional fields to return.  See {@link Pledge.PledgeField}
   * @return the page of pledges
   * @throws IOException Thrown when the GET request failed
   */
  public JSONAPIDocument<List<Pledge>> fetchPageOfPledges(String campaignId, int pageSize, String pageCursor, Collection<Pledge.PledgeField> optionalFields) throws IOException {
    URIBuilder pathBuilder = new URIBuilder()
                               .setPath(String.format("campaigns/%s/pledges", campaignId))
                               .addParameter("page[count]", String.valueOf(pageSize));
    if (pageCursor != null) {
      pathBuilder.addParameter("page[cursor]", pageCursor);
    }
    if (optionalFields != null) {
      Set<Pledge.PledgeField> optionalAndDefaultFields = new HashSet<>(optionalFields);
      optionalAndDefaultFields.addAll(Pledge.PledgeField.getDefaultFields());
      addFieldsParam(pathBuilder, Pledge.class, optionalAndDefaultFields);
    }
    return converter.readDocumentCollection(
      getDataStream(pathBuilder.toString()),
      Pledge.class
    );
  }

  public String getNextCursorFromDocument(JSONAPIDocument document) {
    Links links = document.getLinks();
    if (links == null) {
      return null;
    }
    Link nextLink = links.getNext();
    if (nextLink == null) {
      return null;
    }
    String nextLinkString = nextLink.toString();
    try {
      List<NameValuePair> queryParameters = URLEncodedUtils.parse(new URI(nextLinkString), "utf8");
      for (NameValuePair pair : queryParameters) {
        String name = pair.getName();
        if (name.equals("page[cursor]")) {
          return pair.getValue();
        }
      }
    } catch (URISyntaxException e) {
      LOG.error(e.getMessage());
    }
    return null;
  }

  public List<Pledge> fetchAllPledges(String campaignId) throws IOException {
    Set<Pledge> pledges = new HashSet<>();
    String cursor = null;
    while (true) {
      JSONAPIDocument<List<Pledge>> pledgesPage = fetchPageOfPledges(campaignId, 15, cursor);
      pledges.addAll(pledgesPage.get());
      cursor = getNextCursorFromDocument(pledgesPage);
      if (cursor == null) {
        break;
      }
    }
    return new ArrayList<>(pledges);
  }


  private InputStream getDataStream(String suffix) throws IOException {
    return this.requestUtil.request(suffix, this.accessToken);
  }

  /**
   * Add fields[type]=fieldName,fieldName,fieldName as a query parameter to the request represented by builder
   * @param builder A URIBuilder building a request to the API
   * @param type A BaseResource annotated with {@link com.github.jasminb.jsonapi.annotations.Type}
   * @param fields A list of fields to include.  Only fields in this list will be retrieved in the query
   * @return builder
   */
  private URIBuilder addFieldsParam(URIBuilder builder, Class<? extends BaseResource> type, Collection<? extends Field> fields) {
    List<String> fieldNames = new ArrayList<>();
    for (Field f : fields) {
      fieldNames.add(f.getPropertyName());
    }
    String typeStr = BaseResource.getType(type);
    builder.addParameter("fields[" + typeStr + "]", String.join(",", fieldNames));

    return builder;
  }

}