/**
 * Copyright 2017-2019 The Feign Authors
 *
 * 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 feign.error;

import feign.Response;
import feign.codec.Decoder;
import feign.codec.ErrorDecoder;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import static feign.Feign.configKey;

public class AnnotationErrorDecoder implements ErrorDecoder {

  private final Map<String, MethodErrorHandler> errorHandlerMap;
  private final ErrorDecoder defaultDecoder;


  AnnotationErrorDecoder(Map<String, MethodErrorHandler> errorHandlerMap,
      ErrorDecoder defaultDecoder) {
    this.errorHandlerMap = errorHandlerMap;
    this.defaultDecoder = defaultDecoder;
  }

  @Override
  public Exception decode(String methodKey, Response response) {
    if (errorHandlerMap.containsKey(methodKey)) {
      return errorHandlerMap.get(methodKey).decode(response);
    }
    return defaultDecoder.decode(methodKey, response);
  }


  public static AnnotationErrorDecoder.Builder builderFor(Class<?> apiType) {
    return new Builder(apiType);
  }

  public static class Builder {
    private final Class<?> apiType;
    private ErrorDecoder defaultDecoder = new ErrorDecoder.Default();
    private Decoder responseBodyDecoder = new Decoder.Default();


    public Builder(Class<?> apiType) {
      this.apiType = apiType;
    }

    public Builder withDefaultDecoder(ErrorDecoder defaultDecoder) {
      this.defaultDecoder = defaultDecoder;
      return this;
    }

    public Builder withResponseBodyDecoder(Decoder responseBodyDecoder) {
      this.responseBodyDecoder = responseBodyDecoder;
      return this;
    }

    public AnnotationErrorDecoder build() {
      Map<String, MethodErrorHandler> errorHandlerMap = generateErrorHandlerMapFromApi(apiType);
      return new AnnotationErrorDecoder(errorHandlerMap, defaultDecoder);
    }

    Map<String, MethodErrorHandler> generateErrorHandlerMapFromApi(Class<?> apiType) {

      ExceptionGenerator classLevelDefault = new ExceptionGenerator.Builder()
          .withResponseBodyDecoder(responseBodyDecoder)
          .withExceptionType(ErrorHandling.NO_DEFAULT.class)
          .build();
      Map<Integer, ExceptionGenerator> classLevelStatusCodeDefinitions =
          new HashMap<Integer, ExceptionGenerator>();

      Optional<ErrorHandling> classLevelErrorHandling =
          readErrorHandlingIncludingInherited(apiType);
      if (classLevelErrorHandling.isPresent()) {
        ErrorHandlingDefinition classErrorHandlingDefinition =
            readAnnotation(classLevelErrorHandling.get(), responseBodyDecoder);
        classLevelDefault = classErrorHandlingDefinition.defaultThrow;
        classLevelStatusCodeDefinitions = classErrorHandlingDefinition.statusCodesMap;
      }

      Map<String, MethodErrorHandler> methodErrorHandlerMap =
          new HashMap<String, MethodErrorHandler>();
      for (Method method : apiType.getMethods()) {
        if (method.isAnnotationPresent(ErrorHandling.class)) {
          ErrorHandlingDefinition methodErrorHandling =
              readAnnotation(method.getAnnotation(ErrorHandling.class), responseBodyDecoder);
          ExceptionGenerator methodDefault = methodErrorHandling.defaultThrow;
          if (methodDefault.getExceptionType().equals(ErrorHandling.NO_DEFAULT.class)) {
            methodDefault = classLevelDefault;
          }

          MethodErrorHandler methodErrorHandler =
              new MethodErrorHandler(methodErrorHandling.statusCodesMap,
                  classLevelStatusCodeDefinitions, methodDefault);

          methodErrorHandlerMap.put(configKey(apiType, method), methodErrorHandler);
        }
      }

      return methodErrorHandlerMap;
    }

    Optional<ErrorHandling> readErrorHandlingIncludingInherited(Class<?> apiType) {
      if (apiType.isAnnotationPresent(ErrorHandling.class)) {
        return Optional.of(apiType.getAnnotation(ErrorHandling.class));
      }
      for (Class<?> parentInterface : apiType.getInterfaces()) {
        Optional<ErrorHandling> errorHandling =
            readErrorHandlingIncludingInherited(parentInterface);
        if (errorHandling.isPresent()) {
          return errorHandling;
        }
      }
      // Finally, if there's a superclass that isn't Object check if the superclass has anything
      if (!apiType.isInterface() && !apiType.getSuperclass().equals(Object.class)) {
        return readErrorHandlingIncludingInherited(apiType.getSuperclass());
      }
      return Optional.empty();
    }

    static ErrorHandlingDefinition readAnnotation(ErrorHandling errorHandling,
                                                  Decoder responseBodyDecoder) {
      ExceptionGenerator defaultException = new ExceptionGenerator.Builder()
          .withResponseBodyDecoder(responseBodyDecoder)
          .withExceptionType(errorHandling.defaultException())
          .build();
      Map<Integer, ExceptionGenerator> statusCodesDefinition =
          new HashMap<Integer, ExceptionGenerator>();

      for (ErrorCodes statusCodeDefinition : errorHandling.codeSpecific()) {
        for (int statusCode : statusCodeDefinition.codes()) {
          if (statusCodesDefinition.containsKey(statusCode)) {
            throw new IllegalStateException(
                "Status Code [" + statusCode + "] " +
                    "has already been declared to throw ["
                    + statusCodesDefinition.get(statusCode).getExceptionType().getName() + "] " +
                    "and [" + statusCodeDefinition.generate() + "] - dupe definition");
          }
          statusCodesDefinition.put(statusCode,
              new ExceptionGenerator.Builder()
                  .withResponseBodyDecoder(responseBodyDecoder)
                  .withExceptionType(statusCodeDefinition.generate())
                  .build());
        }
      }

      return new ErrorHandlingDefinition(defaultException, statusCodesDefinition);
    }

    private static class ErrorHandlingDefinition {
      private final ExceptionGenerator defaultThrow;
      private final Map<Integer, ExceptionGenerator> statusCodesMap;


      private ErrorHandlingDefinition(ExceptionGenerator defaultThrow,
          Map<Integer, ExceptionGenerator> statusCodesMap) {
        this.defaultThrow = defaultThrow;
        this.statusCodesMap = statusCodesMap;
      }
    }
  }
}