/*
 * Copyright (C) 2005-2008 Jive Software, Ignite Realtime Foundation 2023. All rights reserved.
 *
 * Licensed 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.jivesoftware.openfire.session;

import org.jivesoftware.openfire.RoutableChannelHandler;
import org.jivesoftware.openfire.StreamID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;

import javax.annotation.Nonnull;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * The session represents a connection between the server and a client (c2s) or
 * another server (s2s) as well as a connection with a component. Authentication and
 * user accounts are associated with c2s connections while s2s has an optional authentication
 * association but no single user.
 *
 * Obtain object managers from the session in order to access server resources.
 *
 * @author Gaston Dombiak
 */
public interface Session extends RoutableChannelHandler {

    Logger Log = LoggerFactory.getLogger(Session.class);

    /**
     * Version of the XMPP spec supported as MAJOR_VERSION.MINOR_VERSION (e.g. 1.0).
     */
    int MAJOR_VERSION = 1;
    int MINOR_VERSION = 0;

    enum Status {
        CLOSED,
        CONNECTED,
        AUTHENTICATED
    }

    /**
     * @deprecated Use {@link Status#CLOSED} instead.
     */
    @Deprecated // Remove in or after Openfire 4.9.
    Status STATUS_CLOSED = Status.CLOSED;

    /**
     * @deprecated Use {@link Status#CONNECTED} instead.
     */
    @Deprecated // Remove in or after Openfire 4.9.
    Status STATUS_CONNECTED = Status.CONNECTED;

    /**
     * @deprecated Use {@link Status#AUTHENTICATED} instead.
     */
    @Deprecated // Remove in or after Openfire 4.9.
    Status STATUS_AUTHENTICATED = Status.AUTHENTICATED;

    /**
      * Obtain the address of the user. The address is used by services like the core
      * server packet router to determine if a packet should be sent to the handler.
      * Handlers that are working on behalf of the server should use the generic server
      * hostname address (e.g. server.com).
      *
      * @return the address of the packet handler.
      */
    @Override
    JID getAddress();

    /**
     * Obtain the current status of this session.
     *
     * @return The status code for this session
     */
    Status getStatus();

    /**
     * Obtain the stream ID associated with this session. Stream ID's are generated by the server
     * and should be unique and random.
     *
     * @return This session's assigned stream ID
     */
    StreamID getStreamID();

    /**
     * Obtain the name of the server this session belongs to.
     *
     * @return the server name.
     */
    String getServerName();
    
    /**
     * Obtain the date the session was created.
     *
     * @return the session's creation date.
     */
    Date getCreationDate();

    /**
     * Obtain the time the session last had activity.
     *
     * @return The last time the session received activity.
     */
    Date getLastActiveDate();

    /**
     * Obtain the number of packets sent from the client to the server.
     *
     * @return The number of packets sent from the client to the server.
     */
    long getNumClientPackets();

    /**
     * Obtain the number of packets sent from the server to the client.
     *
     * @return The number of packets sent from the server to the client.
     */
    long getNumServerPackets();
    
    /**
     * Close this session including associated socket connection. The order of
     * events for closing the session is:
     * <ul>
     *      <li>Set closing flag to prevent redundant shutdowns.
     *      <li>Call notifyEvent all listeners that the channel is shutting down.
     *      <li>Close the socket.
     * </ul>
     * Implementations should ensure that after invocation, the result of {@link #getStatus()} will be CLOSED.
     */
    void close();

    /**
     * Returns true if the connection/session is closed.
     *
     * @return true if the connection is closed.
     */
    default boolean isClosed() {
        return getStatus() == Status.CLOSED;
    };

    /**
     * Returns true if this connection is secure.
     *
     * @return true if the connection is secure (e.g. SSL/TLS)
     * @deprecated Renamed. Use {@link #isEncrypted()} instead.
     */
    @Deprecated // Remove in Openfire 4.9 or later.
    boolean isSecure();

    /**
     * Returns true if this session uses encrypted connections.
     *
     * @return true if the session is encrypted (e.g. SSL/TLS)
     */
    default boolean isEncrypted() {
        return isSecure();
    }

    /**
     * Returns true if this session is authenticated (eg: SASL or Dialback authentication has completed successfully).
     *
     * @return true if the session is authenticated.
     */
    default boolean isAuthenticated() {
        return getStatus() == Status.AUTHENTICATED;
    }

    /**
     * Returns the peer certificates associated with this session, if any.
     *
     * @return certificates, possibly empty or null.
     */
    Certificate[] getPeerCertificates();

    /**
     * Returns the IP address string in textual presentation.
     *
     * @return  the raw IP address in a string format.
     * @throws java.net.UnknownHostException if IP address of host could not be determined.
     */
    String getHostAddress() throws UnknownHostException;

