/*
 * 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.jena.fuseki.cmd;

import static java.lang.String.format;
import static org.apache.jena.fuseki.Fuseki.serverLog;

import java.nio.file.Path;

import jakarta.servlet.ServletContext;
import org.apache.jena.atlas.lib.DateTimeUtils;
import org.apache.jena.atlas.lib.FileOps;
import org.apache.jena.fuseki.Fuseki;
import org.apache.jena.fuseki.FusekiConfigException;
import org.apache.jena.fuseki.FusekiException;
import org.apache.jena.fuseki.server.DataAccessPointRegistry;
import org.apache.jena.fuseki.server.FusekiCoreInfo;
import org.apache.jena.fuseki.webapp.FusekiEnv;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.security.ConstraintMapping;
import org.eclipse.jetty.ee10.servlet.security.ConstraintSecurityHandler;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.security.*;
import org.eclipse.jetty.security.Constraint.Authorization;
import org.eclipse.jetty.security.authentication.BasicAuthenticator;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.gzip.GzipHandler;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.xml.XmlConfiguration;

/** Standalone full server, not run as a WAR file.
 *
 * SPARQLServer is the Jena server instance which wraps/utilizes
 * {@link org.eclipse.jetty.server.Server}. This class provides
 * immediate access to the {@link org.eclipse.jetty.server.Server#start()} and
 * {@link org.eclipse.jetty.server.Server#stop()} commands as well as obtaining
 * instances of the server and server configuration. Finally we can obtain
 * instances of {@link org.apache.jena.fuseki.cmd.JettyServerConfig}.
 */
public class JettyFusekiWebapp {
    // Jetty specific.
    // This class is becoming less important - it now sets up a Jetty server for in-process use
    // either for the command line in development
    // and in testing but not direct webapp deployments.
    static { Fuseki.init(); }

    public static JettyFusekiWebapp  instance    = null;

    private ServerConnector serverConnector = null;
    // If a separate ...
    private ServerConnector mgtConnector    = null;

    private JettyServerConfig serverConfig;

    // The jetty server.

    private Server              server         = null;
    private ServletContext      servletContext = null;

    // Location of webapp static resources.
    // -- Standalone jar
    public static final String baseResource1   = "webapp";
    // -- Development
    public static final String baseResource2   = "target/webapp";

    /**
     * Default setup which requires a {@link org.apache.jena.fuseki.cmd.JettyServerConfig}
     * object as input.  We use this config to pass in the command line arguments for dataset,
     * name etc.
     * @param config
     */
    public static void initializeServer(JettyServerConfig config) {
        instance = new JettyFusekiWebapp(config);
    }

    private JettyFusekiWebapp(JettyServerConfig config) {
        this.serverConfig = config;
        buildServerWebapp(serverConfig.contextPath, serverConfig.jettyConfigFile);
        if ( mgtConnector == null )
            mgtConnector = serverConnector;

        if ( config.enableCompression ) {
            GzipHandler gzipHandler = new GzipHandler();
            gzipHandler.setHandler(server.getHandler());
            server.setHandler(gzipHandler);
        }
    }

    /**
     * Initialize the {@link JettyFusekiWebapp} instance.
     */
    public void start() {

        FusekiCoreInfo.logCode(serverLog);
        // This does not get anything usefully for Jetty as we use it.
        // String jettyVersion = org.eclipse.jetty.server.Server.getVersion();
        // serverLog.info(format("Jetty %s",jettyVersion));

        String host = serverConnector.getHost();
        if ( host != null )
            serverLog.info("Incoming connections limited to " + host);

        try {
            server.start();
        } catch (java.net.BindException ex) {
            serverLog.error("SPARQLServer (port="+serverConnector.getPort()+"): Failed to start server: " + ex.getMessage());
            throw new FusekiException("BindException: port="+serverConnector.getPort()+": Failed to start server: " + ex.getMessage(), ex);
        } catch (Exception ex) {
            serverLog.error("SPARQLServer: Failed to start server: " + ex.getMessage(), ex);
            throw new FusekiException("Failed to start server: " + ex.getMessage(), ex);
        }
        String now = DateTimeUtils.nowAsString();
        serverLog.info(format("Started %s on port %d", now, serverConnector.getPort()));
    }

