/*
 * Jooby https://jooby.io
 * Apache License Version 2.0 https://jooby.io/LICENSE.txt
 * Copyright 2014 Edgar Espina
 */
package io.jooby;

import static java.util.Optional.ofNullable;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.stream.Stream;

import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import io.jooby.annotation.Transactional;
import io.jooby.exception.*;
import io.jooby.internal.RouterImpl;

/**
 * Route contains information about the HTTP method, path pattern, which content types consumes and
 * produces, etc..
 *
 * <p>Additionally, contains metadata about route return Java type, argument source (query, path,
 * etc..) and Java type.
 *
 * <p>This class contains all the metadata associated to a route. It is like a {@link Class} object
 * for routes.
 *
 * @author edgar
 * @since 2.0.0
 */
public class Route {

  /**
   * Decorates a route handler by running logic before and after route handler.
   *
   * <pre>{@code
   * {
   *   use(next -> ctx -> {
   *     long start = System.currentTimeMillis();
   *     Object result = next.apply(ctx);
   *     long end = System.currentTimeMillis();
   *     System.out.println("Took: " + (end - start));
   *     return result;
   *   });
   * }
   * }</pre>
   *
   * @author edgar
   * @since 2.0.0
   */
  public interface Filter extends Aware {
    /**
     * Chain the filter within next handler.
     *
     * @param next Next handler.
     * @return A new handler.
     */
    Handler apply(Handler next);

    /**
     * Chain this filter with another and produces a new decorator.
     *
     * @param next Next decorator.
     * @return A new decorator.
     */
    default Filter then(@NonNull Filter next) {
      return new ThenFilter(this, next);
    }

    /**
     * Chain this filter with a handler and produces a new handler.
     *
     * @param next Next handler.
     * @return A new handler.
     */
    default Handler then(@NonNull Handler next) {
      return new ThenHandler(this, next);
    }
  }

  private record ThenFilter(Filter filter, Filter next) implements Filter {
    @Override
    public Handler apply(@NonNull Handler handler) {
      return new ThenHandler(filter, next.apply(handler));
    }
  }

  private record ThenHandler(Filter filter, Handler next) implements Handler {
    @Override
    public Object apply(@NonNull Context ctx) throws Exception {
      return filter.apply(next).apply(ctx);
    }

    @Override
    public void setRoute(Route route) {
      filter.setRoute(route);
      next.setRoute(route);
    }
  }

  /** Marker interface for reactive responses. */
  public interface Reactive extends Filter {}

  /**
   * Decorates a handler and run logic before handler is executed.
   *
   * @author edgar
   * @since 2.0.0
   */
  public interface Before extends Filter {

    default @Override Handler apply(@NonNull Handler next) {
      return ctx -> {
        apply(ctx);
        return next.apply(ctx);
      };
    }

    /**
     * Execute application code before next handler.
     *
     * @param ctx Web context.
     * @throws Exception If something goes wrong.
     */
    void apply(@NonNull Context ctx) throws Exception;

    /**
     * Chain this filter with next one and produces a new before filter.
     *
     * @param next Next decorator.
     * @return A new decorator.
     */
    default Before then(@NonNull Before next) {
      return ctx -> {
        apply(ctx);
        if (!ctx.isResponseStarted()) {
          next.apply(ctx);
        }
      };
    }

    /**
     * Chain this decorator with a handler and produces a new handler.
     *
     * @param next Next handler.
     * @return A new handler.
     */
    default Handler then(@NonNull Handler next) {
      return ctx -> {
        apply(ctx);
        if (!ctx.isResponseStarted()) {
          return next.apply(ctx);
        }
        return ctx;
      };
    }
  }

  /**
   * Execute application logic after a response has been generated by a route handler.
   *
   * <p>For functional handler the value is accessible and you are able to modify the response:
   *
   * <pre>{@code
   * {
   *   after((ctx, result) -> {
   *     // Modify response
   *     ctx.setResponseHeader("foo", "bar");
   *     // do something with value:
   *     log.info("{} produces {}", ctx, result);
   *   });
   *
   *   get("/", ctx -> {
   *     return "Functional value";
   *   });
   * }
   * }</pre>
   *
   * For side-effect handler (direct use of send methods, outputstream, writer, etc.) you are not
   * allowed to modify the response or access to the value (value is always <code>null</code>):
   *
   * <pre>{@code
   * {
   *   after((ctx, result) -> {
   *     // Always null:
   *     assertNull(result);
   *
   *     // Response started is set to: true
   *     assertTrue(ctx.isResponseStarted());
   *   });
   *
   *   get("/", ctx -> {
   *     return ctx.send("Side effect");
   *   });
   * }
   * }</pre>
   *
   * @author edgar
   * @since 2.0.0
   */
  public interface After {

