/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.exoplatform.web;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.servlet.ServletConfig;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.exoplatform.commons.utils.Safe;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.RootContainer;
import org.exoplatform.container.component.RequestLifeCycle;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.management.annotations.Impact;
import org.exoplatform.management.annotations.ImpactType;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.annotations.ManagedName;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.management.rest.annotations.RESTEndpoint;
import org.exoplatform.web.application.Application;
import org.exoplatform.web.controller.QualifiedName;
import org.exoplatform.web.controller.metadata.ControllerDescriptor;
import org.exoplatform.web.controller.metadata.DescriptorBuilder;
import org.exoplatform.web.controller.router.Router;
import org.exoplatform.web.controller.router.RouterConfigException;
import org.gatein.common.http.QueryStringParser;
import org.exoplatform.services.log.Log;
import org.exoplatform.services.log.ExoLogger;
import org.picocontainer.Startable;

/**
 * The WebAppController is the entry point of the GateIn service.
 */
@Managed
@ManagedDescription("The portal controller")
@NameTemplate({ @Property(key = "view", value = "portal"), @Property(key = "service", value = "controller") })
@RESTEndpoint(path = "portalcontroller")
public class WebAppController implements Startable {

    /** . */
    public static final QualifiedName HANDLER_PARAM = QualifiedName.create("gtn", "handler");

    /** . */
    protected static Log                           log           = ExoLogger.getLogger(WebAppController.class);

    /** . */
    private final HashMap<String, Object> attributes_;

    /** . */
    private volatile HashMap<String, Application> applications_;

    /** . */
    private final HashMap<String, WebRequestHandler> handlers;

    /** . */
    private final AtomicReference<Router> routerRef;

    /** . */
    private final AtomicReference<String> configurationPathRef;

    /**
     * Must have 'controller.config' as init parameter configuration (in xml)
     * that point to the controller.xml used to initialize navigation controller
     *
     * This service component is a container for applications: PortalApplication, PortletApplication....
     * It use the navigation controller to route the request to corresponding handlers (config as service plugin)
     *
     * @param params the init params
     * @throws Exception any exception
     */
    public WebAppController(InitParams params) throws Exception {
        // Get router config
        ValueParam routerConfig = params.getValueParam("controller.config");
        if (routerConfig == null) {
            throw new IllegalArgumentException("No router param defined");
        }
        String configurationPath = routerConfig.getValue();

        //
        this.applications_ = new HashMap<String, Application>();
        this.attributes_ = new HashMap<String, Object>();
        this.handlers = new HashMap<String, WebRequestHandler>();
        this.routerRef = new AtomicReference<Router>();
        this.configurationPathRef = new AtomicReference<String>(configurationPath);

        //
        reloadConfiguration();
    }

    public Object getAttribute(String name, Object value) {
        return attributes_.get(name);
    }

    @SuppressWarnings("unchecked")
    public <T extends Application> T getApplication(String appId) {
        return (T) applications_.get(appId);
    }

    public List<Application> getApplicationByType(String type) {
        List<Application> applications = new ArrayList<Application>();
        for (Application app : applications_.values()) {
            if (app.getApplicationType().equals(type))
                applications.add(app);
        }
        return applications;
    }

    public synchronized void removeApplication(String appId) {
        applications_.remove(appId);
    }

    @Managed
    @ManagedDescription("The configuration path")
    public String getConfigurationPath() {
        return String.valueOf(configurationPathRef.get());
    }

    @Managed
    @ManagedDescription("Load the controller configuration")
    @Impact(ImpactType.WRITE)
    public void loadConfiguration(@ManagedDescription("The configuration path") @ManagedName("path") String path)
            throws IOException, RouterConfigException {
        File f = new File(path);
        if (!f.exists()) {
            throw new MalformedURLException("Could not resolve path " + path);
        }
        if (!f.isFile()) {
            throw new MalformedURLException("Could not resolve path " + path + " to a valid file");
        }
        loadConfiguration(f.toURI().toURL());
        configurationPathRef.set(path);
    }

    protected void loadConfiguration(URL url) throws RouterConfigException, IOException {
        log.info("Loading router configuration " + url);
        InputStream in = url.openStream();
        try {
            ControllerDescriptor routerDesc = new DescriptorBuilder().build(in);
            Router router = new Router(routerDesc);
            routerRef.set(router);
        } finally {
            Safe.close(in);
        }
    }

    @Managed
    @ManagedDescription("Reload the controller configuration")
    @Impact(ImpactType.WRITE)
    public void reloadConfiguration() throws RouterConfigException, IOException {
        log.info("Loading router configuration " + configurationPathRef.get());
        loadConfiguration(configurationPathRef.get());
    }

    @Managed
    @ManagedDescription("Enumerates the routes found for the specified request")
    @Impact(ImpactType.READ)
    public String findRoutes(
            @ManagedDescription("The request uri relative to the web application") @ManagedName("uri") String uri) {
        Router router = routerRef.get();
        if (router != null) {
            Map<String, String[]> parameters;
            String path;
            int pos = uri.indexOf('?');
            if (pos != -1) {
                parameters = QueryStringParser.getInstance().parseQueryString(uri.substring(pos + 1));
                path = uri.substring(0, pos);
            } else {
                parameters = Collections.emptyMap();
                path = uri;
            }

            //
            List<Map<QualifiedName, String>> results = new ArrayList<Map<QualifiedName, String>>();
            Iterator<Map<QualifiedName, String>> matcher = router.matcher(path, parameters);
            while (matcher.hasNext()) {
                Map<QualifiedName, String> match = matcher.next();
                results.add(match);
            }

            //
            return results.toString();
        } else {
            throw new IllegalStateException("No route currently configured");
        }
    }

