package app.coronawarn.verification.controller;

import static java.util.concurrent.TimeUnit.MILLISECONDS;

import app.coronawarn.verification.config.VerificationApplicationConfig;
import app.coronawarn.verification.domain.VerificationAppSession;
import app.coronawarn.verification.exception.VerificationServerException;
import app.coronawarn.verification.model.AppSessionSourceOfTrust;
import app.coronawarn.verification.model.HashedGuid;
import app.coronawarn.verification.model.LabTestResult;
import app.coronawarn.verification.model.RegistrationToken;
import app.coronawarn.verification.model.TestResult;
import app.coronawarn.verification.service.AppSessionService;
import app.coronawarn.verification.service.FakeDelayService;
import app.coronawarn.verification.service.FakeRequestService;
import app.coronawarn.verification.service.TestResultServerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.time.ZoneOffset;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StopWatch;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;


/**
 * This class represents the rest controller for requests regarding test states.
 */
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/version/v1")
@Validated
@Profile("external")
public class ExternalTestStateController {

  /**
   * The route to the test status of the COVID-19 test endpoint.
   */
  public static final String TESTRESULT_ROUTE = "/testresult";

  public static final Integer RESPONSE_PADDING_LENGTH = 45;

  private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);

  private final FakeRequestService fakeRequestService;

  private final AppSessionService appSessionService;

  private final TestResultServerService testResultServerService;

  private final FakeDelayService fakeDelayService;

  private final VerificationApplicationConfig config;

  /**
   * Returns the test status of the COVID-19 test with cwa-fake header.
   *
   * @param registrationToken generated by a hashed guid {@link RegistrationToken}
   * @param fake              flag for fake request
   * @return result of the test, which can be POSITIVE, NEGATIVE, INVALID, PENDING, FAILED,
   *     quick-test-POSITIVE, quick-test-NEGATIVE, quick-test-INVALID, quick-test-PENDING or quick-test-FAILED
   *     will be POSITIVE for TeleTan
   */
  @Operation(
    summary = "COVID-19 test result for given RegistrationToken",
    description = "Gets the result of COVID-19 Test. "
      + "If the RegistrationToken belongs to a TeleTan the result is always positive"
  )
  @ApiResponses(value = {
    @ApiResponse(responseCode = "200", description = "Testresult retrieved"),
    @ApiResponse(responseCode = "403", description = "TestResult of dob hash does not equal to TestResult of hash")
  })
  @PostMapping(value = TESTRESULT_ROUTE,
    consumes = MediaType.APPLICATION_JSON_VALUE,
    produces = MediaType.APPLICATION_JSON_VALUE
  )
  public DeferredResult<ResponseEntity<TestResult>> getTestState(
    @Valid @RequestBody RegistrationToken registrationToken,
    @RequestHeader(value = "cwa-fake", required = false) String fake) {
    if ((fake != null) && (fake.equals("1"))) {
      return fakeRequestService.getTestState(registrationToken);
    }
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    Optional<VerificationAppSession> appSession =
      appSessionService.getAppSessionByToken(registrationToken.getRegistrationToken());
    if (appSession.isPresent()) {
      AppSessionSourceOfTrust sourceOfTrust = appSession.get().getSourceOfTrust();
      DeferredResult<ResponseEntity<TestResult>> deferredResult = new DeferredResult<>();

      switch (sourceOfTrust) {
        case HASHED_GUID:
          HashedGuid hash = new HashedGuid(appSession.get().getHashedGuid());
          TestResult testResult = testResultServerService.result(hash);
          testResult.setResponsePadding(RandomStringUtils.randomAlphanumeric(RESPONSE_PADDING_LENGTH));

          // Check DOB Hash if present
          if (!config.isDisableDobHashCheckForExternalTestResult() && appSession.get().getHashedGuidDob() != null) {
            HashedGuid hashDob = new HashedGuid(appSession.get().getHashedGuidDob());
            TestResult testResultDob = testResultServerService.result(hashDob);

            // TRS will always respond with a TestResult so we have to check if both results are equal
            if (testResultDob.getTestResult() != testResult.getTestResult()) {
              // given DOB Hash is invalid
              throw new VerificationServerException(HttpStatus.FORBIDDEN,
                "TestResult of dob hash does not equal to TestResult of hash");
            }
          }

          log.debug("Result {}", testResult);
          log.info("The result for registration token based on hashed Guid will be returned.");
          stopWatch.stop();
          fakeDelayService.updateFakeTestRequestDelay(stopWatch.getTotalTimeMillis());
          deferredResult.setResult(ResponseEntity.ok(generateReturnTestResult(testResult.getTestResult(),
            testResult.getSc(), testResult.getLabId())));
          return deferredResult;
        case TELETAN:
          log.info("The result for registration token based on teleTAN will be returned.");
          stopWatch.stop();
          fakeDelayService.updateFakeTestRequestDelay(stopWatch.getTotalTimeMillis());
          scheduledExecutor.schedule(() -> deferredResult.setResult(ResponseEntity.ok(
            generateReturnTestResult(LabTestResult.POSITIVE.getTestResult(),
              appSession.get().getCreatedAt().toEpochSecond(ZoneOffset.UTC), null))),
            fakeDelayService.realDelayTest(), MILLISECONDS);
          return deferredResult;
        default:
          stopWatch.stop();
          throw new VerificationServerException(HttpStatus.BAD_REQUEST,
            "Unknown source of trust inside the appsession for the registration token");
      }
    }
    log.info("The registration token doesn't exist.");
    throw new VerificationServerException(HttpStatus.BAD_REQUEST,
      "Returning the test result for the registration token failed");
  }

  private TestResult generateReturnTestResult(Integer testResult, Long sc, String labId) {
    return new TestResult(testResult,sc, labId, RandomStringUtils.randomAlphanumeric(RESPONSE_PADDING_LENGTH));
  }
}