    /**
     * Chain this filter with next one and produces a new after filter.
     *
     * @param next Next filter.
     * @return A new filter.
     */
    default After then(@NonNull After next) {
      return (ctx, result, failure) -> {
        next.apply(ctx, result, failure);
        apply(ctx, result, failure);
      };
    }

    /**
     * Execute application logic on a route response.
     *
     * @param ctx Web context.
     * @param result Response generated by route handler.
     * @param failure Uncaught exception generated by route handler.
     * @throws Exception If something goes wrong.
     */
    void apply(@NonNull Context ctx, @Nullable Object result, @Nullable Throwable failure)
        throws Exception;
  }

  /**
   * Listener interface for events that are run at the completion of a request/response cycle (i.e.
   * when the request has been completely read, and the response has been fully written).
   *
   * <p>At this point it is too late to modify the exchange further.
   *
   * <p>Completion listeners are invoked in reverse order.
   *
   * @author edgar
   */
  public interface Complete {
    /**
     * Callback method.
     *
     * @param ctx Read-Only web context.
     * @throws Exception If something goes wrong.
     */
    void apply(@NonNull Context ctx) throws Exception;
  }

  /**
   * Callback that allow to customize a route while the route pipeline is being created. The {@link
   * #setRoute(Route)} is called once at application startup time.
   */
  public interface Aware {
    /**
     * Allows a handler to listen for route metadata.
     *
     * @param route Route metadata.
     */
    default void setRoute(Route route) {}
  }

  /**
   * Route handler here is where the application logic lives.
   *
   * @author edgar
   * @since 2.0.0
   */
  public interface Handler extends Aware {

    /**
     * Execute application code.
     *
     * @param ctx Web context.
     * @return Route response.
     * @throws Exception If something goes wrong.
     */
    Object apply(@NonNull Context ctx) throws Exception;

    /**
     * Chain this after decorator with next and produces a new decorator.
     *
     * @param next Next decorator.
     * @return A new handler.
     */
    default Handler then(@NonNull After next) {
      return ctx -> {
        Throwable cause = null;
        Object value = null;
        try {
          value = apply(ctx);
        } catch (Throwable x) {
          cause = x;
          // Early mark context as errored response code:
          ctx.setResponseCode(ctx.getRouter().errorCode(cause));
        }
        Object result;
        try {
          if (ctx.isResponseStarted()) {
            result = Context.readOnly(ctx);
            next.apply((Context) result, value, cause);
          } else {
            result = value;
            next.apply(ctx, value, cause);
          }
        } catch (Throwable x) {
          result = null;
          if (cause == null) {
            cause = x;
          } else {
            cause.addSuppressed(x);
          }
        }

        if (cause == null) {
          return result;
        } else {
          if (ctx.isResponseStarted()) {
            return ctx;
          } else {
            throw SneakyThrows.propagate(cause);
          }
        }
      };
    }
  }

  /**
   * Route location.
   *
   * @param filename File name.
   * @param line Line.
   */
  public record Location(String filename, int line) {}

  private static final Location NO_LOCATION = new Location("<<Unknown>>", -1);

  /** Handler for {@link StatusCode#NOT_FOUND} responses. */
  public static final Handler NOT_FOUND =
      ctx -> ctx.sendError(new NotFoundException(ctx.getRequestPath()));

  /** Handler for {@link StatusCode#METHOD_NOT_ALLOWED} responses. */
  public static final Handler METHOD_NOT_ALLOWED =
      ctx -> {
        ctx.setResetHeadersOnError(false);
        // Allow header was generated by routing algorithm
        if (ctx.getMethod().equals(Router.OPTIONS)) {
          return ctx.send(StatusCode.OK);
        } else {
          List<String> allow =
              Optional.ofNullable(ctx.getResponseHeader("Allow"))
                  .map(it -> Arrays.asList(it.split(",")))
                  .orElseGet(Collections::emptyList);
          return ctx.sendError(new MethodNotAllowedException(ctx.getMethod(), allow));
        }
      };

