/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.flink.connector.http.table.lookup;

import org.apache.flink.annotation.VisibleForTesting;
import org.apache.flink.connector.http.LookupQueryCreator;
import org.apache.flink.connector.http.config.HttpConnectorConfigConstants;
import org.apache.flink.connector.http.preprocessor.HeaderPreprocessor;
import org.apache.flink.connector.http.utils.HttpHeaderUtils;
import org.apache.flink.table.data.RowData;
import org.apache.flink.util.FlinkRuntimeException;

import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;

import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.Builder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;

import static org.apache.flink.connector.http.config.HttpConnectorConfigConstants.PLACEHOLDER_END;
import static org.apache.flink.connector.http.config.HttpConnectorConfigConstants.PLACEHOLDER_START;

/** Base class for {@link HttpRequest} factories. */
@Slf4j
public abstract class RequestFactoryBase implements HttpRequestFactory {

    public static final String DEFAULT_REQUEST_TIMEOUT_SECONDS = "30";

    /** Base url used for {@link HttpRequest} for example "http://localhost:8080". */
    protected final String baseUrl;

    protected final LookupQueryCreator lookupQueryCreator;

    protected final int httpRequestTimeOutSeconds;

    /** HTTP headers that should be used for {@link HttpRequest} created by factory. */
    private final String[] headersAndValues;

    private final HttpLookupConfig options;

    private final HttpClient.Version httpVersion;

    public RequestFactoryBase(
            LookupQueryCreator lookupQueryCreator,
            HeaderPreprocessor headerPreprocessor,
            HttpLookupConfig options) {

        this.baseUrl = options.getUrl();
        this.lookupQueryCreator = lookupQueryCreator;
        this.options = options;
        // note that the OIDC header preprocessor is not setup here, because it
        // issues a network call to the authentication server. This code is driven for
        // explain select. Explain should not issue network calls.
        // We setup the OIDC authentication header at lookup query time.
        var headerMap =
                HttpHeaderUtils.prepareHeaderMap(
                        HttpConnectorConfigConstants.LOOKUP_SOURCE_HEADER_PREFIX,
                        options.getProperties(),
                        headerPreprocessor);

        this.headersAndValues = HttpHeaderUtils.toHeaderAndValueArray(headerMap);

        this.httpRequestTimeOutSeconds =
                Integer.parseInt(
                        options.getProperties()
                                .getProperty(
                                        HttpConnectorConfigConstants.LOOKUP_HTTP_TIMEOUT_SECONDS,
                                        DEFAULT_REQUEST_TIMEOUT_SECONDS));

        String httpVersionFromConfig = null;
        if (options.getReadableConfig() != null) {
            httpVersionFromConfig =
                    options.getReadableConfig().get(HttpLookupConnectorOptions.LOOKUP_HTTP_VERSION);
        }
        if (httpVersionFromConfig == null) {
            httpVersion = null;
        } else {
            httpVersion = HttpClient.Version.valueOf(httpVersionFromConfig);
        }
    }

    @Override
    public HttpLookupSourceRequestEntry buildLookupRequest(RowData lookupRow) {

        LookupQueryInfo lookupQueryInfo = lookupQueryCreator.createLookupQuery(lookupRow);
        getLogger().debug("Created Http lookup query: " + lookupQueryInfo);

        Builder requestBuilder = setUpRequestMethod(lookupQueryInfo);
        if (headersAndValues.length != 0) {
            requestBuilder.headers(headersAndValues);
        }

        return new HttpLookupSourceRequestEntry(requestBuilder.build(), lookupQueryInfo);
    }

    protected abstract Logger getLogger();

    /**
     * Method for preparing {@link Builder} for REST method.
     *
     * @param lookupQuery lookup query used for request query parameters or body.
     * @return {@link Builder} for given lookupQuery.
     */
    protected Builder setUpRequestMethod(LookupQueryInfo lookupQuery) {
        HttpRequest.Builder builder =
                HttpRequest.newBuilder()
                        .timeout(Duration.ofSeconds(this.httpRequestTimeOutSeconds));
        return httpVersion == null ? builder : builder.version(httpVersion);
    }

    protected static StringBuilder resolvePathParameters(
            LookupQueryInfo lookupQueryInfo, StringBuilder resolvedUrl) {
        if (lookupQueryInfo.hasPathBasedUrlParameters()) {
            for (Map.Entry<String, String> entry :
                    lookupQueryInfo.getPathBasedUrlParameters().entrySet()) {
                String pathParam = PLACEHOLDER_START + entry.getKey() + PLACEHOLDER_END;
                int startIndex = resolvedUrl.indexOf(pathParam);
                if (startIndex == -1) {
                    throw new FlinkRuntimeException(
                            "Unexpected error while parsing the URL for path parameters.");
                }
                int endIndex = startIndex + pathParam.length();
                String value = entry.getValue();
                // Don't encode if the entire URL is just a placeholder (e.g., "{url}")
                // Check if resolvedUrl starts with { and ends with }
                String replacementValue;
                if (resolvedUrl.charAt(0) == '{'
                        && resolvedUrl.charAt(resolvedUrl.length() - 1) == '}') {
                    replacementValue = value;
                } else {
                    replacementValue = URLEncoder.encode(value, StandardCharsets.UTF_8);
                }
                resolvedUrl = resolvedUrl.replace(startIndex, endIndex, replacementValue);
            }
        }
        return resolvedUrl;
    }

    @VisibleForTesting
    String[] getHeadersAndValues() {
        return Arrays.copyOf(headersAndValues, headersAndValues.length);
    }
}
