/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.gatein.pc.controller;

import org.gatein.pc.api.Mode;
import org.gatein.pc.api.WindowState;
import org.gatein.pc.api.PortletInvokerException;
import org.gatein.pc.api.StateString;
import org.gatein.pc.controller.event.WindowEvent;
import org.gatein.pc.controller.event.EventControllerContext;
import org.gatein.pc.controller.request.PortletRequest;
import org.gatein.pc.controller.response.ControllerResponse;
import org.gatein.pc.controller.response.PageUpdateResponse;
import org.gatein.pc.controller.response.PortletResponse;
import org.gatein.pc.controller.state.PageNavigationalState;
import org.gatein.pc.controller.state.WindowNavigationalState;
import org.gatein.pc.api.invocation.EventInvocation;
import org.gatein.pc.api.invocation.response.PortletInvocationResponse;
import org.gatein.pc.api.invocation.response.ResponseProperties;
import org.gatein.pc.api.invocation.response.UpdateNavigationalStateResponse;
import org.gatein.pc.api.spi.PortletInvocationContext;

import jakarta.servlet.http.Cookie;
import java.util.List;
import java.util.Map;

class PortletRequestHandler extends RequestHandler<PortletRequest> {

  /** . */
  private static final EventControllerContextSafeInvoker safeInvoker = new EventControllerContextSafeInvoker();

  public PortletRequestHandler(PortletController controller) {
    super(PortletRequest.class, controller);
  }

  ControllerResponse processResponse(ControllerContext context,
                                     PortletRequest portletRequest,
                                     PortletInvocationResponse response) throws PortletInvokerException {
    // The page navigational state we will operate on during the request
    // Either we have nothing in the request so we create a new one
    // Or we have one but we copy it as we should not modify the input state
    // provided
    PageNavigationalState pageNavigationalState = portletRequest.getPageNavigationalState();
    if (pageNavigationalState == null) {
      pageNavigationalState = new PageNavigationalState(true);
    } else {
      pageNavigationalState = new PageNavigationalState(pageNavigationalState, true);
    }

    //
    ResponseProperties requestProperties = new ResponseProperties();

    //
    if (response instanceof UpdateNavigationalStateResponse) {
      // Update portlet NS
      UpdateNavigationalStateResponse updateResponse = (UpdateNavigationalStateResponse) response;

      //
      updateNavigationalState(context, portletRequest.getWindowId(), updateResponse, pageNavigationalState);

      //
      ResponseProperties update = updateResponse.getProperties();
      if (update != null) {
        requestProperties.append(update);
      }

      //
      EventControllerContext eventCC = context.getEventControllerContext();

      //
      EventPhaseContext phaseContext = new EventPhaseContext(controller, context, log);

      // Feed session it with the events that may have been produced
      for (UpdateNavigationalStateResponse.Event portletEvent : updateResponse.getEvents()) {
        if (!phaseContext.push(new WindowEvent(portletEvent.getName(),
                                               portletEvent.getPayload(),
                                               portletRequest.getWindowId()))) {
          return new PageUpdateResponse(updateResponse, requestProperties, pageNavigationalState, PortletResponse.INTERRUPTED);
        }
      }

      // Deliver events
      while (phaseContext.hasNext()) {
        WindowEvent toConsumeEvent = phaseContext.next();

        // Apply consumed event quota if necessary
        int consumedEventThreshold = controller.getConsumedEventThreshold();
        if (consumedEventThreshold >= 0) {
          if (phaseContext.consumedEventSize + 1 > consumedEventThreshold) {
            log.trace("Event distribution interrupted because the maximum number of consumed event is reached");
            safeInvoker.eventDiscarded(eventCC, phaseContext, toConsumeEvent, EventControllerContext.CONSUMED_EVENT_FLOODED);
            return new PageUpdateResponse(updateResponse, requestProperties, pageNavigationalState, PortletResponse.INTERRUPTED);
          }
        }

        //
        PortletInvocationResponse eventResponse;
        try {
          eventResponse = deliverEvent(context,
                                       toConsumeEvent,
                                       pageNavigationalState,
                                       getCookies(requestProperties));
        } catch (Exception e) {
          log.trace("Event delivery of " + toConsumeEvent + " failed", e);
          safeInvoker.eventFailed(eventCC, phaseContext, toConsumeEvent, e);
          continue;
        }

        // Now it is consumed
        phaseContext.consumedEventSize++;

        // Update nav state if needed
        if (eventResponse instanceof UpdateNavigationalStateResponse) {
          UpdateNavigationalStateResponse eventStateResponse = (UpdateNavigationalStateResponse) eventResponse;

          // Update ns
          updateNavigationalState(context, toConsumeEvent.getWindowId(), eventStateResponse, pageNavigationalState);

          // Add events to source event queue
          for (UpdateNavigationalStateResponse.Event portletEvent : eventStateResponse.getEvents()) {
            WindowEvent toRouteEvent = new WindowEvent(portletEvent.getName(),
                                                       portletEvent.getPayload(),
                                                       toConsumeEvent.getWindowId());

            //
            if (!phaseContext.push(toConsumeEvent, toRouteEvent)) {
              return new PageUpdateResponse(updateResponse,
                                            requestProperties,
                                            pageNavigationalState,
                                            PortletResponse.INTERRUPTED);
            }
          }

          //
          ResponseProperties updateProperties = eventStateResponse.getProperties();
          if (updateProperties != null) {
            requestProperties.append(updateProperties);
          }
        }

        // And we make a callback
        safeInvoker.eventConsumed(eventCC, phaseContext, toConsumeEvent, eventResponse);
      }

      //
      return new PageUpdateResponse(updateResponse, requestProperties, pageNavigationalState, PortletResponse.DISTRIBUTION_DONE);
    } else {
      return new PortletResponse(response, PortletResponse.DISTRIBUTION_DONE);
    }
  }

