/*
 * Copyright (C) 2020 Ignite Realtime Foundation. 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.muc.spi;

import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.IQHandlerInfo;
import org.jivesoftware.openfire.PacketException;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.handler.IQHandler;
import org.jivesoftware.openfire.muc.MUCRole;
import org.jivesoftware.openfire.muc.MUCRoom;
import org.jivesoftware.openfire.muc.MultiUserChatService;
import org.jivesoftware.openfire.session.Session;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.openfire.vcard.VCardManager;
import org.jivesoftware.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.Message;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;

import java.util.Iterator;
import java.util.Locale;

/**
 * Implements the TYPE_IQ vcard-temp protocol, to be used for MUC rooms.
 * <p>
 * This implementation borrows heavily from IQvCardHandler.
 *
 * @author Guus der Kinderen, [email protected]
 * @see org.jivesoftware.openfire.handler.IQvCardHandler
 */
public class IQMUCvCardHandler extends IQHandler
{

    private static final Logger Log = LoggerFactory.getLogger(IQMUCvCardHandler.class);

    public static SystemProperty<Boolean> PROPERTY_ENABLED = SystemProperty.Builder.ofType(Boolean.class)
        .setKey("xmpp.muc.vcard.enabled")
        .setDynamic(true)
        .setDefaultValue(true)
        .build();

    public static final String REQUEST_ELEMENT_NAME = "vCard";
    public static final String RESPONSE_ELEMENT_NAME = "vCard";
    public static final String NAMESPACE = "vcard-temp";

    private IQHandlerInfo info;
    private MultiUserChatService mucService;

    public IQMUCvCardHandler( MultiUserChatService mucService )
    {
        super("XMPP vCard Handler for MUC");
        this.mucService = mucService;
        info = new IQHandlerInfo(REQUEST_ELEMENT_NAME, RESPONSE_ELEMENT_NAME);
    }

