/*
 * 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.camel.component.platform.http.springboot;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.Suspendable;
import org.apache.camel.SuspendableService;
import org.apache.camel.component.platform.http.PlatformHttpEndpoint;
import org.apache.camel.component.platform.http.cookie.CookieConfiguration;
import org.apache.camel.component.platform.http.cookie.CookieHandler;
import org.apache.camel.component.platform.http.spi.PlatformHttpConsumer;
import org.apache.camel.http.common.HttpBinding;
import org.apache.camel.http.common.HttpHelper;
import org.apache.camel.support.DefaultConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ResponseBody;

public class SpringBootPlatformHttpConsumer extends DefaultConsumer implements PlatformHttpConsumer, Suspendable, SuspendableService {

    private static final Logger LOG = LoggerFactory.getLogger(SpringBootPlatformHttpConsumer.class);

    private HttpBinding binding;
    private final boolean handleWriteResponseError;
    private CookieConfiguration cookieConfiguration;
    private Executor executor;

    public SpringBootPlatformHttpConsumer(PlatformHttpEndpoint endpoint, Processor processor) {
        super(endpoint, processor);
        this.binding = new SpringBootPlatformHttpBinding();
        this.binding.setHeaderFilterStrategy(endpoint.getHeaderFilterStrategy());
        this.binding.setMuteException(endpoint.isMuteException());
        this.binding.setFileNameExtWhitelist(endpoint.getFileNameExtWhitelist());
        this.binding.setUseReaderForPayload(!endpoint.isUseStreaming());
        this.handleWriteResponseError = endpoint.isHandleWriteResponseError();
        this.executor = Executors.newSingleThreadExecutor();
    }

    public SpringBootPlatformHttpConsumer(PlatformHttpEndpoint endpoint, Processor processor, Executor executor) {
        this(endpoint, processor);
        this.executor = executor;
    }

    /**
     * Used for testing purposes
     */
    void setBinding(HttpBinding binding) {
        this.binding = binding;
    }

    @Override
    public PlatformHttpEndpoint getEndpoint() {
        return (PlatformHttpEndpoint) super.getEndpoint();
    }

    /**
     * This method is invoked by Spring Boot when invoking Camel via platform-http.
     *
     * The method is already running asynchronously via AsyncExecutionInterceptor.
     *
     * Returns an empty CompletableFuture as per documentation https://spring.io/guides/gs/async-method
     */
    @ResponseBody
    public CompletableFuture<Void> service(HttpServletRequest request, HttpServletResponse response) {
        return CompletableFuture.runAsync(() -> {
            LOG.trace("Service: {}", request);
            try {
                handleService(request, response);
            } catch (Exception e) {
                // do not leak exception back to caller
                LOG.warn("Error handling request due to: {}", e.getMessage(), e);
                try {
                    if (!response.isCommitted()) {
                        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    }
                } catch (Exception e1) {
                    // ignore
                }
            }
        }, executor);
    }

    protected void handleService(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (isSuspended()) {
            LOG.debug("Consumer suspended, cannot service request: {}", request);
            response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
            return;
        }

        Exchange exchange = createExchange(true);
        exchange.setPattern(ExchangePattern.InOut);
        HttpHelper.setCharsetFromContentType(request.getContentType(), exchange);
        PlatformHttpMessage msg = new PlatformHttpMessage(request, response, exchange,binding, false);
        exchange.setIn(msg);
        msg.init(exchange, binding, request, response);
        String contextPath = getEndpoint().getPath();
        exchange.getIn().setHeader(SpringBootPlatformHttpConstants.CONTEXT_PATH, contextPath);
        // set context path as header
        String httpPath = (String) exchange.getIn().getHeader(Exchange.HTTP_PATH);
        // here we just remove the CamelServletContextPath part from the HTTP_PATH
        if (contextPath != null && httpPath.startsWith(contextPath)) {
            exchange.getIn().setHeader(Exchange.HTTP_PATH, httpPath.substring(contextPath.length()));
        }
        if (getEndpoint().isUseCookieHandler()) {
            cookieConfiguration = getEndpoint().getCookieConfiguration();
            exchange.setProperty(Exchange.COOKIE_HANDLER, new SpringBootCookieHandler(request, response));
        }

        // we want to handle the UoW
        try {
            createUoW(exchange);
        } catch (Exception e) {
            throw new ServletException(e);
        }
        if (LOG.isTraceEnabled()) {
            LOG.trace("Processing request for exchangeId: {}", exchange.getExchangeId());
        }
        try {
            getProcessor().process(exchange);
        } catch (Exception e) {
            exchange.setException(e);
        } finally {
            afterProcess(response, exchange);
        }
    }

    protected void afterProcess(HttpServletResponse response, Exchange exchange) throws Exception {
        boolean writeFailure = false;
        try {
            // now lets output to the res
            if (LOG.isTraceEnabled()) {
                LOG.trace("Writing res for exchangeId: {}", exchange.getExchangeId());
            }
            binding.writeResponse(exchange, response);
        } catch (Exception e) {
            writeFailure = true;
            handleFailure(exchange, e);
        } finally {
            doneUoW(exchange);
            releaseExchange(exchange, false);
        }
        try {
            if (writeFailure && !response.isCommitted()) {
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        } catch (Exception e) {
            // ignore
        }

    }

    private void handleFailure(Exchange exchange, Throwable failure) {
        getExceptionHandler().handleException(
                "Failed writing HTTP response url: " + getEndpoint().getPath() + " due to: " + failure.getMessage(),
                failure);
        if (handleWriteResponseError) {
            Exception existing = exchange.getException();
            if (existing != null) {
                failure.addSuppressed(existing);
            }
            exchange.setProperty(Exchange.EXCEPTION_CAUGHT, failure);
            exchange.setException(failure);
        }
    }

    class SpringBootCookieHandler implements CookieHandler {

        private final HttpServletRequest request;
        private final HttpServletResponse response;

        public SpringBootCookieHandler(HttpServletRequest request, HttpServletResponse response) {
            this.request = request;
            this.response = response;
        }

        @Override
        public void addCookie(String cookieName, String cookieValue) {
            Cookie cookie = new Cookie(cookieName, cookieValue);
            cookie.setPath(cookieConfiguration.getCookiePath());
            cookie.setDomain(cookieConfiguration.getCookieDomain());
            cookie.setSecure(cookieConfiguration.isCookieSecure());
            cookie.setHttpOnly(cookieConfiguration.isCookieHttpOnly());
            cookie.setAttribute("SameSite", cookieConfiguration.getCookieSameSite().getValue());
            if (cookieConfiguration.getCookieMaxAge() != null) {
                cookie.setMaxAge(Math.toIntExact(cookieConfiguration.getCookieMaxAge()));
            }
            response.addCookie(cookie);
        }

        @Override
        public String removeCookie(String cookieName) {
            Cookie cookieToRemove = new Cookie(cookieName, null);
            cookieToRemove.setPath(cookieConfiguration.getCookiePath());
            // set max age to 0 to delete the cookie
            cookieToRemove.setMaxAge(0);
            response.addCookie(cookieToRemove);
            return cookieName;
        }

        @Override
        public String getCookieValue(@CookieValue String cookieName) {
            Cookie[] cookie = request.getCookies();
            // ensure cookies are not null
            if (cookie != null) {
                for (Cookie c : cookie) {
                    if (c.getName().equals(cookieName)) {
                        return c.getValue();
                    }
                }
            }
            return "Cookie not found";
        }
    }
}


