/*
 * 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.hive.ptest.api.server;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

import org.apache.hive.ptest.api.Status;
import org.apache.hive.ptest.api.request.TestListRequest;
import org.apache.hive.ptest.api.request.TestLogRequest;
import org.apache.hive.ptest.api.request.TestStartRequest;
import org.apache.hive.ptest.api.request.TestStopRequest;
import org.apache.hive.ptest.api.response.TestListResponse;
import org.apache.hive.ptest.api.response.TestLogResponse;
import org.apache.hive.ptest.api.response.TestStartResponse;
import org.apache.hive.ptest.api.response.TestStatus;
import org.apache.hive.ptest.api.response.TestStatusResponse;
import org.apache.hive.ptest.api.response.TestStopResponse;
import org.apache.hive.ptest.execution.PTest;
import org.apache.hive.ptest.execution.conf.Context;
import org.apache.hive.ptest.execution.conf.ExecutionContextConfiguration;
import org.apache.hive.ptest.execution.context.ExecutionContextProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;




/**
 * Server interface of the ptest environment. Each request
 * is converted from JSON and each response is returned in JSON.
 */
@Controller
@RequestMapping(value = "/api/v1")
public class ExecutionController {
  private static final long MAX_READ_SIZE = 1024L * 1024L;
  private static final String CONF_PROPERTY = "hive.ptest.execution.context.conf";

  private static final Logger LOG = LoggerFactory
      .getLogger(ExecutionController.class);
  private final ExecutionContextConfiguration mExecutionContextConfiguration;
  private final ExecutionContextProvider mExecutionContextProvider;
  private final Map<String, Test> mTests;
  private final BlockingQueue<Test> mTestQueue;
  private final TestExecutor mTestExecutor;
  private final File mGlobalLogDir;
  public ExecutionController()
      throws IOException {
    String executionContextConfigurationFile = System.getProperty(CONF_PROPERTY, "").trim();
    Preconditions.checkArgument(!executionContextConfigurationFile.isEmpty(), CONF_PROPERTY + " is required");
    LOG.info("Reading configuration from file: " + executionContextConfigurationFile);
    mExecutionContextConfiguration = ExecutionContextConfiguration.withContext(
        Context.fromFile(executionContextConfigurationFile)
    );
    LOG.info("ExecutionContext is [{}]", mExecutionContextConfiguration);
    mExecutionContextProvider = mExecutionContextConfiguration.getExecutionContextProvider();
    mTests = Collections.synchronizedMap(new LinkedHashMap<String, Test>() {
      private static final long serialVersionUID = 1L;
      @Override
      public boolean removeEldestEntry(Map.Entry<String, Test> entry) {
        Test testExecution = entry.getValue();
        File testOutputFile = testExecution.getOutputFile();
        return size() > 30 || (testOutputFile != null && !testOutputFile.isFile());
      }
    });
    mTestQueue = new ArrayBlockingQueue<Test>(5);
    mGlobalLogDir = new File(mExecutionContextConfiguration.getGlobalLogDirectory());
    mTestExecutor = new TestExecutor(mExecutionContextConfiguration, mExecutionContextProvider,
        mTestQueue, new PTest.Builder());
    mTestExecutor.setName("TestExecutor");
    mTestExecutor.setDaemon(true);
    mTestExecutor.start();
    Runtime.getRuntime().addShutdownHook(new Thread() {
      @Override
      public void run() {
        LOG.info("Shutdown hook called");
        try {
          mTestExecutor.shutdown();
        } catch (Exception e) {
          LOG.error("Error shutting down TestExecutor", e);
        }
        try {
          mExecutionContextProvider.close();
        } catch (Exception e) {
          LOG.error("Error shutting down ExecutionContextProvider", e);
        }
      }
    });
  }

  @RequestMapping(value="/testStart", method = RequestMethod.POST)
  public @ResponseBody TestStartResponse testStart(@RequestBody TestStartRequest startRequest,
      BindingResult result) {
    LOG.info("startRequest " + startRequest.toString());
    TestStartResponse startResponse = doStartTest(startRequest, result);
    LOG.info("startResponse " + startResponse.toString());
    return startResponse;
  }