    @Override
    public IQ handleIQ( IQ packet ) throws PacketException
    {
        IQ result = IQ.createResultIQ(packet);
        IQ.Type type = packet.getType();
        if ( type.equals(IQ.Type.set) )
        {
            Log.debug("vCard update request received from: '{}', for: '{}'", packet.getFrom(), packet.getTo());
            try
            {
                String roomName = packet.getTo().getNode();
                // If no TO was specified then return an error.
                if ( roomName == null )
                {
                    Log.debug("vCard update request from: '{}', for: '{}' is invalid: it does not refer to a specific room.", packet.getFrom(), packet.getTo());
                    result.setChildElement(packet.getChildElement().createCopy());
                    result.setError(PacketError.Condition.not_acceptable);
                    result.getError().setText("Request 'to' attribute has no node-part. The request should be addressed to a room of a MUC service.");
                }
                else
                {
                    final MUCRoom room = mucService.getChatRoom(roomName);
                    Log.debug("vCard update request from: '{}', for: '{}' relates to room: {}", packet.getFrom(), packet.getTo(), room);
                    if ( room == null || !room.getOwners().contains(packet.getFrom().asBareJID()) )
                    {
                        Log.debug("vCard update request from: '{}', for: '{}' is invalid: room does not exist, or sender is not allowed to discover the room.", packet.getFrom(), packet.getTo());
                        result.setChildElement(packet.getChildElement().createCopy());
                        result.setError(PacketError.Condition.forbidden);
                        result.getError().setText("You are not an owner of this room.");
                    }
                    else
                    {
                        Element vcard = packet.getChildElement();
                        if ( vcard != null )
                        {
                            try
                            {
                                VCardManager.getInstance().setVCard(room.getJID().toString(), vcard);

                                // This is what EJabberd does. Mimic it, for compatibility.
                                sendConfigChangeNotification(room);

                                // Mimic a client that broadcasts a vCard update. Converse seems to need this.
                                final String hash = calculatePhotoHash(vcard);
                                sendVCardUpdateNotification(room, hash);
                                Log.debug("vCard update request from: '{}', for: '{}' processed successfully.", packet.getFrom(), packet.getTo());
                            }
                            catch ( UnsupportedOperationException e )
                            {
                                Log.debug("Entity '{}' tried to set VCard, but the configured VCard provider is read-only. An IQ error will be returned to sender.", packet.getFrom());
                                // VCards can include binary data. Let's not echo that back in the error.
                                // result.setChildElement( packet.getChildElement().createCopy() );

                                result.setError(PacketError.Condition.not_allowed);

                                Locale locale = JiveGlobals.getLocale(); // default to server locale.
                                final Session session = SessionManager.getInstance().getSession(result.getTo());
                                if ( session != null && session.getLanguage() != null )
                                {
                                    locale = session.getLanguage(); // use client locale if one is available.
                                }
                                result.getError().setText(LocaleUtils.getLocalizedString("vcard.read_only", locale), locale.getLanguage());
                            }
                        }
                    }
                }
            }
            catch ( UserNotFoundException e )
            {
                // VCards can include binary data. Let's not echo that back in the error.
                // result.setChildElement( packet.getChildElement().createCopy() );

                result.setError(PacketError.Condition.item_not_found);
            }
            catch ( Exception e )
            {
                Log.error(e.getMessage(), e);
                result.setError(PacketError.Condition.internal_server_error);
            }
        }
        else if ( type.equals(IQ.Type.get) )
        {
            Log.debug("vCard retrieve request received from: '{}', for: '{}'", packet.getFrom(), packet.getTo());
            String roomName = packet.getTo().getNode();
            // If no TO was specified then return an error.
            if ( roomName == null )
            {
                Log.debug("vCard retrieve request from: '{}', for: '{}' is invalid: it does not refer to a specific room.", packet.getFrom(), packet.getTo());
                result.setChildElement(packet.getChildElement().createCopy());
                result.setError(PacketError.Condition.not_acceptable);
                result.getError().setText("Request 'to' attribute has no node-part. The request should be addressed to a room of a MUC service.");
            }
            else
            {
                // By default return an empty vCard
                result.setChildElement(RESPONSE_ELEMENT_NAME, NAMESPACE);
                // Only try to get the vCard values of rooms that can be discovered
                // Answer the room occupants as items if that info is publicly available
                final MUCRoom room = mucService.getChatRoom(roomName);
                Log.debug("vCard retrieve request from: '{}', for: '{}' relates to room: {}", packet.getFrom(), packet.getTo(), room);

                if ( room != null && mucService.canDiscoverRoom(room, packet.getFrom()) )
                {
                    VCardManager vManager = VCardManager.getInstance();
                    Element userVCard = vManager.getVCard(room.getJID().toString());
                    if ( userVCard != null )
                    {
                        // Check if the requester wants to ignore some vCard's fields
                        Element filter = packet.getChildElement().element(QName.get("filter", "vcard-temp-filter"));
                        if ( filter != null )
                        {
                            // Create a copy so we don't modify the original vCard
                            userVCard = userVCard.createCopy();
                            // Ignore fields requested by the user
                            for ( Iterator<Element> toFilter = filter.elementIterator(); toFilter.hasNext(); )
                            {
                                Element field = toFilter.next();
                                Element fieldToRemove = userVCard.element(field.getName());
                                if ( fieldToRemove != null )
                                {
                                    fieldToRemove.detach();
                                }
                            }
                        }
                        result.setChildElement(userVCard);
                        Log.debug("vCard retrieve request from: '{}', for: '{}' processed successfully.", packet.getFrom(), packet.getTo());
                    }
                }
                else
                {
                    Log.debug("vCard retrieve request from: '{}', for: '{}' is invalid: room does not exist, or sender is not allowed to discover the room.", packet.getFrom(), packet.getTo());
                    result = IQ.createResultIQ(packet);
                    result.setChildElement(packet.getChildElement().createCopy());
                    result.setError(PacketError.Condition.item_not_found);
                    result.getError().setText("Request 'to' references a room that cannot be found (or is not discoverable by you).");
                }
            }
        }
        else
        {
            // Ignore non-request IQs
            return null;
        }
        return result;
    }

    private void sendVCardUpdateNotification( final MUCRoom room, String hash )
    {
        Log.debug("Sending vcard-temp update notification to all occupants of room {}, using hash {}", room.getName(), hash);
        final Presence notification = new Presence();
        notification.setFrom(room.getJID());
        final Element x = notification.addChildElement("x", "vcard-temp:x:update");
        final Element photo = x.addElement("photo");
        photo.setText(hash);

        for ( final MUCRole occupant : room.getOccupants() )
        {
            occupant.send(notification);
        }
    }

    private void sendConfigChangeNotification( final MUCRoom room )
    {
        Log.debug("Sending configuration change notification to all occupants of room {}", room.getName());
        final Message notification = new Message();
        notification.setType(Message.Type.groupchat);
        notification.setFrom(room.getJID());
        final Element x = notification.addChildElement("x", "http://jabber.org/protocol/muc#user");
        final Element status = x.addElement("status");
        status.addAttribute("code", "104");

        for ( final MUCRole occupant : room.getOccupants() )
        {
            occupant.send(notification);
        }
    }

    public static String calculatePhotoHash( Element vcard )
    {
        if ( vcard.element("PHOTO") == null )
        {
            return "";
        }
        final Element element = vcard.element("PHOTO").element("BINVAL");
        if ( element == null )
        {
            return "";
        }

        final byte[] photo = Base64.decode(element.getTextTrim());
        return StringUtils.hash(photo, "SHA-1");
    }

    @Override
    public IQHandlerInfo getInfo()
    {
        return info;
    }
}