    /**
     * Gets the host name for this IP address.
     *
     * <p>If this InetAddress was created with a host name,
     * this host name will be remembered and returned;
     * otherwise, a reverse name lookup will be performed
     * and the result will be returned based on the system
     * configured name lookup service. If a lookup of the name service
     * is required, call
     * {@link java.net.InetAddress#getCanonicalHostName() getCanonicalHostName}.
     *
     * <p>If there is a security manager, its
     * <code>checkConnect</code> method is first called
     * with the hostname and <code>-1</code>
     * as its arguments to see if the operation is allowed.
     * If the operation is not allowed, it will return
     * the textual representation of the IP address.
     *
     * @return  the host name for this IP address, or if the operation
     *    is not allowed by the security check, the textual
     *    representation of the IP address.
     * @throws java.net.UnknownHostException if IP address of host could not be determined.
     *
     * @see java.net.InetAddress#getCanonicalHostName
     * @see SecurityManager#checkConnect
     */
    String getHostName() throws UnknownHostException;

    @Override
    void process( Packet packet );

    /**
     * Delivers raw text to this connection. This is a very low level way for sending
     * XML stanzas to the client. This method should not be used unless you have very
     * good reasons for not using {@link #process(Packet)}.<p>
     *
     * This method avoids having to get the writer of this connection and mess directly
     * with the writer. Therefore, this method ensures a correct delivery of the stanza
     * even if other threads were sending data concurrently.
     *
     * @param text the XML stanzas represented kept in a String.
     */
    void deliverRawText( String text );

    /**
     * Verifies that the connection is still live. Typically this is done by
     * sending a whitespace character between packets.
     *
     * // TODO No one is sending this message now. Delete it?
     *
     * @return true if the socket remains valid, false otherwise.
     */
    boolean validate();

    /**
     * Returns the TLS protocol name used by the connection of the session, if any.
     *
     * Always returns a valid string, though the string may be "NONE"
     *
     * @return a TLS protocol (version) name.
     */
    @Nonnull
    String getTLSProtocolName();

    /**
     * Returns the TLS cipher suite name used by the connection of the session, if any.
     *
     * Always returns a valid string, though the string may be "NONE"
     *
     * @return cipher suite name.
     */
    @Nonnull
    String getCipherSuiteName();

    /**
     * Returns the locale that is used for this session (e.g. {@link Locale#ENGLISH}).
     *
     * @return The language for the session.
     */
    Locale getLanguage();

    /**
     * Returns all Software Version data as reported by the peer on this connection,
     * as obtained through XEP-0092.
     *
     * @return The Software Version information (never null, possibly empty)
     */
    default Map<String, String> getSoftwareVersion()
    {
        return new HashMap<>();
    }

    /**
     * Parses a locale from the 'lang' attribute of the element that the provided parser is currently on.
     *
     * This method returns the English locale when there is no 'lang' attribute found. It will return an undefined
     * Locale if the value of the 'lang' attribute could not be used to identify a language.
     *
     * @param xpp An XML parser
     * @return A Locale
     */
    static Locale detectLanguage(XmlPullParser xpp) {
        // Default language is English ("en").
        Locale language = Locale.forLanguageTag("en");
        for (int i = 0; i < xpp.getAttributeCount(); i++) {
            if ("lang".equals(xpp.getAttributeName(i))) {
                language = Locale.forLanguageTag(xpp.getAttributeValue(i));
            }
        }
        return language;
    }

    /**
     * Returns a two-digit version identifier based on the value of the 'version' attribute of the element that the
     * provided parser is currently on. The value of the 'version' attribute is expected to match 'MAJOR.MINOR' where
     * both MAJOR and MINOR are integer values.
     *
     * The version number defaults to 0.0. It is returned as an array of integers, with the first element in the array
     * being the MAJOR version number.
     *
     * If the version that is being reported is larger than the version supported by this implementation, the version
     * number that is supported by this implementation is returned instead of the reported version number.
     *
     * @param xpp An XML parser
     * @return an integer array that has a size of two.
     */
    static int[] detectVersion(XmlPullParser xpp) {
        // Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
        // not report a version in which case "0.0" should be assumed (per rfc3920
        // section 4.4.1).
        int majorVersion = 0;
        int minorVersion = 0;
        for (int i = 0; i < xpp.getAttributeCount(); i++) {
            if ("version".equals(xpp.getAttributeName(i))) {
                try {
                    int[] version = decodeVersion(xpp.getAttributeValue(i));
                    majorVersion = version[0];
                    minorVersion = version[1];
                }
                catch (Exception e) {
                    Log.error("Unable to parse version from 'version' attribute of the element that is being parsed.", e);
                }
            }
        }

        // If the client supports a greater major version than the server,
        // set the version to the highest one the server supports.
        if (majorVersion > MAJOR_VERSION) {
            majorVersion = MAJOR_VERSION;
            minorVersion = MINOR_VERSION;
        }
        else if (majorVersion == MAJOR_VERSION) {
            // If the client supports a greater minor version than the
            // server, set the version to the highest one that the server
            // supports.
            if (minorVersion > MINOR_VERSION) {
                minorVersion = MINOR_VERSION;
            }
        }
        return new int[] { majorVersion, minorVersion };
    }

    static int[] decodeVersion(String version) {
        int[] answer = new int[] {0 , 0};
        String [] versionString = version.split("\\.");
        answer[0] = Integer.parseInt(versionString[0]);
        answer[1] = Integer.parseInt(versionString[1]);
        return answer;
    }
}