  /** Handler for body error decoder responses. */
  public static final Handler FORM_DECODER_HANDLER =
      ctx -> {
        var tooManyFields = (Throwable) ctx.getAttributes().remove("__too_many_fields");
        BadRequestException cause;
        if (tooManyFields != null) {
          cause = new BadRequestException("Too many form fields", tooManyFields);
        } else {
          cause = new BadRequestException("Failed to decode HTTP body");
        }
        return ctx.sendError(cause);
      };

  /** Handler for {@link StatusCode#REQUEST_ENTITY_TOO_LARGE} responses. */
  public static final Handler REQUEST_ENTITY_TOO_LARGE =
      ctx ->
          ctx.setResponseCode(StatusCode.REQUEST_ENTITY_TOO_LARGE)
              .sendError(new StatusCodeException(StatusCode.REQUEST_ENTITY_TOO_LARGE));

  /** Handler for {@link StatusCode#NOT_ACCEPTABLE} responses. */
  public static final Route.Before ACCEPT =
      ctx -> {
        List<MediaType> produceTypes = ctx.getRoute().getProduces();
        MediaType contentType = ctx.accept(produceTypes);
        if (contentType == null) {
          throw new NotAcceptableException(ctx.header(Context.ACCEPT).valueOrNull());
        }
        ctx.setDefaultResponseType(contentType);
      };

  /** Handler for {@link StatusCode#UNSUPPORTED_MEDIA_TYPE} responses. */
  public static final Route.Before SUPPORT_MEDIA_TYPE =
      ctx -> {
        if (!ctx.isPreflight()) {
          MediaType contentType = ctx.getRequestType();
          if (contentType == null) {
            throw new UnsupportedMediaType(null);
          }
          if (ctx.getRoute().getConsumes().stream().noneMatch(contentType::matches)) {
            throw new UnsupportedMediaType(contentType.getValue());
          }
        }
      };

  /**
   * Carry metadata for mvc/controller method.
   *
   * @param declaringClass Controller class.
   * @param name Method name.
   * @param returnType Method return type.
   * @param parameterTypes Method argument types.
   */
  public record MvcMethod(
      @NonNull Class<?> declaringClass,
      @NonNull String name,
      @NonNull Class<?> returnType,
      Class<?>... parameterTypes) {

    /**
     * Convert to {@link java.lang.reflect.Method}.
     *
     * @return A {@link java.lang.reflect.Method}.
     */
    public Method toMethod() {
      try {
        return declaringClass.getDeclaredMethod(name, parameterTypes);
      } catch (NoSuchMethodException e) {
        throw SneakyThrows.propagate(e);
      }
    }

    /**
     * Convert to {@link MethodHandle}.
     *
     * @param lookup Lookup to use.
     * @return A {@link MethodHandle}.
     */
    public MethodHandle toMethodHandle(MethodHandles.Lookup lookup) {
      try {
        return lookup.unreflect(toMethod());
      } catch (IllegalAccessException e) {
        throw SneakyThrows.propagate(e);
      }
    }

    /**
     * Convert to {@link MethodHandle} using a public lookup.
     *
     * @return A {@link MethodHandle}.
     */
    public MethodHandle toMethodHandle() {
      return toMethodHandle(MethodHandles.publicLookup());
    }
  }

  /** Favicon handler as a silent 404 error. */
  public static final Handler FAVICON = ctx -> ctx.send(StatusCode.NOT_FOUND);

  private static final List EMPTY_LIST = Collections.emptyList();

  private static final Map EMPTY_MAP = Collections.emptyMap();

  private Map<String, MessageDecoder> decoders = EMPTY_MAP;

  private final String pattern;

  private final String method;

  private List<String> pathKeys = EMPTY_LIST;

  private Filter filter;

  private Handler handler;

  private After after;

  private Handler pipeline;

  private MessageEncoder encoder;

  private List<MediaType> produces = EMPTY_LIST;

  private List<MediaType> consumes = EMPTY_LIST;

  private Map<String, Object> attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

  private java.util.Set<String> supportedMethod;

  private String executorKey;

  private List<String> tags = EMPTY_LIST;

  private String summary;