  private TestStartResponse doStartTest(TestStartRequest startRequest, BindingResult result) {
    if(result.hasErrors() ||
        Strings.nullToEmpty(startRequest.getProfile()).trim().isEmpty() ||
        Strings.nullToEmpty(startRequest.getTestHandle()).trim().isEmpty() ||
        startRequest.getProfile().contains("/")) {
      return new TestStartResponse(Status.illegalArgument());
    }
    if(!assertTestHandleIsAvailable(startRequest.getTestHandle())) {
      return new TestStartResponse(Status.illegalArgument("Test handle " + startRequest.getTestHandle() + " already used"));
    }
    Test test = new Test(startRequest,
        Status.pending(), System.currentTimeMillis());
    if(mTestQueue.offer(test)) {
      mTests.put(startRequest.getTestHandle(), test);
      return new TestStartResponse(Status.ok());
    } else {
      return new TestStartResponse(Status.queueFull());
    }
  }

  @RequestMapping(value="/testStop", method = RequestMethod.POST)
  public @ResponseBody TestStopResponse testStop(@RequestBody TestStopRequest stopRequest,
      BindingResult result) {
    String testHandle = stopRequest.getTestHandle();
    Test test = mTests.get(testHandle);
    if(result.hasErrors() ||
        Strings.nullToEmpty(stopRequest.getTestHandle()).trim().isEmpty() ||
        test == null) {
      return new TestStopResponse(Status.illegalArgument());
    }
    test.setStopRequested(true);
    return new TestStopResponse(Status.ok());
  }

  @RequestMapping(value="/testStatus", method = RequestMethod.POST)
  public @ResponseBody TestStatusResponse testStatus(@RequestBody TestStopRequest stopRequest,
      BindingResult result) {
    String testHandle = stopRequest.getTestHandle();
    Test test = mTests.get(testHandle);
    if(result.hasErrors() ||
        Strings.nullToEmpty(stopRequest.getTestHandle()).trim().isEmpty() ||
        test == null) {
      return new TestStatusResponse(Status.illegalArgument());
    }
    return new TestStatusResponse(Status.ok(), test.toTestStatus());
  }

  @RequestMapping(value="/testLog", method = RequestMethod.POST)
  public @ResponseBody TestLogResponse testLog(@RequestBody TestLogRequest logsRequest,
      BindingResult result) {
    String testHandle = logsRequest.getTestHandle();
    Test testExecution = mTests.get(testHandle);
    if(result.hasErrors() ||
        Strings.nullToEmpty(logsRequest.getTestHandle()).trim().isEmpty() ||
        testExecution == null ||
        logsRequest.getLength() > MAX_READ_SIZE) {
      return new TestLogResponse(Status.illegalArgument());
    }
    File outputFile = testExecution.getOutputFile();
    if(outputFile == null ||
        logsRequest.getOffset() > outputFile.length()) {
      return new TestLogResponse(Status.illegalArgument());
    }
    RandomAccessFile fileHandle = null;
    try {
      fileHandle = new RandomAccessFile(outputFile, "r");
      long offset = logsRequest.getOffset();
      fileHandle.seek(offset);
      int readLength = 0;
      if(offset < fileHandle.length()) {
        readLength = (int)Math.min(fileHandle.length() - offset, logsRequest.getLength());
      }
      byte[] buffer = new byte[readLength];
      fileHandle.readFully(buffer);
      offset += readLength;
      return new TestLogResponse(Status.ok(), offset,
          new String(buffer, Charsets.UTF_8));
    } catch (IOException e) {
      LOG.info("Unexpected IO error reading " + testExecution.getOutputFile() , e);
      return new TestLogResponse(Status.internalError(e.getMessage()));
    } finally {
      if(fileHandle != null) {
        try {
          fileHandle.close();
        } catch (IOException e) {
          LOG.warn("Error closing " + outputFile, e);
        }
      }
    }
  }

  @RequestMapping(value="/testList", method = RequestMethod.POST)
  public @ResponseBody TestListResponse testList(@RequestBody TestListRequest request) {
    List<TestStatus> entries = Lists.newArrayList();
    synchronized (mTests) {
      for(String testHandle : mTests.keySet()) {
        Test test = mTests.get(testHandle);
        entries.add(test.toTestStatus());
      }
    }
    return new TestListResponse(Status.ok(), entries);
  }
  private synchronized boolean assertTestHandleIsAvailable(final String testHandle) {
    File testOutputDir = new File(mGlobalLogDir, testHandle);
    Preconditions.checkState(!testOutputDir.isFile(), "Output directory " + testOutputDir + " is file");
    return testOutputDir.mkdir();
  }
}