    /**
     * Sync with the {@link JettyFusekiWebapp} instance.
     * Returns only if the server exits cleanly
     */
    public void join() {
        try {
            server.join();
        } catch (InterruptedException ex) { }
    }

        /**
     * Stop the {@link JettyFusekiWebapp} instance.
     */
    public void stop() {
        String now = DateTimeUtils.nowAsString();
        serverLog.info(format("Stopped %s on port %d", now, serverConnector.getPort()));
        try {
            server.stop();
        } catch (Exception ex) {
            Fuseki.serverLog.warn("SPARQLServer: Exception while stopping server: " + ex.getMessage(), ex);
        }
    }

    public static WebAppContext createWebApp(String contextPath) {
        FusekiEnv.setEnvironment();
        WebAppContext webapp = new WebAppContext();
        webapp.getContext().getServletContextHandler().setMaxFormContentSize(20 * 1000 * 1000);

        // Hunt for the webapp for the standalone jar (or development system).
        // Note that Path FUSEKI_HOME is not initialized until the webapp starts
        // so it is not available here.

        String baseResource3 = null;
        String baseResource4 = null;
        if ( FusekiEnv.FUSEKI_HOME != null ) {
            String HOME = FusekiEnv.FUSEKI_HOME.toString();
            baseResource3 = HOME+"/"+baseResource1;
            baseResource4 = HOME+"/"+baseResource2;
        }

        // The location in the webapp, not the URL names.
        String baseResource = tryBaseResource(baseResource1, null);
        baseResource = tryBaseResource(baseResource2, baseResource);
        baseResource = tryBaseResource(baseResource3, baseResource);
        baseResource = tryBaseResource(baseResource4, baseResource);

        if ( baseResource == null ) {
            if ( baseResource3 == null )
                Fuseki.serverLog.error("Can't find baseResource (tried "+baseResource1+" and "+baseResource2+")");
            else
                Fuseki.serverLog.error("Can't find baseResource (tried "+baseResource1+", "+baseResource2+", "+baseResource3+" and "+baseResource4+")");
            Fuseki.serverLog.error("Failed to start");
            throw new FusekiException("Failed to start");
        }

        String web_xml = baseResource+"/WEB-INF/web.xml";
        if ( ! FileOps.exists(web_xml) )
            Fuseki.serverLog.warn("Can't find WEB-INF/web.xml in "+baseResource);
        webapp.setDescriptor(web_xml);
        webapp.setContextPath(contextPath);
        // Avoid BaseResource aliasing warnings.
        Path absBaseResource = Path.of(baseResource).toAbsolutePath();
        webapp.getContext().getServletContextHandler().setBaseResourceAsPath(absBaseResource);

        //-- Jetty setup for the ServletContext logger.
        // The name of the Jetty-allocated slf4j/log4j logger is
        // the display name or, if null, the context path name.
        // It is set, without checking for a previous call of setLogger in "doStart"
        // which happens during server startup.
        // This the name of the ServletContext logger as well
        webapp.setDisplayName(Fuseki.servletRequestLogName);
        webapp.setParentLoaderPriority(true);               // Normal Java classloader behaviour.
        webapp.setErrorHandler(new FusekiErrorHandler());   // If used.
        return webapp;
    }

    public static String getenv(String name) {
        String x = System.getenv(name);
        if ( x == null )
            x = System.getProperty(name);
        return x;
    }

    public DataAccessPointRegistry getDataAccessPointRegistry() {
        return DataAccessPointRegistry.get(servletContext);
    }

    private static String tryBaseResource(String maybeResourceBase, String currentResourceBase) {
        if ( currentResourceBase != null )
            return currentResourceBase;
        if ( maybeResourceBase != null && FileOps.exists(maybeResourceBase) )
            return maybeResourceBase;
        return currentResourceBase;
    }