  private String description;

  private Boolean nonBlocking;

  private MvcMethod mvcMethod;

  private boolean httpHead;

  private final Location location;

  /**
   * Creates a new route.
   *
   * @param method HTTP method.
   * @param pattern Path pattern.
   * @param handler Route handler.
   */
  public Route(@NonNull String method, @NonNull String pattern, @NonNull Handler handler) {
    this.method = method.toUpperCase();
    this.pattern = pattern;
    this.handler = handler;
    this.location =
        StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
            .walk(
                frame ->
                    frame
                        .dropWhile(
                            f ->
                                Stream.of(
                                        Route.class.getName(),
                                        RouterImpl.class.getName(),
                                        Router.class.getName(),
                                        Jooby.class.getName(),
                                        "io.jooby.kt.Kooby",
                                        "io.jooby.kt.CoroutineRouter")
                                    .anyMatch(it -> it.equals(f.getDeclaringClass().getName())))
                        .findFirst()
                        .map(it -> new Location(it.getFileName(), it.getLineNumber()))
                        .orElse(NO_LOCATION));
  }

  /**
   * Where the route was defined.
   *
   * @return Where the route was defined.
   */
  public Location getLocation() {
    return location;
  }

  /**
   * Path pattern.
   *
   * @return Path pattern.
   */
  public String getPattern() {
    return pattern;
  }

  /**
   * HTTP method.
   *
   * @return HTTP method.
   */
  public String getMethod() {
    return method;
  }

  /**
   * Path keys.
   *
   * @return Path keys.
   */
  public List<String> getPathKeys() {
    return pathKeys;
  }

  /**
   * Set path keys.
   *
   * @param pathKeys Path keys or empty list.
   * @return This route.
   */
  public Route setPathKeys(@NonNull List<String> pathKeys) {
    this.pathKeys = pathKeys;
    return this;
  }

  /**
   * Route handler.
   *
   * @return Route handler.
   */
  public Handler getHandler() {
    return handler;
  }

  /**
   * Route pipeline.
   *
   * @return Route pipeline.
   */
  public Handler getPipeline() {
    if (pipeline == null) {
      pipeline = computePipeline();
    }
    return pipeline;
  }

  /**
   * Recreate a path pattern using the given variables. <code>
   * reserve(/{k1}/{k2}, {"k1": ""foo", "k2": "bar"}) =&gt; /foo/bar</code>
   *
   * @param keys Path keys.
   * @return Path.
   */
  public String reverse(@NonNull Map<String, Object> keys) {
    return Router.reverse(getPattern(), keys);
  }

  /**
   * Recreate a path pattern using the given variables. <code>
   * reserve(/{k1}/{k2}, "foo", "bar") =&gt; /foo/bar</code>
   *
   * @param values Values.
   * @return Path.
   */
  public String reverse(Object... values) {
    return Router.reverse(getPattern(), values);
  }

  /**
   * After filter or <code>null</code>.
   *
   * @return After filter or <code>null</code>.
   */
  public @Nullable After getAfter() {
    return after;
  }

  /**
   * Set after filter.
   *
   * @param after After filter.
   * @return This route.
   */
  public Route setAfter(@NonNull After after) {
    this.after = after;
    return this;
  }

  /**
   * Decorator or <code>null</code>.
   *
   * @return Decorator or <code>null</code>.
   */
  public @Nullable Filter getFilter() {
    return filter;
  }

  /**
   * Set route filter.
   *
   * @param filter Filter.
   * @return This route.
   */
  public Route setFilter(@Nullable Filter filter) {
    this.filter = filter;
    return this;
  }

  /**
   * Set route pipeline. This method is part of public API but isn't intended to be used by public.
   *
   * @param pipeline Pipeline.
   * @return This routes.
   */
  public Route setPipeline(Route.Handler pipeline) {
    this.pipeline = pipeline;
    return this;
  }

  /**
   * Route encoder.
   *
   * @return Route encoder.
   */
  public MessageEncoder getEncoder() {
    return encoder;
  }

  /**
   * Set encoder.
   *
   * @param encoder MessageEncoder.
   * @return This route.
   */
  public Route setEncoder(@NonNull MessageEncoder encoder) {
    this.encoder = encoder;
    return this;
  }