    public WebRequestHandler getHandler(String handlerName) {
      return handlers.get(handlerName);
    }

    /**
     * Add application (portlet, gadget) to the global application map if and only if it has not been registered yet.
     *
     * @param <T>
     * @param app
     * @return
     */
    public <T extends Application> T addApplication(T app) {
        Application result = getApplication(app.getApplicationId());

        // Double-check block
        if (result == null) {
            synchronized (this) {
                result = getApplication(app.getApplicationId());
                if (result == null) {
                    HashMap<String, Application> temporalApplicationsMap = new HashMap<String, Application>(applications_);
                    temporalApplicationsMap.put(app.getApplicationId(), app);
                    this.applications_ = temporalApplicationsMap;
                    result = app;
                }
            }
        }

        return (T) result;
    }

    /**
     * Register an handler as a component plugin, this method is invoked by the kernel with reflection.
     *
     * @param handler the handler
     */
    public void register(WebRequestHandler handler) {
        handlers.put(handler.getHandlerName(), handler);
    }

    public void unregister(String[] paths) {
        for (String path : paths) {
            WebRequestHandler handler = handlers.remove(path);
            handler.onDestroy(this);
        }
    }

    /**
     * Delegate init action to handler, for example: PortalRequestHandler have
     * a change to create and register PortalApplication with WebAppController
     * @param config
     * @throws Exception
     */
    public void onHandlersInit(ServletConfig config) throws Exception {
        Collection<WebRequestHandler> hls = handlers.values();
        for (WebRequestHandler handler : hls) {
            handler.onInit(this, config);
        }
    }

    public Router getRouter() {
        return routerRef.get();
    }

    /**
     * <p>
     * This is the first method - in the GateIn portal - reached by incoming HTTP request, it acts like a servlet service()
     * method. According to the servlet path used the correct handler is selected and then executed.
     * </p>
     *
     * <p>
     * During a request the request life cycle is demarcated by calls to {@link RequestLifeCycle#begin(ExoContainer)} and
     * {@link RequestLifeCycle#end()}.
     * </p>
     *
     * @param req the http request
     * @param res the http response
     * @throws Exception any exception
     */
    public void service(HttpServletRequest req, HttpServletResponse res) throws Exception {
        boolean debug = log.isDebugEnabled();

        try {
            // We set the character encoding now to UTF-8 before obtaining parameters
            req.setCharacterEncoding("UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error("Encoding not supported", e);
        }

        String portalPath = req.getRequestURI().substring(req.getContextPath().length());
        Router router = routerRef.get();

        //
        if (router != null) {
            Iterator<Map<QualifiedName, String>> matcher = router.matcher(portalPath, req.getParameterMap());

            //
            boolean started = false;
            boolean processed = false;

            //
            try {
                while (matcher.hasNext() && !processed) {
                    //
                    Map<QualifiedName, String> parameters = matcher.next();
                    String handlerKey = parameters.get(HANDLER_PARAM);
                    if (handlerKey != null) {
                        WebRequestHandler handler = handlers.get(handlerKey);
                        if (handler != null) {
                            if (debug) {
                                log.debug("Serving request path=" + portalPath + ", parameters=" + parameters
                                        + " with handler " + handler);
                            }

                            if (!started && handler.getRequiresLifeCycle()) {
                                started = true;
                                ExoContainer container = ExoContainerContext.getCurrentContainer();
                                if (debug) {
                                    log.debug("Starting RequestLifeCycle for handler " + handler);
  
                                    if (!(container instanceof PortalContainer)) {
                                        //We need this log to trace the issue for TA-423 if the bug still happen
                                        log.debug("current container is " + container + ", it should be a PortalContainer instance to start a portal request lifecycle");                                    
                                    }
                                }
                                RequestLifeCycle.begin(container);
                            }

                            //
                            PortalHttpServletResponseWrapper httpServletResponseWrapper = new PortalHttpServletResponseWrapper(res);
                            processed = handler.execute(new ControllerContext(this, router, req, httpServletResponseWrapper, parameters));
                        } else {
                            if (debug) {
                                log.debug("No handler " + handlerKey + " for request path=" + portalPath + ", parameters="
                                        + parameters);
                            }
                        }
                    }
                }
            } finally {
                if (started) {
                    if (debug) {
                        log.debug("Finishing RequestLifeCycle for current request");
                    }
                    RequestLifeCycle.end();
                }
            }

            //
            if (!processed) {
                log.error("Could not associate the request path=" + portalPath + " with an handler");
                res.sendError(HttpServletResponse.SC_NOT_FOUND);
            }
        } else {
            log.error("Missing valid router configuration " + configurationPathRef.get());
            res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    public void start() {
    }

    @Override
    public void stop() {
        for (WebRequestHandler handler : handlers.values()) {
            handler.onDestroy(this);
        }
    }
}
