// Copyright (C) 2010 The Android Open Source Project
//
// 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.gerrit.sshd.commands;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ListChildProjects;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
import com.google.inject.Provider;

import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
@CommandMetaData(name = "set-project-parent", description = "Change the project permissions are inherited from")
final class AdminSetParent extends SshCommand {
  private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);

  @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
  private ProjectControl newParent;

  @Option(name = "--children-of", metaVar = "NAME",
      usage = "parent project for which the child projects should be reparented")
  private ProjectControl oldParent;

  @Option(name = "--exclude", metaVar = "NAME",
      usage = "child project of old parent project which should not be reparented")
  private List<ProjectControl> excludedChildren = new ArrayList<ProjectControl>();

  @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
      usage = "projects to modify")
  private List<ProjectControl> children = new ArrayList<ProjectControl>();

  @Inject
  private ProjectCache projectCache;

  @Inject
  private MetaDataUpdate.User metaDataUpdateFactory;

  @Inject
  private AllProjectsName allProjectsName;

  @Inject
  private Provider<ListChildProjects> listChildProjects;

  private Project.NameKey newParentKey = null;

  @Override
  protected void run() throws Failure {
    if (oldParent == null && children.isEmpty()) {
      throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
                                   "arguments or the --children-of option has to be set");
    }
    if (oldParent == null && !excludedChildren.isEmpty()) {
      throw new UnloggedFailure(1, "fatal: --exclude can only be used together " +
                                   "with --children-of");
    }

    final StringBuilder err = new StringBuilder();
    final Set<Project.NameKey> grandParents = new HashSet<Project.NameKey>();

    grandParents.add(allProjectsName);

    if (newParent != null) {
      newParentKey = newParent.getProject().getNameKey();

      // Catalog all grandparents of the "parent", we want to
      // catch a cycle in the parent pointers before it occurs.
      //
      Project.NameKey gp = newParent.getProject().getParent();
      while (gp != null && grandParents.add(gp)) {
        final ProjectState s = projectCache.get(gp);
        if (s != null) {
          gp = s.getProject().getParent();
        } else {
          break;
        }
      }
    }

    final List<Project.NameKey> childProjects = Lists.newArrayList();
    for (final ProjectControl pc : children) {
      childProjects.add(pc.getProject().getNameKey());
    }
    if (oldParent != null) {
      childProjects.addAll(getChildrenForReparenting(oldParent));
    }

    for (final Project.NameKey nameKey : childProjects) {
      final String name = nameKey.get();

      if (allProjectsName.equals(nameKey)) {
        // Don't allow the wild card project to have a parent.
        //
        err.append("error: Cannot set parent of '").append(name).append("'\n");
        continue;
      }

      if (grandParents.contains(nameKey) || nameKey.equals(newParentKey)) {
        // Try to avoid creating a cycle in the parent pointers.
        //
        err.append("error: Cycle exists between '")
           .append(name)
           .append("' and '")
           .append(newParentKey != null ? newParentKey.get() : allProjectsName.get())
           .append("'\n");
        continue;
      }

      try {
        MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
        try {
          ProjectConfig config = ProjectConfig.read(md);
          config.getProject().setParentName(newParentKey);
          md.setMessage("Inherit access from "
              + (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
          config.commit(md);
        } finally {
          md.close();
        }
      } catch (RepositoryNotFoundException notFound) {
        err.append("error: Project ").append(name).append(" not found\n");
      } catch (IOException e) {
        final String msg = "Cannot update project " + name;
        log.error(msg, e);
        err.append("error: ").append(msg).append("\n");
      } catch (ConfigInvalidException e) {
        final String msg = "Cannot update project " + name;
        log.error(msg, e);
        err.append("error: ").append(msg).append("\n");
      }

      projectCache.evict(nameKey);
    }

    if (err.length() > 0) {
      while (err.charAt(err.length() - 1) == '\n') {
        err.setLength(err.length() - 1);
      }
      throw new UnloggedFailure(1, err.toString());
    }
  }

  /**
   * Returns the children of the specified parent project that should be
   * reparented. The returned list of child projects does not contain projects
   * that were specified to be excluded from reparenting.
   */
  private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) {
    final List<Project.NameKey> childProjects = Lists.newArrayList();
    final List<Project.NameKey> excluded =
      new ArrayList<Project.NameKey>(excludedChildren.size());
    for (final ProjectControl excludedChild : excludedChildren) {
      excluded.add(excludedChild.getProject().getNameKey());
    }
    final List<Project.NameKey> automaticallyExcluded =
      new ArrayList<Project.NameKey>(excludedChildren.size());
    if (newParentKey != null) {
      automaticallyExcluded.addAll(getAllParents(newParentKey));
    }
    for (final ProjectInfo child : listChildProjects.get().apply(
        new ProjectResource(parent))) {
      final Project.NameKey childName = new Project.NameKey(child.name);
      if (!excluded.contains(childName)) {
        if (!automaticallyExcluded.contains(childName)) {
          childProjects.add(childName);
        } else {
          stdout.println("Automatically excluded '" + childName + "' " +
                         "from reparenting because it is in the parent " +
                         "line of the new parent '" + newParentKey + "'.");
        }
      }
    }
    return childProjects;
  }

  private Set<Project.NameKey> getAllParents(final Project.NameKey projectName) {
    ProjectState ps = projectCache.get(projectName);
    return ImmutableSet.copyOf(Iterables.transform(
      ps != null ? ps.parents() : Collections.<ProjectState> emptySet(),
      new Function<ProjectState, Project.NameKey> () {
        @Override
        public Project.NameKey apply(ProjectState in) {
          return in.getProject().getNameKey();
        }
      }));
  }
}