  /**
   * Truth when route is non-blocking. False otherwise. Blocking code isn't allowed for non-blocking
   * routes.
   *
   * <p>Default is <code>blocking</code> or <code>non-blocking=false</code>. Except when you startup
   * your application using {@link ExecutionMode#EVENT_LOOP}.
   *
   * @return Truth when route is non-blocking. False otherwise or <code>null</code> otherwise.
   */
  public boolean isNonBlocking() {
    return nonBlocking == Boolean.TRUE;
  }

  /**
   * Test if the {@link #isNonBlocking()} flag was set or not. Internal use only.
   *
   * @return Test if the {@link #isNonBlocking()} flag was set or not. Internal use only.
   */
  public boolean isNonBlockingSet() {
    return nonBlocking != null;
  }

  /**
   * Set when the route is blocking or non-blocking.
   *
   * <p>Default is <code>blocking</code> or <code>non-blocking=false</code>. Except when you startup
   * your application using {@link ExecutionMode#EVENT_LOOP}.
   *
   * @param nonBlocking True for non-blocking routes.
   * @return This route.
   */
  public Route setNonBlocking(boolean nonBlocking) {
    this.nonBlocking = nonBlocking;
    return this;
  }

  /**
   * Response types (format) produces by this route. If set, we expect to find a match in the <code>
   * Accept</code> header. If none matches, we send a {@link StatusCode#NOT_ACCEPTABLE} response.
   *
   * @return Immutable list of produce types.
   */
  public List<MediaType> getProduces() {
    return produces;
  }

  /**
   * Add one or more response types (format) produces by this route.
   *
   * @param produces Produce types.
   * @return This route.
   */
  public Route produces(@NonNull MediaType... produces) {
    return setProduces(Arrays.asList(produces));
  }

  /**
   * Add one or more response types (format) produces by this route.
   *
   * @param produces Produce types.
   * @return This route.
   */
  public Route setProduces(@NonNull Collection<MediaType> produces) {
    if (!produces.isEmpty()) {
      if (this.produces == EMPTY_LIST) {
        this.produces = new ArrayList<>();
      }
      this.produces.addAll(produces);
    }
    return this;
  }

  /**
   * Request types (format) consumed by this route. If set the <code>Content-Type</code> header is
   * checked against these values. If none matches we send a {@link
   * StatusCode#UNSUPPORTED_MEDIA_TYPE} exception.
   *
   * @return Immutable list of consumed types.
   */
  public List<MediaType> getConsumes() {
    return consumes;
  }

  /**
   * Add one or more request types (format) consumed by this route.
   *
   * @param consumes Consume types.
   * @return This route.
   */
  public Route consumes(@NonNull MediaType... consumes) {
    return setConsumes(Arrays.asList(consumes));
  }

  /**
   * Add one or more request types (format) consumed by this route.
   *
   * @param consumes Consume types.
   * @return This route.
   */
  public Route setConsumes(@NonNull Collection<MediaType> consumes) {
    if (!consumes.isEmpty()) {
      if (this.consumes == EMPTY_LIST) {
        this.consumes = new ArrayList<>();
      }
      this.consumes.addAll(consumes);
    }
    return this;
  }

  /**
   * Attributes set to this route.
   *
   * @return Map of attributes set to the route.
   */
  public Map<String, Object> getAttributes() {
    return attributes;
  }

  /**
   * Retrieve value of this specific Attribute set to this route.
   *
   * @param name of the attribute to retrieve.
   * @param <T> Generic type.
   * @return value of the specific attribute.
   */
  public @Nullable <T> T getAttribute(@NonNull String name) {
    //noinspection unchecked
    return (T) attributes.get(name);
  }

  /**
   * Add one or more attributes applied to this route.
   *
   * @param attributes .
   * @return This route.
   */
  public Route setAttributes(@NonNull Map<String, Object> attributes) {
    this.attributes.putAll(attributes);
    return this;
  }

  /**
   * Add one or more attributes applied to this route.
   *
   * @param name attribute name
   * @param value attribute value
   * @return This route.
   */
  public Route setAttribute(@NonNull String name, @NonNull Object value) {
    if (this.attributes == EMPTY_MAP) {
      this.attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    }

    this.attributes.put(name, value);

    return this;
  }

