/**
 * Copyright 2016 Google Inc.
 *
 * 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 com.google.cloud.security.scanner.sources;

import com.google.api.services.cloudresourcemanager.CloudResourceManager.Projects;
import com.google.api.services.cloudresourcemanager.model.ListProjectsResponse;
import com.google.api.services.cloudresourcemanager.model.Project;
import com.google.cloud.dataflow.sdk.coders.Coder;
import com.google.cloud.dataflow.sdk.coders.SerializableCoder;
import com.google.cloud.dataflow.sdk.io.BoundedSource;
import com.google.cloud.dataflow.sdk.options.PipelineOptions;
import com.google.cloud.security.scanner.primitives.GCPProject;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;

/** Source for listing projects through the CRM API. */
public class LiveProjectSource extends BoundedSource<GCPProject> {
  private static final long SIZE_ESTIMATE = 200;
  private static final String DELETE_PREFIX = "DELETE";
  private String orgId;

  /**
   * Constructor for LiveProjectSource.
   * @param orgId The ID of the organization from which we read the projects list.
   *  Set as null to read from all visible orgs.
   */
  public LiveProjectSource(String orgId) {
    this.orgId = orgId;
  }

  /**
   * Getter for the field orgId.
   * @return The ID of the organization whose projects are to be read.
   */
  public String getOrgId() {
    return this.orgId;
  }

  /**
   * This function just returns the same source as a list, and does not
   * actually split the load into several bundles.
   * @param desiredBundleSizeBytes The desired bundle size. Not used.
   * @param options Pipeline options. Not used
   * @return A list containing this source as its only element.
   */
  @Override
  public List<LiveProjectSource> splitIntoBundles(
      long desiredBundleSizeBytes, PipelineOptions options) throws Exception {
    List<LiveProjectSource> projectSources = new ArrayList<>(1);
    projectSources.add(this);
    return projectSources;
  }

  /**
   * Currently returns a hardcoded value.
   * Get the estimated size of the data that will be read by this source.
   * @param options Pipeline options. Not used.
   * @return The size estimate of the data to be read by this source.
   */
  @Override
  public long getEstimatedSizeBytes(PipelineOptions options) throws Exception {
    return SIZE_ESTIMATE;
  }

  /**
   * This function does not give any guarantees about whether its output will be sorted or not.
   * @param options Pipeline options. Not used.
   * @return Returns true if the source's output is always sorted. False otherwise.
   */
  @Override
  public boolean producesSortedKeys(PipelineOptions options) throws Exception {
    return false;
  }

  /**
   * Create a new reader that will read from this source.
   * @param options Pipeline options. Not used.
   * @return A BoundedReader object to read from this source.
   */
  @Override
  public BoundedReader<GCPProject> createReader(PipelineOptions options) throws IOException {
    return new LiveProjectReader(this);
  }

  /**
   * Validate whether this source can function or not.
   */
  @Override
  public void validate() {}

  /**
   * Get the default coder to use for this source's output.
   * @return The default coder to use for this source's output.
   */
  @Override
  public Coder<GCPProject> getDefaultOutputCoder() {
    return SerializableCoder.of(GCPProject.class);
  }

  /** Reader for LiveProjectSource */
  public static class LiveProjectReader extends BoundedReader<GCPProject> {
    private LiveProjectSource source;
    private List<GCPProject> projects;
    private String nextPageToken;

    /**
     * Getter for the projects field.
     * @return Return the projects currently in the reader's queue.
     */
    List<GCPProject> getProjects() {
      return projects;
    }

    /**
     * Getter for the nextPageToken field.
     * @return Return the nextPageToken to be used in the
     * next request to get the next page of projects.
     */
    String getNextPageToken() {
      return nextPageToken;
    }

    /**
     * Construct a LiveProjectReader object.
     * @param source The source this reader is supposed to read from.
     */
    public LiveProjectReader(LiveProjectSource source) {
      this.source = source;
      this.projects = new ArrayList<>();
      this.nextPageToken = null;
    }

    /**
     * Initialize the reader so it can begin reading files.
     * @return True if the initialization succeeded. False otherwise.
     */
    @Override
    public boolean start() throws IOException {
      return refreshProjects(null);
    }

    /**
     * Advance the reader so it can read the next file in the queue.
     * @return True if there are still more files to be read. False otherwise.
     */
    @Override
    public boolean advance() throws IOException {
      if (this.projects.size() > 1) {
        this.projects.remove(0);
        return true;
      } else if (this.projects.size() == 1) {
        this.projects.remove(0);
        // if token is null the last page read was the last page to be read.
        return (this.nextPageToken != null) && refreshProjects(this.nextPageToken);
      }
      return false;
    }

    /**
     * Get the next file in queue.
     * @return A GCPProject object representing the project that is read.
     * @throws NoSuchElementException If the file can't be read from the CRM API.
     */
    @Override
    public GCPProject getCurrent() throws NoSuchElementException {
      if (this.projects.isEmpty()) {
        throw new NoSuchElementException("No more GCPProject objects");
      }
      return this.projects.get(0);
    }

    /**
     * Close this reader.
     */
    @Override
    public void close() throws IOException {}

    /**
     * Return the source this reader is reading from.
     * @return The source this reader is reading from.
     */
    @Override
    public BoundedSource<GCPProject> getCurrentSource() {
      return this.source;
    }

    private boolean refreshProjects(String nextPageToken) throws IOException {
      ListProjectsResponse projectListResponse;
      Projects.List projectsList;
      try {
        projectsList = GCPProject.getProjectsApiStub().list();
        if (nextPageToken != null) {
          projectsList = projectsList.setPageToken(nextPageToken);
        }
        if (source.getOrgId() != null) {
            projectsList = projectsList
                .setFilter("parent.type:organization parent.id:" + source.getOrgId());
        }
        projectListResponse = projectsList.execute();
      } catch (GeneralSecurityException gse) {
        throw new IOException("Cannot get projects. Access denied");
      }
      List<Project> projects = projectListResponse.getProjects();

      for (Project project : projects) {
        String orgId = null;
        if (project.getParent() != null) {
          orgId = project.getParent().getId();
        }
        if (project.getLifecycleState() == null
            || project.getLifecycleState().startsWith(DELETE_PREFIX)) {
          continue;
        }
        this.projects.add(new GCPProject(project.getProjectId(), orgId, project.getName()));
      }
      this.nextPageToken = projectListResponse.getNextPageToken();

      return !this.projects.isEmpty();
    }
  }
}