/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.hadoop.fs.azure;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import com.google.common.annotations.VisibleForTesting;

/**
 * An fsck tool implementation for WASB that does various admin/cleanup/recovery
 * tasks on the WASB file system.
 */
@InterfaceAudience.Public
@InterfaceStability.Evolving
public class WasbFsck extends Configured implements Tool {
  private FileSystem mockFileSystemForTesting = null;
  private static final String LOST_AND_FOUND_PATH = "/lost+found";
  private boolean pathNameWarning = false;

  public WasbFsck(Configuration conf) {
    super(conf);
  }

  /**
   * For testing purposes, set the file system to use here instead of relying on
   * getting it from the FileSystem class based on the URI.
   * 
   * @param fileSystem
   *          The file system to use.
   */
  @VisibleForTesting
  public void setMockFileSystemForTesting(FileSystem fileSystem) {
    this.mockFileSystemForTesting = fileSystem;
  }

  @Override
  public int run(String[] args) throws Exception {
    if (doPrintUsage(Arrays.asList(args))) {
      printUsage();
      return -1;
    }
    Path pathToCheck = null;
    boolean doRecover = false;
    boolean doDelete = false;
    for (String arg : args) {
      if (!arg.startsWith("-")) {
        if (pathToCheck != null) {
          System.err
              .println("Can't specify multiple paths to check on the command-line");
          return 1;
        }
        pathToCheck = new Path(arg);
      } else if (arg.equals("-move")) {
        doRecover = true;
      } else if (arg.equals("-delete")) {
        doDelete = true;
      }
    }
    if (doRecover && doDelete) {
      System.err
          .println("Conflicting options: can't specify both -move and -delete.");
      return 1;
    }
    if (pathToCheck == null) {
      pathToCheck = new Path("/"); // Check everything.
    }
    FileSystem fs;
    if (mockFileSystemForTesting == null) {
      fs = FileSystem.get(pathToCheck.toUri(), getConf());
    } else {
      fs = mockFileSystemForTesting;
    }

    if (!recursiveCheckChildPathName(fs, fs.makeQualified(pathToCheck))) {
      pathNameWarning = true;
    }

    if (!(fs instanceof NativeAzureFileSystem)) {
      System.err
          .println("Can only check WASB file system. Instead I'm asked to"
              + " check: " + fs.getUri());
      return 2;
    }
    NativeAzureFileSystem wasbFs = (NativeAzureFileSystem) fs;
    if (doRecover) {
      System.out.println("Recovering files with dangling data under: "
          + pathToCheck);
      wasbFs.recoverFilesWithDanglingTempData(pathToCheck, new Path(
          LOST_AND_FOUND_PATH));
    } else if (doDelete) {
      System.out.println("Deleting temp files with dangling data under: "
          + pathToCheck);
      wasbFs.deleteFilesWithDanglingTempData(pathToCheck);
    } else {
      System.out.println("Please specify -move or -delete");
    }
    return 0;
  }

  public boolean getPathNameWarning() {
    return pathNameWarning;
  }

  /**
   * Recursively check if a given path and its child paths have colons in their
   * names. It returns true if none of them has a colon or this path does not
   * exist, and false otherwise.
   */
  private boolean recursiveCheckChildPathName(FileSystem fs, Path p)
      throws IOException {
    if (p == null) {
      return true;
    }
    if (!fs.exists(p)) {
      System.out.println("Path " + p + " does not exist!");
      return true;
    }

    if (fs.isFile(p)) {
      if (containsColon(p)) {
        System.out.println("Warning: file " + p + " has a colon in its name.");
        return false;
      } else {
        return true;
      }
    } else {
      boolean flag;
      if (containsColon(p)) {
        System.out.println("Warning: directory " + p
            + " has a colon in its name.");
        flag = false;
      } else {
        flag = true;
      }
      FileStatus[] listed = fs.listStatus(p);
      for (FileStatus l : listed) {
        if (!recursiveCheckChildPathName(fs, l.getPath())) {
          flag = false;
        }
      }
      return flag;
    }
  }

  private boolean containsColon(Path p) {
    return p.toUri().getPath().toString().contains(":");
  }

  private static void printUsage() {
    System.out.println("Usage: WasbFSck [<path>] [-move | -delete]");
    System.out.println("\t<path>\tstart checking from this path");
    System.out.println("\t-move\tmove any files whose upload was interrupted"
        + " mid-stream to " + LOST_AND_FOUND_PATH);
    System.out
        .println("\t-delete\tdelete any files whose upload was interrupted"
            + " mid-stream");
    ToolRunner.printGenericCommandUsage(System.out);
  }

  private boolean doPrintUsage(List<String> args) {
    return args.contains("-H");
  }

  public static void main(String[] args) throws Exception {
    int res = ToolRunner.run(new WasbFsck(new Configuration()), args);
    System.exit(res);
  }
}