  /**
   * MessageDecoder for given media type.
   *
   * @param contentType Media type.
   * @return MessageDecoder.
   */
  public MessageDecoder decoder(@NonNull MediaType contentType) {
    return decoders.getOrDefault(contentType.getValue(), MessageDecoder.UNSUPPORTED_MEDIA_TYPE);
  }

  /**
   * Route message decoder.
   *
   * @return Message decoders.
   */
  public Map<String, MessageDecoder> getDecoders() {
    return decoders;
  }

  /**
   * Set message decoders. Map key is a mime-type.
   *
   * @param decoders message decoder.
   * @return This route.
   */
  public Route setDecoders(@NonNull Map<String, MessageDecoder> decoders) {
    this.decoders = decoders;
    return this;
  }

  /**
   * True if route support HTTP OPTIONS.
   *
   * @return True if route support HTTP OPTIONS.
   */
  public boolean isHttpOptions() {
    return isHttpMethod(Router.OPTIONS);
  }

  /**
   * True if route support HTTP TRACE.
   *
   * @return True if route support HTTP TRACE.
   */
  public boolean isHttpTrace() {
    return isHttpMethod(Router.TRACE);
  }

  /**
   * True if route support HTTP HEAD.
   *
   * @return True if route support HTTP HEAD.
   */
  public boolean isHttpHead() {
    return httpHead;
  }

  /**
   * Enabled or disabled HTTP Options.
   *
   * @param enabled Enabled or disabled HTTP Options.
   * @return This route.
   */
  public Route setHttpOptions(boolean enabled) {
    addHttpMethod(enabled, Router.OPTIONS);
    return this;
  }

  /**
   * Enabled or disabled HTTP TRACE.
   *
   * @param enabled Enabled or disabled HTTP TRACE.
   * @return This route.
   */
  public Route setHttpTrace(boolean enabled) {
    addHttpMethod(enabled, Router.TRACE);
    return this;
  }

  /**
   * Enabled or disabled HTTP HEAD.
   *
   * @param enabled Enabled or disabled HTTP HEAD.
   * @return This route.
   */
  public Route setHttpHead(boolean enabled) {
    addHttpMethod(enabled, Router.HEAD);
    this.httpHead = enabled;
    return this;
  }

  /**
   * Specify the name of the executor where the route is going to run. Default is <code>null</code>.
   *
   * @return Executor key.
   */
  public @Nullable String getExecutorKey() {
    return executorKey;
  }

  /**
   * Set executor key. The route is going to use the given key to fetch an executor. Possible values
   * are:
   *
   * <p>- <code>null</code>: no specific executor, uses the default Jooby logic to choose one, based
   * on the value of {@link ExecutionMode}; - <code>worker</code>: use the executor provided by the
   * server. - <code>arbitrary name</code>: use an named executor which as registered using {@link
   * Router#executor(String, Executor)}.
   *
   * @param executorKey Executor key.
   * @return This route.
   */
  public Route setExecutorKey(@Nullable String executorKey) {
    this.executorKey = executorKey;
    return this;
  }

  /**
   * Route tags.
   *
   * @return Route tags.
   */
  public List<String> getTags() {
    return tags;
  }

  /**
   * Tag this route. Tags are used for documentation purpose from openAPI generator.
   *
   * @param tags Tags.
   * @return This route.
   */
  public Route setTags(@NonNull List<String> tags) {
    if (this.tags == EMPTY_LIST) {
      this.tags = new ArrayList<>();
    }
    this.tags.addAll(tags);
    return this;
  }

  /**
   * Add a tag to this route.
   *
   * <p>Tags are used for documentation purpose from openAPI generator.
   *
   * @param tag Tag.
   * @return This route.
   */
  public Route addTag(@NonNull String tag) {
    if (this.tags == EMPTY_LIST) {
      this.tags = new ArrayList<>();
    }
    this.tags.add(tag);
    return this;
  }

  /**
   * Tag this route. Tags are used for documentation purpose from openAPI generator.
   *
   * @param tags Tags.
   * @return This route.
   */
  public Route tags(@NonNull String... tags) {
    return setTags(Arrays.asList(tags));
  }

  /**
   * Route summary useful for documentation purpose from openAPI generator.
   *
   * @return Summary.
   */
  public @Nullable String getSummary() {
    return summary;
  }