  private PortletInvocationResponse deliverEvent(ControllerContext context,
                                                 WindowEvent event,
                                                 PageNavigationalState pageNavigationalState,
                                                 List<Cookie> requestCookies) throws PortletInvokerException {
    WindowNavigationalState windowNS = pageNavigationalState.getWindowNavigationalState(event.getWindowId());

    //
    if (windowNS == null) {
      windowNS = new WindowNavigationalState();
    }

    //
    Map<String, String[]> publicNS =
                                   context.getStateControllerContext()
                                          .getPublicWindowNavigationalState(context, pageNavigationalState, event.getWindowId());

    //
    PortletInvocationContext portletInvocationContext = context.createPortletInvocationContext(event.getWindowId(),
                                                                                               pageNavigationalState);
    EventInvocation eventInvocation = new EventInvocation(portletInvocationContext);

    //
    eventInvocation.setMode(windowNS.getMode());
    eventInvocation.setWindowState(windowNS.getWindowState());
    eventInvocation.setNavigationalState(windowNS.getPortletNavigationalState());
    eventInvocation.setPublicNavigationalState(publicNS);
    eventInvocation.setName(event.getName());
    eventInvocation.setPayload(event.getPayload());

    //
    return context.invoke(event.getWindowId(), requestCookies, eventInvocation);
  }

  private void updateNavigationalState(ControllerContext context,
                                       String windowId,
                                       UpdateNavigationalStateResponse update,
                                       PageNavigationalState page) throws PortletInvokerException {
    WindowNavigationalState windowNS = page.getWindowNavigationalState(windowId);

    //
    if (windowNS == null) {
      windowNS = new WindowNavigationalState();
    }

    //
    Mode mode = windowNS.getMode();
    if (update.getMode() != null) {
      mode = update.getMode();
    }
    WindowState windowState = windowNS.getWindowState();
    if (update.getWindowState() != null) {
      windowState = update.getWindowState();
    }
    StateString portletNS = windowNS.getPortletNavigationalState();
    if (update.getNavigationalState() != null) {
      portletNS = update.getNavigationalState();
    }
    windowNS = new WindowNavigationalState(portletNS, mode, windowState);
    page.setWindowNavigationalState(windowId, windowNS);

    // Now update shared state scoped at page
    Map<String, String[]> publicNS = update.getPublicNavigationalStateUpdates();
    if (publicNS != null) {
      context.getStateControllerContext().updatePublicNavigationalState(context, page, windowId, publicNS);
    }
  }

  private static List<Cookie> getCookies(ResponseProperties properties) {
    if (properties.getCookies() == null) {
      return null;
    }
    return properties.getCookies().stream().map(cookie -> {
      Cookie jakartaCookie = new Cookie(cookie.getName(), cookie.getValue());
      jakartaCookie.setDomain(cookie.getDomain());
      jakartaCookie.setHttpOnly(cookie.isHttpOnly());
      jakartaCookie.setMaxAge(cookie.getMaxAge());
      jakartaCookie.setPath(cookie.getPath());
      jakartaCookie.setSecure(cookie.getSecure());
      return jakartaCookie;
    }).toList();
  }

}
