/*
 * 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.directory.server;


import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;

import org.apache.directory.api.util.Network;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.core.api.InstanceLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * The command line main for the server.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class UberjarMain
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( UberjarMain.class );
    
    /** The key of the property use to specify the shutdown port */
    private static final String PROPERTY_SHUTDOWN_PORT = "apacheds.shutdown.port";

    /** The ApacheDS service */
    private ApacheDsService service;

    /**
     * Takes a single argument, the path to the installation home, which
     * contains the configuration to load with server startup settings.
     *
     * @param args the arguments
     * @throws Exception If the startup failed
     */
    public static void main( String[] args ) throws Exception
    {
        if ( ( args == null ) || ( args.length < 1 ) )
        {
            throw new IllegalArgumentException( "Instance directory argument is missing" );
        }

        String instanceDirectory = args[0];
        Action action = ( args.length == 2 ) ? Action.fromString( args[1] ) : Action.START;

        UberjarMain instance = new UberjarMain();
        
        switch ( action )
        {
            case START :
                // Starts the server
                LOG.debug( "Starting runtime" );
                instance.start( instanceDirectory );

                break;

            case STOP :
                // Stops the server
                LOG.debug( "Stopping runtime" );
                InstanceLayout layout = new InstanceLayout( instanceDirectory );
                try ( Socket socket = new Socket( Network.LOOPBACK, readShutdownPort( layout ) );
                    OutputStreamWriter osw = new OutputStreamWriter( socket.getOutputStream(), StandardCharsets.UTF_8 );
                    PrintWriter writer = new PrintWriter( osw ) )
                {
                    writer.print( readShutdownPassword( layout ) );
                }
                
                break;
                
            case REPAIR :
                // Try to fix the JDBM database
                LOG.debug( "Fixing the database runtime" );
                instance.repair( instanceDirectory );
                
                break;

            default:
                throw new IllegalArgumentException( "Unexpected action " + action );
        }

        LOG.trace( "Exiting main" );
    }

    
    private int getShutdownPort()
    {
        int shutdownPort = Integer.parseInt( System.getProperty( PROPERTY_SHUTDOWN_PORT, "0" ) );
        if ( shutdownPort < 0 || ( shutdownPort > 0 && shutdownPort < 1024 ) || shutdownPort > 65536 )
        {
            throw new IllegalArgumentException( "Shutdown port [" + shutdownPort + "] is an illegal port number" );
        }
        return shutdownPort;
    }

    
    private static int readShutdownPort( InstanceLayout layout ) throws IOException 
    {
        return Integer.parseInt( Strings.asciiBytesToString( 
                Files.readAllBytes( Paths.get( layout.getRunDirectory().getAbsolutePath(), ".shutdown.port" ) ) ) );
    }
    

    private static String readShutdownPassword( InstanceLayout layout ) throws IOException 
    {
        return Strings.asciiBytesToString(  Files.readAllBytes( 
                Paths.get( layout.getRunDirectory().getAbsolutePath(), ".shutdown.pwd" ) ) );
    }

    
    /**
     * Try to start the databases
     *
     * @param instanceDirectory The directory containing the server instance 
     */
    public void start( String instanceDirectory )
    {
        InstanceLayout layout = new InstanceLayout( instanceDirectory );
        
        // Creating ApacheDS service
        service = new ApacheDsService();

        // Initializing the service
        try
        {
            LOG.info( "Starting the service." );
            service.start( layout );
            
            startShutdownListener( layout );
        }
        catch ( Exception e )
        {
            LOG.error( "Failed to start the service.", e );
            stop();
            System.exit( 1 );
        }
    }

    
    /**
     * Try to repair the databases
     *
     * @param instanceDirectory The directory containing the server instance 
     */
    public void repair( String instanceDirectory )
    {
        InstanceLayout layout = new InstanceLayout( instanceDirectory );
        
        // Creating ApacheDS service
        service = new ApacheDsService();
        
        try
        {
            System.out.println( "Starting the service." );
            // must start servers otherwise stop() won't work
            service.start( layout, true );
            // no need to start the shutdown listener
            System.out.println( "Service started." );
        }
        catch ( Exception e )
        {
            return;
        }

        // Repairing the database
        try
        {
            LOG.info( "Starting the service." );
            service.repair( layout );
            LOG.info( "Database repaired." );
        }
        catch ( Exception e )
        {
            LOG.error( "Failed to repair the database.", e );
            stop();
            System.exit( 1 );
        }
        
        // Stop the service
        stop();

    }
    

    public void stop()
    {
        if ( service != null )
        {
            try
            {
                LOG.info( "Stopping the service." );
                service.stop();
                LOG.info( "Service stopped successfully." );
            }
            catch ( Exception e )
            {
                LOG.error( "Failed to start the service.", e );
                System.exit( 1 );
            }
        }
    }
    
    
    /**
     * Starts a thread that creates a ServerSocket which listens for shutdown command.
     *
     * @param layout the InstanceLayout
     * @throws IOException
     */
    private void startShutdownListener( final InstanceLayout layout ) throws IOException
    {
        final int shutdownPort = getShutdownPort();
        final String shutdownPassword = writeShutdownPassword( layout, UUID.randomUUID().toString() );
        
        new Thread( new Runnable()
        {
            @Override
            public void run()
            {
                // bind to localhost only to prevent connections from outside the box
                try ( ServerSocket shutdownSocket = new ServerSocket( shutdownPort, 1, Network.LOOPBACK ) )
                {
                    writeShutdownPort( layout, shutdownSocket.getLocalPort() );
                    
                    LOG.info( "Start the shutdown listener on port {}", shutdownSocket.getLocalPort() );
                    
                    Socket socket;
                    while ( ( socket = shutdownSocket.accept() ) != null )
                    {
                        if ( shutdownPassword == null || shutdownPassword.isEmpty() ) 
                        {
                            stop();
                            break;
                        }
                        else
                        {
                            try ( InputStreamReader reader = new InputStreamReader( socket.getInputStream(),
                                StandardCharsets.UTF_8 ); )
                            {
                                
                                
                                CharBuffer buffer = CharBuffer.allocate( 2048 );
                                while ( reader.read( buffer ) >= 0 )
                                {
                                    // read till end of stream
                                }
                                buffer.flip();
                                String password = buffer.toString();
                                
                                if ( shutdownPassword.equals( password ) )
                                {
                                    stop();
                                    break;
                                }
                                else
                                {
                                    LOG.warn( "Illegal attempt to shutdown, incorrect password {}", password );
                                }
                            }
                            catch ( IOException e )
                            {
                                LOG.warn( "Failed to handle the shutdown request", e );
                            }
                        }
                    }
                }
                catch ( IOException e )
                {
                    LOG.error( "Failed to start the shutdown listener, stopping the server", e );
                    stop();
                }
                
            }
        } ).start();
    }
    
    
    private static String writeShutdownPassword( InstanceLayout layout, String password ) throws IOException 
    {
        Files.write(
                Paths.get( layout.getRunDirectory().getAbsolutePath(), ".shutdown.pwd" ),
                password.getBytes( Charset.forName( "utf-8" ) ) );
        return password;
    }
    
    
    private static int writeShutdownPort( InstanceLayout layout, int portNumber ) throws IOException 
    {
        Files.write(
                Paths.get( layout.getRunDirectory().getAbsolutePath(), ".shutdown.port" ),
                Integer.toString( portNumber ).getBytes( Charset.forName( "utf-8" ) ) );
        return portNumber;
    }

    
    private enum Action
    {
        START, STOP, REPAIR;

        public static Action fromString( String actionString )
        {
            for ( Action action : values() )
            {
                if ( action.name().equalsIgnoreCase( actionString ) )
                {
                    return action;
                }
            }
            
            throw new IllegalArgumentException( "Unknown action " + actionString );
        }
    }
}
