"""
Generate the `src/services/{service}.jl` file.
"""
function _generate_high_level_wrapper(
    service_files::AbstractArray{ServiceFile}, auth::GitHub.OAuth2
)
    service_dir = joinpath(@__DIR__, "..", "services")

    # Remove old service files to ensure services that no longer exist are removed.
    for file in readdir(service_dir)
        path = joinpath(service_dir, file)
        if endswith(path, ".jl")
            rm(path)
        end
    end

    Threads.@threads for service_file in service_files
        service_name = service_file.name
        @info "Generating high level wrapper for $service_name"
        service = service_definition(service_file; auth=auth)

        service_name = lowercase(service["metadata"]["serviceId"])
        service_name = replace(service_name, ' ' => '_')
        operations = service["operations"]
        shapes = service["shapes"]

        protocol = service["metadata"]["protocol"]

        operations = sort!(
            _generate_high_level_definitions(service_name, protocol, operations, shapes)
        )

        service_path = joinpath(service_dir, "$service_name.jl")
        open(service_path, "w") do f
            println(
                f,
                """
             # This file is auto-generated by AWSMetadata.jl
             using AWS
             using AWS.AWSServices: $service_name
             using AWS.Compat
             using AWS.UUIDs
             """,
            )
            join(f, operations, "\n")
        end
    end
end

"""
Generate high-level definitions for the `service`.
All high-level definitions and documentation to be written into `services/{Service}.jl`
"""
function _generate_high_level_definitions(
    service_name::String, protocol::String, operations::AbstractDict, shapes::AbstractDict
)
    operation_definitions = String[]

    for (_, operation) in operations
        name = operation["name"]
        method = operation["http"]["method"]
        request_uri = operation["http"]["requestUri"]

        documentation = ""

        if haskey(operation, "documentation")
            documentation = _clean_documentation(operation["documentation"])
        end

        required_parameters = Dict{String,Any}()
        optional_parameters = Dict{String,Any}()

        if haskey(operation, "input")
            required_parameters, optional_parameters = _get_function_parameters(
                operation["input"]["shape"], shapes
            )
        end

        operation_definition = _generate_high_level_definition(
            service_name,
            protocol,
            name,
            method,
            request_uri,
            required_parameters,
            optional_parameters,
            documentation,
        )

        push!(operation_definitions, operation_definition)
    end

    return operation_definitions
end

