@*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 *@

@*
 * Copyright (C) 2018-2021 Lightbend Inc. <https://www.lightbend.com>
 *@

@(service: org.apache.pekko.grpc.gen.scaladsl.Service, powerApis: Boolean)

@org.apache.pekko.grpc.gen.Constants.DoNotEditComment
package @service.packageName

import scala.concurrent.ExecutionContext

import org.apache.pekko
import pekko.grpc.scaladsl.{ GrpcExceptionHandler, GrpcMarshalling }
import pekko.grpc.Trailers

import pekko.actor.ActorSystem
import pekko.actor.ClassicActorSystemProvider
import pekko.annotation.ApiMayChange
import pekko.http.scaladsl.model
import pekko.stream.{Materializer, SystemMaterializer}

import pekko.grpc.internal.TelemetryExtension

import pekko.grpc.PekkoGrpcGenerated

@{if (powerApis) "import pekko.grpc.scaladsl.MetadataBuilder" else ""}

@defining(if (powerApis) service.name + "PowerApi" else service.name) { serviceName =>
/*
 * Generated by Pekko gRPC. DO NOT EDIT.
 *
 * The API of this class may still change in future Pekko gRPC versions, see for instance
 * https://github.com/akka/akka-grpc/issues/994
 */
@@ApiMayChange
@@PekkoGrpcGenerated
object @{serviceName}Handler {
    private val notFound = scala.concurrent.Future.successful(model.HttpResponse(model.StatusCodes.NotFound))
    private val unsupportedMediaType = scala.concurrent.Future.successful(model.HttpResponse(model.StatusCodes.UnsupportedMediaType))

    /**
     * Creates a `HttpRequest` to `HttpResponse` handler that can be used in for example `Http().bindAndHandleAsync`
     * for the generated partial function handler and ends with `StatusCodes.NotFound` if the request is not matching.
     *
     * Use `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` with `@{service.name}Handler.partial` when combining
     * several services.
     */
    def apply(implementation: @serviceName)(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] =
      handler(implementation, @{service.name}.name, GrpcExceptionHandler.defaultMapper)

    /**
     * Creates a `HttpRequest` to `HttpResponse` handler that can be used in for example `Http().bindAndHandleAsync`
     * for the generated partial function handler and ends with `StatusCodes.NotFound` if the request is not matching.
     *
     * Use `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` with `@{service.name}Handler.partial` when combining
     * several services.
     */
    def apply(implementation: @serviceName, eHandler: ActorSystem => PartialFunction[Throwable, Trailers])(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] =
      handler(implementation, @{service.name}.name, eHandler)

    /**
     * Creates a `HttpRequest` to `HttpResponse` handler that can be used in for example `Http().bindAndHandleAsync`
     * for the generated partial function handler and ends with `StatusCodes.NotFound` if the request is not matching.
     *
     * Use `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` with `@{service.name}Handler.partial` when combining
     * several services.
     *
     * Registering a gRPC service under a custom prefix is not widely supported and strongly discouraged by the specification.
     */
    def apply(implementation: @serviceName, prefix: String)(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] =
      handler(implementation, prefix, GrpcExceptionHandler.defaultMapper)

    /**
     * Creates a `HttpRequest` to `HttpResponse` handler that can be used in for example `Http().bindAndHandleAsync`
     * for the generated partial function handler and ends with `StatusCodes.NotFound` if the request is not matching.
     *
     * Use `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` with `@{service.name}Handler.partial` when combining
     * several services.
     *
     * Registering a gRPC service under a custom prefix is not widely supported and strongly discouraged by the specification.
     */
    def apply(implementation: @serviceName, prefix: String, eHandler: ActorSystem => PartialFunction[Throwable, Trailers])(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] =
      handler(implementation, prefix, eHandler)

@if(serviceName != "ServerReflection") {

    /**
     * Creates a `HttpRequest` to `HttpResponse` handler that can be used in for example `Http().bindAndHandleAsync`
     * for the generated partial function handler. The generated handler falls back to a reflection handler for
     * `@{service.name}` and ends with `StatusCodes.NotFound` if the request is not matching.
     *
     * Use `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` with `@{service.name}Handler.partial` when combining
     * several services.
     */
    def withServerReflection(implementation: @serviceName)(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] =
        pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound(
          @{serviceName}Handler.partial(implementation),
          pekko.grpc.scaladsl.ServerReflection.partial(List(@{service.name})))
}

    private def methodName(request: model.HttpRequest, prefix: String): String =
      request.uri.path match {
        case model.Uri.Path.Slash(model.Uri.Path.Segment(`prefix`, model.Uri.Path.Slash(model.Uri.Path.Segment(method, model.Uri.Path.Empty)))) =>
          method
        case _ =>
          null
      }

    private def handler(implementation: @serviceName, prefix: String, eHandler: ActorSystem => PartialFunction[Throwable, Trailers])(implicit system: ClassicActorSystemProvider): model.HttpRequest => scala.concurrent.Future[model.HttpResponse] = {
      implicit val mat: Materializer = SystemMaterializer(system).materializer
      implicit val ec: ExecutionContext = mat.executionContext
      val spi = TelemetryExtension(system).spi

      import @{service.name}.Serializers.@{service.scalaCompatConstants.WildcardImport}

      def handle(request: model.HttpRequest, method: String): scala.concurrent.Future[model.HttpResponse] =
        GrpcMarshalling.negotiated(request, (reader, writer) =>
          method match {
            @for(method <- service.methods) {
            case "@method.grpcName" =>
                @{if(powerApis) { "val metadata = MetadataBuilder.fromHeaders(request.headers)" } else { "" }}
                @if(method.methodType == org.apache.pekko.grpc.gen.Unary) {
                GrpcMarshalling.handleUnary(request.entity, (e: @method.parameterType) => implementation.@{method.nameSafe}(e@{if(powerApis) { ", metadata" } else { "" }}), eHandler)(
                  @{service.scalaCompatConstants.ImplicitUsing}@method.deserializer.name, @method.serializer.name, mat, reader, writer, system, ec)
                } else {
                @{method.unmarshal}(request.entity)(@{service.scalaCompatConstants.ImplicitUsing}@method.deserializer.name, mat, reader)
                  .@{if(method.outputStreaming) { "map" } else { "flatMap" }}(implementation.@{method.nameSafe}(_@{if(powerApis) { ", metadata" } else { "" }}))
                  .map(e => @{method.marshal}(e, eHandler)(@{service.scalaCompatConstants.ImplicitUsing}@method.serializer.name, writer, system))
                  .recoverWith(GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer))
                }
            }
            case m => GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new NotImplementedError(s"Not implemented: $m"))
          }
      ).getOrElse(unsupportedMediaType)

      request => {
        val method = methodName(request, prefix)
        if (method eq null) notFound
        else handle(spi.onRequest(prefix, method, request), method)
      }
    }

    /**
     * Creates a partial `HttpRequest` to `HttpResponse` handler that can be combined with handlers of other
     * services with `org.apache.pekko.grpc.scaladsl.ServiceHandler.concatOrNotFound` and then used in for example
     * `Http().bindAndHandleAsync`.
     *
     * Use `@{service.name}Handler.apply` if the server is only handling one service.
     *
     * Registering a gRPC service under a custom prefix is not widely supported and strongly discouraged by the specification.
     */
    def partial(implementation: @serviceName, prefix: String = @{service.name}.name, eHandler: ActorSystem => PartialFunction[Throwable, Trailers] = GrpcExceptionHandler.defaultMapper)(implicit system: ClassicActorSystemProvider): PartialFunction[model.HttpRequest, scala.concurrent.Future[model.HttpResponse]] = {
      implicit val mat: Materializer = SystemMaterializer(system).materializer
      implicit val ec: ExecutionContext = mat.executionContext
      val spi = TelemetryExtension(system).spi

      import @{service.name}.Serializers.@{service.scalaCompatConstants.WildcardImport}

      def handle(request: model.HttpRequest, method: String): scala.concurrent.Future[model.HttpResponse] =
        GrpcMarshalling.negotiated(request, (reader, writer) =>
          method match {
            @for(method <- service.methods) {
            case "@method.grpcName" =>
                @{if(powerApis) { "val metadata = MetadataBuilder.fromHeaders(request.headers)" } else { "" }}
                @if(method.methodType == org.apache.pekko.grpc.gen.Unary) {
                GrpcMarshalling.handleUnary(request.entity, (e: @method.parameterType) => implementation.@{method.nameSafe}(e@{if(powerApis) { ", metadata" } else { "" }}), eHandler)(
                  @{service.scalaCompatConstants.ImplicitUsing}@method.deserializer.name, @method.serializer.name, mat, reader, writer, system, ec)
                } else {
                @{method.unmarshal}(request.entity)(@{service.scalaCompatConstants.ImplicitUsing}@method.deserializer.name, mat, reader)
                  .@{if(method.outputStreaming) { "map" } else { "flatMap" }}(implementation.@{method.nameSafe}(_@{if(powerApis) { ", metadata" } else { "" }}))
                  .map(e => @{method.marshal}(e, eHandler)(@{service.scalaCompatConstants.ImplicitUsing}@method.serializer.name, writer, system))
                  .recoverWith(GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer))
                }
            }
            case m => GrpcExceptionHandler.from(eHandler(system.classicSystem))(system, writer)(new NotImplementedError(s"Not implemented: $m"))
          }
      ).getOrElse(unsupportedMediaType)

      Function.unlift((req: model.HttpRequest) => {
        val method = methodName(req, prefix)
        if (method eq null) None
        else Some(handle(spi.onRequest(prefix, method, req), method))
      })
    }
  }
}