  /**
   * Route summary useful for documentation purpose from openAPI generator.
   *
   * @param summary Summary.
   * @return This route.
   */
  public Route summary(@Nullable String summary) {
    return setSummary(summary);
  }

  /**
   * Route summary useful for documentation purpose from openAPI generator.
   *
   * @param summary Summary.
   * @return This route.
   */
  public Route setSummary(@Nullable String summary) {
    this.summary = summary;
    return this;
  }

  /**
   * Route description useful for documentation purpose from openAPI generator.
   *
   * @return Route description.
   */
  public @Nullable String getDescription() {
    return description;
  }

  /**
   * Route description useful for documentation purpose from openAPI generator.
   *
   * @param description Description.
   * @return This route.
   */
  public Route setDescription(@Nullable String description) {
    this.description = description;
    return this;
  }

  /**
   * Route description useful for documentation purpose from openAPI generator.
   *
   * @param description Description.
   * @return This route.
   */
  public Route description(@Nullable String description) {
    return setDescription(description);
  }

  /**
   * Returns whether this route is marked as transactional, or returns {@code defaultValue} if this
   * route has not been marked explicitly.
   *
   * @param defaultValue the value to return if this route was not explicitly marked
   * @return whether this route should be considered as transactional
   */
  public boolean isTransactional(boolean defaultValue) {
    Object attribute = getAttribute(Transactional.ATTRIBUTE);

    if (attribute == null) {
      return defaultValue;
    }

    if (attribute instanceof Boolean) {
      return (Boolean) attribute;
    }

    throw new RuntimeException(
        "Invalid value for route attribute " + Transactional.ATTRIBUTE + ": " + attribute);
  }

  /**
   * Method for MVC/Controller available when <code>jooby.mvcMethod</code> processor option is set
   * to <code>true</code>. Not available for lambda routes.
   *
   * @return Method for MVC/Controller. Not available for lambda routes.
   */
  public @Nullable MvcMethod getMvcMethod() {
    return mvcMethod;
  }

  /**
   * Set mvc/controller method.
   *
   * @param mvcMethod Mvc/controller method.
   * @return This route
   */
  public Route setMvcMethod(@Nullable MvcMethod mvcMethod) {
    this.mvcMethod = mvcMethod;
    return this;
  }

  /**
   * Set mvc/controller method.
   *
   * @param mvcMethod Mvc/controller method.
   * @return This route
   */
  public Route mvcMethod(@Nullable MvcMethod mvcMethod) {
    return setMvcMethod(mvcMethod);
  }

  @Override
  public String toString() {
    return method + " " + pattern;
  }

  private boolean isHttpMethod(String httpMethod) {
    return supportedMethod != null && supportedMethod.contains(httpMethod);
  }

  private void addHttpMethod(boolean enabled, String httpMethod) {
    if (supportedMethod == null) {
      supportedMethod = new HashSet<>();
    }
    if (enabled) {
      supportedMethod.add(httpMethod);
    } else {
      supportedMethod.remove(httpMethod);
    }
  }

  private Route.Handler computePipeline() {
    Route.Handler pipeline = filter == null ? handler : filter.then(handler);

    if (after != null) {
      pipeline = pipeline.then(after);
    }
    return pipeline;
  }

  /**
   * Give you access to all routes created inside a {@link Router#path(String, Runnable)}. Allow
   * globally applying attributes or metadata.
   *
   * @author edgar
   * @since 2.7.3
   */
  public static class Set implements Iterable<Route> {

    private List<Route> routes;

    private List<String> tags;

    private String summary;

    private String description;

    /**
     * Set of routes.
     *
     * @param routes Routes.
     */
    public Set(List<Route> routes) {
      this.routes = routes;
    }

    /**
     * Sub-routes. Always empty except when used it from {@link Router#path(String, Runnable)} or
     * {@link Router#routes(Runnable)}.
     *
     * @return Sub-routes.
     */
    public List<Route> getRoutes() {
      return routes;
    }

    /**
     * Set sub-routes.
     *
     * @param routes Sub-routes.
     * @return This route.
     */
    public Set setRoutes(@NonNull List<Route> routes) {
      this.routes = routes;
      return this;
    }

    /**
     * Add one or more response types (format) produces by this route.
     *
     * @param produces Produce types.
     * @return This route.
     */
    public Set produces(@NonNull MediaType... produces) {
      return setProduces(Arrays.asList(produces));
    }