"""
Generate the high-level definition for a services function.
"""
function _generate_high_level_definition(
    service_name::String,
    protocol::String,
    name::String,
    method::String,
    request_uri::String,
    required_parameters::AbstractDict,
    optional_parameters::AbstractDict,
    documentation::String,
)
    """
    Generate function definition for a service request given required, header and idempotent parameters.
    """
    function _generate_rest_operation_defintion(
        required_params::AbstractDict,
        optional_params::AbstractDict,
        function_name::String,
        service_name::String,
        method::String,
        request_uri::String,
    )
        request_uri = replace(request_uri, '{' => "\$(")  # Replace { with $(
        request_uri = replace(request_uri, '}' => ')')  # Replace } with )
        request_uri = replace(request_uri, '+' => "")  # Remove + from the request URI

        # Pre Julia-1.3 workaround
        req_keys = [replace(key, "-" => "_") for key in collect(keys(required_params))]

        required_params = filter(p -> (p[2]["location"] != "uri"), required_params)
        header_params = filter(p -> (p[2]["location"] == "header"), required_params)
        required_params = setdiff(required_params, header_params)

        idempotent_params = filter(p -> (p[2]["idempotent"]), optional_params)

        req_kv = ["\"$(p[1])\"=>$(replace(p[1], "-" => "_"))" for p in required_params]
        header_kv = ["\"$(p[1])\"=>$(replace(p[1], "-" => "_"))" for p in header_params]
        idempotent_kv = ["\"$(p[1])\"=>string(uuid4())" for p in idempotent_params]

        required_keys = !isempty(req_keys)
        headers = !isempty(header_params)
        idempotent = !isempty(idempotent_params)

        req_str = !isempty(req_kv) ? "Dict{String, Any}($(join(req_kv, ", "))" : ""
        params_str = if (!isempty(req_kv) || idempotent)
            "$(join(vcat(req_kv, idempotent_kv), ", "))"
        else
            ""
        end
        headers_str =
            headers ? "\"headers\"=>Dict{String, Any}($(join(header_kv, ", ")))" : ""
        params_headers_str = "Dict{String, Any}($(join([s for s in (params_str, headers_str) if !isempty(s)], ", ")))"

        formatted_function_name = _format_name(function_name)

        if required_keys && (idempotent || headers)
            return """
                $formatted_function_name($(join(req_keys, ", ")); aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", $params_headers_str; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name($(join(req_keys, ", ")), params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", Dict{String, Any}(mergewith(_merge, $params_headers_str, params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        elseif !required_keys && (idempotent || headers)
            return """
                $formatted_function_name(; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", $params_headers_str; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name(params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", Dict{String, Any}(mergewith(_merge, $params_headers_str, params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        elseif required_keys && !isempty(req_kv)
            return """
                $formatted_function_name($(join(req_keys, ", ")); aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", $req_str); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name($(join(req_keys, ", ")), params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", Dict{String, Any}(mergewith(_merge, $req_str), params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        elseif required_keys
            return """
                $formatted_function_name($(join(req_keys, ", ")); aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name($(join(req_keys, ", ")), params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        else
            return """
                $formatted_function_name(; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name(params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$method\", \"$request_uri\", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        end
    end

    """
    Generate a JSON/Query high level definition.
    """
    function _generate_json_query_opeation_definition(
        required_params::AbstractDict,
        optional_params::AbstractDict,
        function_name::String,
        service_name::String,
    )
        req_keys = [replace(key, '-' => '_') for key in collect(keys(required_params))]

        idempotent_params = filter(p -> (p[2]["idempotent"]), optional_params)

        req_kv = ["\"$(p[1])\"=>$(replace(p[1], "-" => "_"))" for p in required_params]
        idempotent_kv = ["\"$(p[1])\"=>string(uuid4())" for p in idempotent_params]

        required = !isempty(req_kv)
        idempotent = !isempty(idempotent_kv)

        formatted_function_name = _format_name(function_name)

        if required && idempotent
            return """
                $formatted_function_name($(join(req_keys, ", ")); aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}($(join(req_kv, ", ")), $(join(idempotent_kv, ", "))); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name($(join(req_keys, ", ")), params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}(mergewith(_merge, Dict{String, Any}($(join(req_kv, ", ")), $(join(idempotent_kv, ", "))), params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        elseif required
            return """
                $formatted_function_name($(join(req_keys, ", ")); aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}($(join(req_kv, ", "))); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name($(join(req_keys, ", ")), params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}(mergewith(_merge, Dict{String, Any}($(join(req_kv, ", "))), params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        elseif idempotent
            return """
                $formatted_function_name(; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}($(join(idempotent_kv, ", "))); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name(params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", Dict{String, Any}(mergewith(_merge, Dict{String, Any}($(join(idempotent_kv, ", "))), params)); aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        else
            return """
                $formatted_function_name(; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\"; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                $formatted_function_name(params::AbstractDict{String}; aws_config::AbstractAWSConfig=current_aws_config()) = $service_name(\"$function_name\", params; aws_config=aws_config, feature_set=SERVICE_FEATURE_SET)
                """
        end
    end

    """
    Generate the docstring for the `function_name`.
    """
    function _generate_docstring(
        function_name, documentation, required_parameters, optional_parameters
    )
        function_name = _format_name(function_name)
        args = join((_format_name(key) for (key, val) in required_parameters), ", ")
        maybejoin = isempty(args) ? "" : ", "
        operation_definition = """
            $(repeat('"', 3))
                $function_name($(args))
                $function_name($(args)$(maybejoin)params::Dict{String,<:Any})

            $(_wraplines(documentation))\n
            """

        # Add in the required parameters if applicable
        if !isempty(required_parameters)
            operation_definition *= "# Arguments\n"

            for (required_key, required_value) in required_parameters
                key = _format_name(required_key)
                operation_definition *= _wraplines(
                    "- `$key`: $(required_value["documentation"])"; delim="\n  "
                )
                operation_definition *= "\n"
            end

            operation_definition *= "\n"
        end

        # Add in the optional parameters if applicable
        if !isempty(optional_parameters)
            operation_definition *= """
                # Optional Parameters
                Optional parameters can be passed as a `params::Dict{String,<:Any}`. Valid keys are:
                """

            for (optional_key, optional_value) in optional_parameters
                operation_definition *= _wraplines(
                    "- `\"$optional_key\"`: $(optional_value["documentation"])";
                    delim="\n  ",
                )
                operation_definition *= "\n"
            end
        end

        return operation_definition *= repeat('"', 3)
    end

    doc_string = _generate_docstring(
        name, documentation, required_parameters, optional_parameters
    )

    if protocol in ("json", "query", "ec2")
        function_string = _generate_json_query_opeation_definition(
            required_parameters, optional_parameters, name, service_name
        )
    elseif protocol in ("rest-json", "rest-xml")
        function_string = _generate_rest_operation_defintion(
            required_parameters,
            optional_parameters,
            name,
            service_name,
            method,
            request_uri,
        )
    else
        throw(
            ProtocolNotDefined(
                "$function_name is using a new protocol; $protocol which is not supported."
            ),
        )
    end

    return string(doc_string, '\n', function_string)
end
