/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package software.amazon.smithy.rust.codegen.client.smithy.generators.client

import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.rust.codegen.client.smithy.ClientCodegenContext
import software.amazon.smithy.rust.codegen.core.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.core.rustlang.docsTemplate
import software.amazon.smithy.rust.codegen.core.rustlang.writable
import software.amazon.smithy.rust.codegen.core.util.inputShape
import software.amazon.smithy.rust.codegen.core.util.serviceNameOrDefault

object FluentClientDocs {
    fun clientConstructionDocs(codegenContext: ClientCodegenContext) = writable {
        val serviceName = codegenContext.serviceShape.serviceNameOrDefault("the service")
        val moduleUseName = codegenContext.moduleUseName()
        docsTemplate(
            """
            Client for calling $serviceName.

            #### Client types and purposes

            Clients generated by smithy-rs have two separate client layers:

            1. The Smithy [`Client`](#{aws_smithy_client}::Client): A _lower level client_ that is not tied
            to the service and directly operates on operation structs. A Smithy client is composed of a
            connector, middleware, and retry strategy.
            2. The "fluent" [`Client`]: A _higher level client_ that is convenient to use.

            The vast majority of use-cases don't require using the Smithy client, so it is usually only relevant
            for client construction. Because of this, this documentation will refer to the fluent client as simply
            the "client". The Smithy client is only relevant when customizing the connector, middleware, or
            retry strategy, or for even more advanced use cases.

            #### Constructing a `Client`

            A fluent [`Client`] is composed of a Smithy client and service configuration. In order to
            construct one, a Smithy client must be created first, and this is done using
            the [`client::Builder`](crate::client::Builder) struct:

            ```rust,no_run
            let smithy_client = $moduleUseName::client::Builder::new()
                // Use the default HTTPS connector
                .dyn_https_connector(Default::default())
                // Use a no-op middleware
                .middleware_fn(|request| request)
                // Build a type-erased Smithy client
                .build_dyn();
            ```

            The client builder has generics `C`, `M`, and `R` representing the connector, middleware, and
            retry strategy:

            - A connector (`C`) specifies how HTTP requests are translated into HTTP responses. This will typically be
            an HTTP client (like `hyper`), but you can also provide your own, like a mock connector for testing.
            - Middleware (`M`) modifies requests prior to them being sent to the service. Most commonly, middleware decide
            what endpoint the requests should be sent to, as well as perform authentication and authorization (such
            as HTTP basic auth or AWS SigV4). You can also have middleware that performs request/response tracing,
            throttling, or other middleware-like tasks.
            - A retry strategy (`R`) dictates the behavior for requests that fail. The default,
            [`RetryMode::Standard`](aws_smithy_types::retry::RetryMode::Standard) is generally what you want since
            it provides a well-vetted exponential backoff retry implementation.

            Once the Smithy client is created, a service config and fluent client can be created. Generally, you
            want to call [`Client::with_config`], which takes a Smithy client and the service [`Config`](crate::Config).
            The config is constructed using the [builder pattern], and has several builder methods to further
            customize the client.

            In _most_ circumstances, you will want to use the following pattern to construct a client:

            ```rust,no_run
            let smithy_client = $moduleUseName::client::Builder::new()
                .dyn_https_connector(Default::default())
            ##  /*
                .middleware(/* discussed below */)
            ##  */
            ##  .middleware_fn(|r| r)
                .build_dyn();

            let config = $moduleUseName::Config::builder().build();
            let client = $moduleUseName::Client::with_config(smithy_client, config);
            ```

            _Note:_ Client construction is expensive due to connection thread pool initialization, and should be done
            once at application start-up.

            For middleware, you'll want to use whatever matches the routing, authentication, and authorization
            required by the target service. For example, for the AWS SDK which uses [SigV4-signed requests], the
            middleware looks like this:

            ```rust,ignore
            use aws_endpoint::AwsEndpointStage;
            use aws_http::auth::CredentialsStage;
            use aws_http::recursion_detection::RecursionDetectionStage;
            use aws_http::user_agent::UserAgentStage;
            use aws_sig_auth::middleware::SigV4SigningStage;
            use aws_sig_auth::signer::SigV4Signer;
            use aws_smithy_client::retry::Config as RetryConfig;
            use aws_smithy_http_tower::map_request::{AsyncMapRequestLayer, MapRequestLayer};
            use std::fmt::Debug;
            use tower::layer::util::{Identity, Stack};
            use tower::ServiceBuilder;

            type AwsMiddlewareStack = Stack<
                MapRequestLayer<RecursionDetectionStage>,
                Stack<
                    MapRequestLayer<SigV4SigningStage>,
                    Stack<
                        AsyncMapRequestLayer<CredentialsStage>,
                        Stack<
                            MapRequestLayer<UserAgentStage>,
                            Stack<MapRequestLayer<AwsEndpointStage>, Identity>,
                        >,
                    >,
                >,
            >;

            /// AWS Middleware Stack
            ///
            /// This implements the middleware stack for this service. It will:
            /// 1. Load credentials asynchronously into the property bag
            /// 2. Sign the request with SigV4
            /// 3. Resolve an Endpoint for the request
            /// 4. Add a user agent to the request
            ##[derive(Debug, Default, Clone)]
            ##[non_exhaustive]
            pub struct AwsMiddleware;

            impl AwsMiddleware {
                /// Create a new `AwsMiddleware` stack
                ///
                /// Note: `AwsMiddleware` holds no state.
                pub fn new() -> Self {
                    AwsMiddleware::default()
                }
            }

            // define the middleware stack in a non-generic location to reduce code bloat.
            fn base() -> ServiceBuilder<AwsMiddlewareStack> {
                let credential_provider = AsyncMapRequestLayer::for_mapper(CredentialsStage::new());
                let signer = MapRequestLayer::for_mapper(SigV4SigningStage::new(SigV4Signer::new()));
                let endpoint_resolver = MapRequestLayer::for_mapper(AwsEndpointStage);
                let user_agent = MapRequestLayer::for_mapper(UserAgentStage::new());
                let recursion_detection = MapRequestLayer::for_mapper(RecursionDetectionStage::new());
                // These layers can be considered as occurring in order, that is:
                // 1. Resolve an endpoint
                // 2. Add a user agent
                // 3. Acquire credentials
                // 4. Sign with credentials
                // (5. Dispatch over the wire)
                ServiceBuilder::new()
                    .layer(endpoint_resolver)
                    .layer(user_agent)
                    .layer(credential_provider)
                    .layer(signer)
                    .layer(recursion_detection)
            }

            impl<S> tower::Layer<S> for AwsMiddleware {
                type Service = <AwsMiddlewareStack as tower::Layer<S>>::Service;

                fn layer(&self, inner: S) -> Self::Service {
                    base().service(inner)
                }
            }
            ```

            [builder pattern]: https://rust-lang.github.io/api-guidelines/type-safety.html##builders-enable-construction-of-complex-values-c-builder
            [SigV4-signed requests]: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html""${'"'},
            """.trimIndent(),
            "aws_smithy_client" to CargoDependency.smithyClient(codegenContext.runtimeConfig).toDevDependency().toType(),
        )
    }