    private void buildServerWebapp(String contextPath, String jettyConfig) {
        if ( jettyConfig != null )
            // --jetty-config=jetty-fuseki.xml
            // for detailed configuration of the server using Jetty features.
            configServer(jettyConfig);
        else
            defaultServerConfig(serverConfig.port, serverConfig.loopback);

        WebAppContext webapp = createWebApp(contextPath);
        if ( false /*enable symbolic links */ ) {
            // See http://www.eclipse.org/jetty/documentation/current/serving-aliased-files.html
            // Record what would be needed:
            // 1 - Allow all symbolic links without checking
            webapp.addAliasCheck(new AllowedResourceAliasChecker(webapp));
            // 2 - Check links are to valid resources. But default for Unix?
            webapp.addAliasCheck(new SymlinkAllowedResourceAliasChecker(webapp));
        }
        servletContext = webapp.getServletContext();
        server.setHandler(webapp);
        // Replaced by Shiro.
        if ( jettyConfig == null && serverConfig.authConfigFile != null )
            security(webapp, serverConfig.authConfigFile);
    }

    // This is normally provided by Shiro.
    private static void security(ServletContextHandler context, String authfile) {
        UserStore userStore = makeUserStore(authfile);

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        IdentityService identService = new DefaultIdentityService();
        securityHandler.setIdentityService(identService);

        // ---- HashLoginService
        HashLoginService loginService = new HashLoginService("Fuseki Authentication");
        loginService.setUserStore(userStore);
        loginService.setIdentityService(identService);
        securityHandler.setLoginService(loginService);

        Authenticator authenticator = new BasicAuthenticator();
        securityHandler.setAuthenticator(authenticator);
        securityHandler.setRealmName("Fuseki");

        ConstraintMapping mapping = new ConstraintMapping();
        Constraint.Builder constraintBuilder = new Constraint.Builder();
        constraintBuilder.authorization(Authorization.ANY_USER);

        String authName = securityHandler.getAuthenticator().getAuthenticationType();
        constraintBuilder.name(authName);

        Constraint constraint = constraintBuilder.build();
        mapping.setConstraint(constraint);
        mapping.setPathSpec("/*");
        securityHandler.addConstraintMapping(mapping);

        context.setSecurityHandler(securityHandler);
        serverLog.debug("Basic Auth Configuration = " + authfile);
    }

    /**
     * Make a {@link UserStore} from a password file.
     * {@link PropertyUserStore} for details.
     */
    private static UserStore makeUserStore(String passwordFile) {
        if ( ! FileOps.exists(passwordFile) )
            throw new FusekiConfigException("No such file: "+passwordFile);
        PropertyUserStore propertyUserStore = new PropertyUserStore();
        Resource pwResource = newResource(passwordFile);
        propertyUserStore.setConfig(pwResource);
        propertyUserStore.setReloadInterval(5); // Need directory access
        try { propertyUserStore.start(); }
        catch (Exception ex) { throw new RuntimeException("UserStore", ex); }
        return propertyUserStore;
    }

    /** Create a resource for a filename */
    private static Resource newResource(String filename) {
        return ResourceFactory.root().newResource(filename);
    }

    private void defaultServerConfig(int port, boolean loopback) {
        // At least 3.
//        ThreadPool threadPool = new QueuedThreadPool(4);
//        server = new Server(threadPool);
        server = new Server();
        HttpConnectionFactory f1 = new HttpConnectionFactory();
        // Some people do try very large operations ... really, should use POST.
        f1.getHttpConfiguration().setRequestHeaderSize(512 * 1024);
        f1.getHttpConfiguration().setOutputBufferSize(5 * 1024 * 1024);
        // Do not add "Server: Jetty(....) when not a development system.
        if ( ! Fuseki.outputJettyServerHeader )
            f1.getHttpConfiguration().setSendServerVersion(false);

        // https is better done with a Jetty configuration file
        // because there are several things to configure.
        // See "examples/fuseki-jetty-https.xml"

        ServerConnector connector = new ServerConnector(server, f1);
        connector.setPort(port);
        server.addConnector(connector);
        if ( loopback )
            connector.setHost("localhost");
        serverConnector = connector;
    }

    private void configServer(String jettyConfig) {
        try {
            serverLog.info("Jetty server config file = " + jettyConfig);
            server = new Server();
            Resource configXml = ResourceFactory.root().newResource(jettyConfig);
            XmlConfiguration configuration = new XmlConfiguration(configXml);
            configuration.configure(server);
            serverConnector = (ServerConnector)server.getConnectors()[0];
        } catch (Exception ex) {
            serverLog.error("SPARQLServer: Failed to configure server: " + ex.getMessage(), ex);
            throw new FusekiException("Failed to configure a server using configuration file '" + jettyConfig + "'");
        }
    }
}
