/*
 * 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.sshd.common.session;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import org.apache.sshd.common.AttributeStore;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.FactoryManagerHolder;
import org.apache.sshd.common.PropertyResolver;
import org.apache.sshd.common.Service;
import org.apache.sshd.common.auth.MutableUserHolder;
import org.apache.sshd.common.channel.ChannelListenerManager;
import org.apache.sshd.common.cipher.CipherInformation;
import org.apache.sshd.common.compression.CompressionInformation;
import org.apache.sshd.common.forward.PortForwardingEventListenerManager;
import org.apache.sshd.common.future.KeyExchangeFuture;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.kex.KexFactoryManager;
import org.apache.sshd.common.kex.KexProposalOption;
import org.apache.sshd.common.kex.KeyExchange;
import org.apache.sshd.common.mac.MacInformation;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.buffer.Buffer;

/**
 * Represents an SSH session. <B>Note:</B> the associated username for the session
 * may be {@code null}/empty if the session is not yet authenticated
 *
 * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
 */
public interface Session
        extends KexFactoryManager,
                SessionListenerManager,
                ReservedSessionMessagesManager,
                ChannelListenerManager,
                PortForwardingEventListenerManager,
                FactoryManagerHolder,
                PropertyResolver,
                AttributeStore,
                Closeable,
                MutableUserHolder {

    /**
     * Default prefix expected for the client / server identification string
     * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2</A>
     */
    String DEFAULT_SSH_VERSION_PREFIX = "SSH-2.0-";

    /**
     * Backward compatible special prefix
     * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-5">RFC 4253 - section 5</A>
     */
    String FALLBACK_SSH_VERSION_PREFIX = "SSH-1.99-";

    /**
     * Maximum number of characters for any single line sent as part
     * of the initial handshake - according to
     * <A HREF="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253 - section 4.2</A>:</BR>
     *
     * <P><CODE>
     *      The maximum length of the string is 255 characters,
     *      including the Carriage Return and Line Feed.
     * </CODE></P>
     */
    int MAX_VERSION_LINE_LENGTH = 256;

    /**
     * Timeout status.
     */
    enum TimeoutStatus {
        NoTimeout,
        AuthTimeout,
        IdleTimeout
    }

    /**
     * Retrieve the client version for this session.
     *
     * @return the client version.
     */
    String getClientVersion();

    /**
     * Retrieve the server version for this session.
     *
     * @return the server version.
     */
    String getServerVersion();

    /**
     * Retrieve one of the negotiated values during the KEX stage
     *
     * @param paramType The request {@link KexProposalOption} value - ignored
     *                  if {@code null}
     * @return The negotiated parameter value - {@code null} if invalid
     * parameter or no negotiated value
     */
    String getNegotiatedKexParameter(KexProposalOption paramType);

    /**
     * Retrieves current cipher information - <B>Note:</B> may change if
     * key re-exchange executed
     *
     * @param incoming If {@code true} then the cipher for the incoming data,
     *                 otherwise for the outgoing data
     * @return The {@link CipherInformation} - or {@code null} if not negotiated yet.
     */
    CipherInformation getCipherInformation(boolean incoming);

    /**
     * Retrieves current compression information - <B>Note:</B> may change if
     * key re-exchange executed
     *
     * @param incoming If {@code true} then the compression for the incoming data,
     *                 otherwise for the outgoing data
     * @return The {@link CompressionInformation} - or {@code null} if not negotiated yet.
     */
    CompressionInformation getCompressionInformation(boolean incoming);

    /**
     * Retrieves current MAC information - <B>Note:</B> may change if
     * key re-exchange executed
     *
     * @param incoming If {@code true} then the MAC for the incoming data,
     *                 otherwise for the outgoing data
     * @return The {@link MacInformation} - or {@code null} if not negotiated yet.
     */
    MacInformation getMacInformation(boolean incoming);

    /**
     * Create a new buffer for the specified SSH packet and reserve the needed space
     * (5 bytes) for the packet header.
     *
     * @param cmd the SSH command
     * @return a new buffer ready for write
     */
    Buffer createBuffer(byte cmd);

    /**
     * Create a new buffer for the specified SSH packet and reserve the needed space
     * (5 bytes) for the packet header.
     *
     * @param cmd           The SSH command to initialize the buffer with
     * @param estimatedSize Estimated number of bytes the buffer will hold, 0 if unknown.
     * @return a new buffer ready for write
     * @see #prepareBuffer(byte, Buffer)
     */
    Buffer createBuffer(byte cmd, int estimatedSize);

    /**
     * Prepare a new &quot;clean&quot; buffer while reserving the needed space
     * (5 bytes) for the packet header.
     * @param cmd    The SSH command to initialize the buffer with
     * @param buffer The {@link Buffer} instance to initialize
     * @return The initialized buffer
     */
    Buffer prepareBuffer(byte cmd, Buffer buffer);

    /**
     * Sends an {@code SSH_MSG_DEBUG} to the peer session
     *
     * @param display {@code true} if OK to display the message at the peer as-is
     * @param msg The message object whose {@code toString()} value to be used - if
     * {@code null} then the &quot;null&quot; string is sent
     * @param lang The language - {@code null}/empty if some pre-agreed default is used
     * @return An {@code IoWriteFuture} that can be used to check when the packet has actually been sent
     * @throws IOException if an error occurred when encoding sending the packet
     * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-11.3">RFC 4253 - section 11.3</A>
     */
    IoWriteFuture sendDebugMessage(boolean display, Object msg, String lang) throws IOException;

    /**
     * Sends an {@code SSH_MSG_IGNORE} to the peer session
     *
     * @param data The message data
     * @return An {@code IoWriteFuture} that can be used to check when the packet has actually been sent
     * @throws IOException if an error occurred when encoding sending the packet
     * @see <A HREF="https://tools.ietf.org/html/rfc4253#section-11.2">RFC 4253 - section 11.2</A>
     */
    IoWriteFuture sendIgnoreMessage(byte... data) throws IOException;

    /**
     * Encode and send the given buffer.
     * The buffer has to have 5 bytes free at the beginning to allow the encoding to take place.
     * Also, the write position of the buffer has to be set to the position of the last byte to write.
     *
     * @param buffer the buffer to encode and send
     * @return An {@code IoWriteFuture} that can be used to check when the packet has actually been sent
     * @throws IOException if an error occurred when encoding sending the packet
     */
    IoWriteFuture writePacket(Buffer buffer) throws IOException;

    /**
     * Encode and send the given buffer with the specified timeout.
     * If the buffer could not be written before the timeout elapses, the returned
     * {@link org.apache.sshd.common.io.IoWriteFuture} will be set with a
     * {@link java.util.concurrent.TimeoutException} exception to indicate a timeout.
     *
     * @param buffer  the buffer to encode and spend
     * @param timeout the timeout
     * @param unit    the time unit of the timeout parameter
     * @return a future that can be used to check when the packet has actually been sent
     * @throws IOException if an error occurred when encoding sending the packet
     */
    IoWriteFuture writePacket(Buffer buffer, long timeout, TimeUnit unit) throws IOException;

    /**
     * Send a global request and wait for the response. This must only be used when sending
     * a {@code SSH_MSG_GLOBAL_REQUEST} with a result expected, else it will time out
     *
     * @param request the request name - used mainly for logging and debugging
     * @param buffer the buffer containing the global request
     * @param timeout The number of time units to wait - must be <U>positive</U>
     * @param unit The {@link TimeUnit} to wait for the response
     * @return the return buffer if the request was successful, {@code null} otherwise.
     * @throws IOException if an error occurred when encoding sending the packet
     */
    Buffer request(String request, Buffer buffer, long timeout, TimeUnit unit) throws IOException;

    /**
     * Handle any exceptions that occurred on this session.
     * The session will be closed and a disconnect packet will be
     * sent before if the given exception is an
     * {@link org.apache.sshd.common.SshException} with a positive error code
     *
     * @param t the exception to process
     */
    void exceptionCaught(Throwable t);

    /**
     * Initiate a new key exchange.
     *
     * @return A {@link KeyExchangeFuture} for awaiting the completion of the exchange
     * @throws IOException If failed to request keys re-negotiation
     */
    KeyExchangeFuture reExchangeKeys() throws IOException;

    /**
     * Get the service of the specified type.
     * If the service is not of the specified class,
     * an IllegalStateException will be thrown.
     *
     * @param <T>   The generic {@link Service} type
     * @param clazz The service class
     * @return The service instance
     * @throws IllegalStateException If failed to find a matching service
     */
    <T extends Service> T getService(Class<T> clazz);

    /**
     * @return the {@link IoSession} associated to this session
     */
    IoSession getIoSession();

    /**
     * Re-start idle timeout timer
     */
    void resetIdleTimeout();

    /**
     * Check if timeout has occurred.
     *
     * @return the timeout status, never {@code null}
     */
    TimeoutStatus getTimeoutStatus();

    /**
     * @return Timeout value in milliseconds for authentication stage
     */
    long getAuthTimeout();

    /**
     * @return Timeout value in milliseconds for communication
     */
    long getIdleTimeout();

    boolean isAuthenticated();
    void setAuthenticated() throws IOException;

    /**
     * @return The established session identifier - {@code null} if
     * not yet established
     */
    byte[] getSessionId();

    KeyExchange getKex();

    /**
     * Send a disconnect packet with the given reason and message.
     * Once the packet has been sent, the session will be closed
     * asynchronously.
     *
     * @param reason the reason code for this disconnect
     * @param msg    the text message
     * @throws IOException if an error occurred sending the packet
     */
    void disconnect(int reason, String msg) throws IOException;

    /**
     * @param name Service name
     * @throws Exception If failed to start it
     */
    void startService(String name) throws Exception;

    /**
     * @param version The reported client/server version
     * @return {@code true} if version not empty and starts with either
     * {@value #DEFAULT_SSH_VERSION_PREFIX} or {@value #FALLBACK_SSH_VERSION_PREFIX}
     */
    static boolean isValidVersionPrefix(String version) {
        return GenericUtils.isNotEmpty(version)
            && (version.startsWith(DEFAULT_SSH_VERSION_PREFIX) || version.startsWith(FALLBACK_SSH_VERSION_PREFIX));
    }
}
