/**
 * 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.gateway.services.security.impl;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.net.ntp.TimeStamp;
import org.apache.hadoop.gateway.i18n.GatewaySpiMessages;
import org.apache.hadoop.gateway.i18n.messages.MessagesFactory;
import org.apache.hadoop.gateway.services.ServiceLifecycleException;
import org.apache.hadoop.gateway.services.security.EncryptionResult;

import java.io.Console;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class CMFMasterService {
  private static GatewaySpiMessages LOG = MessagesFactory.get( GatewaySpiMessages.class );

  private static final String MASTER_PASSPHRASE = "masterpassphrase";
  private static final String MASTER_PERSISTENCE_TAG = "#1.0# " + TimeStamp.getCurrentTime().toDateString();
  protected char[] master = null;
  protected String serviceName = null;
  private AESEncryptor aes = new AESEncryptor(MASTER_PASSPHRASE);

  public CMFMasterService(String serviceName) {
    super();
    this.serviceName = serviceName;
  }

  public char[] getMasterSecret() {
    return this.master;
  }

  protected void setupMasterSecret(String securityDir, boolean persisting) throws ServiceLifecycleException {
    setupMasterSecret(securityDir, serviceName + "-master", persisting);
  }

  protected void setupMasterSecret(String securityDir, String filename, boolean persisting) throws ServiceLifecycleException {
    File masterFile = new File(securityDir, filename);
    if (masterFile.exists()) {
      try {
        initializeFromMaster(masterFile);
      } catch (Exception e) {
        // TODO Auto-generated catch block
        throw new ServiceLifecycleException("Unable to load the persisted master secret.", e);
      }
    }
    else {
	  if (master == null) {
        displayWarning(persisting);
        promptUser();
      }
      if(persisting) {
        persistMaster(master, masterFile);
      }
    }
  }

  protected void promptUser() {
    Console c = System.console();
    if (c == null) {
      LOG.unableToPromptForMasterUseKnoxCLI();
      System.err.println("No console.");
      System.exit(1);
    }
  
    boolean noMatch;
    do {
        char [] newPassword1 = c.readPassword("Enter master secret: ");
        char [] newPassword2 = c.readPassword("Enter master secret again: ");
        noMatch = ! Arrays.equals(newPassword1, newPassword2);
        if (noMatch) {
            c.format("Passwords don't match. Try again.%n");
        } else {
            this.master = Arrays.copyOf(newPassword1, newPassword1.length);
        }
        Arrays.fill(newPassword1, ' ');
        Arrays.fill(newPassword2, ' ');
    } while (noMatch);
  }

  protected void displayWarning(boolean persisting) {
    Console c = System.console();
    if (c == null) {
        LOG.unableToPromptForMasterUseKnoxCLI();
        System.err.println("No console.");
        System.exit(1);
    }
    if (persisting) {
      c.printf("***************************************************************************************************\n");
      c.printf("You have indicated that you would like to persist the master secret for this service instance.\n");
      c.printf("Be aware that this is less secure than manually entering the secret on startup.\n");
      c.printf("The persisted file will be encrypted and primarily protected through OS permissions.\n");
      c.printf("***************************************************************************************************\n");
    }
    else {
      c.printf("***************************************************************************************************\n");
      c.printf("Be aware that you will need to enter your master secret for future starts exactly as you do here.\n");
      c.printf("This secret is needed to access protected resources for the service process.\n");
      c.printf("The master secret must be protected, kept secret and not stored in clear text anywhere.\n");
      c.printf("***************************************************************************************************\n");
    }
  }

  protected void persistMaster(char[] master, File masterFile) {
    EncryptionResult atom = encryptMaster(master);
    try {
      ArrayList<String> lines = new ArrayList<String>();
      lines.add(MASTER_PERSISTENCE_TAG);
      
      String line = Base64.encodeBase64String((
          Base64.encodeBase64String(atom.salt) + "::" + 
          Base64.encodeBase64String(atom.iv) + "::" + 
          Base64.encodeBase64String(atom.cipher)).getBytes("UTF8"));
      lines.add(line);
      FileUtils.writeLines(masterFile, "UTF8", lines);
      
      // restrict os permissions to only the user running this process
      chmod("600", masterFile);
    } catch (IOException e) {
      LOG.failedToPersistMasterSecret(e);
    }
  }

  private EncryptionResult encryptMaster(char[] master) {
    // TODO Auto-generated method stub
    try {
      return aes.encrypt(new String(master));
    } catch (Exception e) {
      LOG.failedToEncryptMasterSecret(e);
    }
    return null;
  }

  protected void initializeFromMaster(File masterFile) throws Exception {
      try {
        List<String> lines = FileUtils.readLines(masterFile, "UTF8");
        String tag = lines.get(0);
        LOG.loadingFromPersistentMaster( tag );
        String line = new String(Base64.decodeBase64(lines.get(1)));
        String[] parts = line.split("::");
        this.master = new String(aes.decrypt(Base64.decodeBase64(parts[0]), Base64.decodeBase64(parts[1]), Base64.decodeBase64(parts[2])), "UTF8").toCharArray();
      } catch (IOException e) {
        LOG.failedToInitializeFromPersistentMaster(masterFile.getName(), e);
        throw e;
      } catch (Exception e) {
        LOG.failedToInitializeFromPersistentMaster(masterFile.getName(), e);
        throw e;
      }
    }

  private void chmod(String args, File file) throws IOException {
      // TODO: move to Java 7 NIO support to add windows as well
      // TODO: look into the following for Windows: Runtime.getRuntime().exec("attrib -r myFile");
      if (isUnixEnv()) {
          //args and file should never be null.
          if (args == null || file == null) 
            throw new IllegalArgumentException("nullArg");
          if (!file.exists()) 
            throw new IOException("fileNotFound");
  
          // " +" regular expression for 1 or more spaces
          final String[] argsString = args.split(" +");
          List<String> cmdList = new ArrayList<String>();
          cmdList.add("/bin/chmod");
          cmdList.addAll(Arrays.asList(argsString));
          cmdList.add(file.getAbsolutePath());
          new ProcessBuilder(cmdList).start();
      }
  }

  private boolean isUnixEnv() {
    return (File.separatorChar == '/');
  }

}