    /**
     * Add one or more response types (format) produces by this route.
     *
     * @param produces Produce types.
     * @return This route.
     */
    public Set setProduces(@NonNull Collection<MediaType> produces) {
      routes.forEach(
          it -> {
            if (it.getProduces().isEmpty()) {
              it.setProduces(produces);
            }
          });
      return this;
    }

    /**
     * Add one or more request types (format) consumed by this route.
     *
     * @param consumes Consume types.
     * @return This route.
     */
    public Set consumes(@NonNull MediaType... consumes) {
      return setConsumes(Arrays.asList(consumes));
    }

    /**
     * Add one or more request types (format) consumed by this route.
     *
     * @param consumes Consume types.
     * @return This route.
     */
    public Set setConsumes(@NonNull Collection<MediaType> consumes) {
      routes.forEach(
          it -> {
            if (it.getConsumes().isEmpty()) {
              it.setConsumes(consumes);
            }
          });
      return this;
    }

    /**
     * Add one or more attributes applied to this route.
     *
     * @param attributes .
     * @return This route.
     */
    public Set setAttributes(@NonNull Map<String, Object> attributes) {
      routes.forEach(it -> attributes.forEach((k, v) -> it.getAttributes().putIfAbsent(k, v)));
      return this;
    }

    /**
     * Add one or more attributes applied to this route.
     *
     * @param name attribute name
     * @param value attribute value
     * @return This route.
     */
    public Set setAttribute(@NonNull String name, @NonNull Object value) {
      routes.forEach(it -> it.getAttributes().putIfAbsent(name, value));
      return this;
    }

    /**
     * Set executor key. The route is going to use the given key to fetch an executor. Possible
     * values are:
     *
     * <p>- <code>null</code>: no specific executor, uses the default Jooby logic to choose one,
     * based on the value of {@link ExecutionMode}; - <code>worker</code>: use the executor provided
     * by the server. - <code>arbitrary name</code>: use an named executor which as registered using
     * {@link Router#executor(String, Executor)}.
     *
     * @param executorKey Executor key.
     * @return This route.
     */
    public Set setExecutorKey(@Nullable String executorKey) {
      routes.forEach(it -> it.setExecutorKey(ofNullable(it.getExecutorKey()).orElse(executorKey)));
      return this;
    }

    /**
     * Route tags.
     *
     * @return Route tags.
     */
    public List<String> getTags() {
      return tags == null ? List.of() : tags;
    }

    /**
     * Tag this route. Tags are used for documentation purpose from openAPI generator.
     *
     * @param tags Tags.
     * @return This route.
     */
    public Set setTags(@NonNull List<String> tags) {
      this.tags = tags;
      routes.forEach(it -> tags.forEach(it::addTag));
      return this;
    }

    /**
     * Tag this route. Tags are used for documentation purpose from openAPI generator.
     *
     * @param tags Tags.
     * @return This route.
     */
    public Set tags(@NonNull String... tags) {
      return setTags(Arrays.asList(tags));
    }

    /**
     * Route summary useful for documentation purpose from openAPI generator.
     *
     * @return Summary.
     */
    public @Nullable String getSummary() {
      return summary;
    }

    /**
     * Route summary useful for documentation purpose from openAPI generator.
     *
     * @param summary Summary.
     * @return This route.
     */
    public Set summary(@Nullable String summary) {
      return setSummary(summary);
    }

    /**
     * Route summary useful for documentation purpose from openAPI generator.
     *
     * @param summary Summary.
     * @return This route.
     */
    public Set setSummary(@Nullable String summary) {
      this.summary = summary;
      return this;
    }

    /**
     * Route description useful for documentation purpose from openAPI generator.
     *
     * @return Route description.
     */
    public @Nullable String getDescription() {
      return description;
    }

    /**
     * Route description useful for documentation purpose from openAPI generator.
     *
     * @param description Description.
     * @return This route.
     */
    public Set setDescription(@Nullable String description) {
      this.description = description;
      return this;
    }

    /**
     * Route description useful for documentation purpose from openAPI generator.
     *
     * @param description Description.
     * @return This route.
     */
    public Set description(@Nullable String description) {
      return setDescription(description);
    }

    @Override
    public Iterator<Route> iterator() {
      return routes.iterator();
    }
  }
}