    fun clientUsageDocs(codegenContext: ClientCodegenContext) = writable {
        val model = codegenContext.model
        val symbolProvider = codegenContext.symbolProvider
        if (model.operationShapes.isNotEmpty()) {
            // Find an operation with a simple string member shape
            val (operation, member) = codegenContext.serviceShape.operations
                .map { id ->
                    val operationShape = model.expectShape(id, OperationShape::class.java)
                    val member = operationShape.inputShape(model)
                        .members()
                        .firstOrNull { model.expectShape(it.target) is StringShape }
                    operationShape to member
                }
                .sortedBy { it.first.id }
                .firstOrNull { (_, member) -> member != null } ?: (null to null)
            if (operation != null && member != null) {
                val operationSymbol = symbolProvider.toSymbol(operation)
                val memberSymbol = symbolProvider.toSymbol(member)
                val operationFnName = FluentClientGenerator.clientOperationFnName(operation, symbolProvider)
                docsTemplate(
                    """
                    ## Using the `Client`

                    A client has a function for every operation that can be performed by the service.
                    For example, the [`${operationSymbol.name}`](${operationSymbol.namespace}) operation has
                    a [`Client::$operationFnName`], function which returns a builder for that operation.
                    The fluent builder ultimately has a `call()` function that returns an async future that
                    returns a result, as illustrated below:

                    ```rust,ignore
                    let result = client.$operationFnName()
                        .${memberSymbol.name}("example")
                        .call()
                        .await;
                    ```

                    The underlying HTTP requests that get made by this can be modified with the `customize_operation`
                    function on the fluent builder. See the [`customize`](crate::client::customize) module for more
                    information.
                    """.trimIndent(),
                    "operation" to operationSymbol,
                )
            }
        }
    }
}
