/*
 * Copyright 2006-2021 Prowide
 *
 * 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 com.prowidesoftware.swift.model;


import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.prowidesoftware.swift.model.mx.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

import javax.persistence.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Calendar;
import java.util.Objects;
import java.util.Optional;

/**
 * MX (ISO 20022) messages entity for JPA persistence.
 *
 * <p>The class holds the full xml content plus message identification metadata gathered from the application header.
 *
 * <p>Notice, the scope of Prowide MX model is the message payload (the actual message or body data) which is the fundamental
 * purpose of the transmission. The transmission wrappers (overhead data) are excluded and intentionally ignored if found.
 *
 * <p>MX messages are uniquely identify by their business process, message functionality, variant and version.<br>
 * Consider the following example: trea.001.001.02
 * <ul>
 * <li>trea refers to 'Treasury'</li>
 * <li>001 refers to 'NDF opening (notification)'</li>
 * <li>001 refers to the variant</li>
 * <li>02 refers to the version message format, in this case version 2 of 'NDF opening' type.</li>
 * </ul>
 *
 * <p>
 * <em>businessProcess</em>: Alphabetic code in four positions (fixed length) identifying the Business Process
 * <br>
 * <em>functionality</em>: Alphanumeric code in three positions (fixed length) identifying the Message Functionality
 * <br>
 * <em>variant</em>: Numeric code in three positions (fixed length) identifying a particular flavor (variant) of Message Functionality
 * <br>
 * <em>version</em>: Numeric code in two positions (fixed length) identifying the version.
 *
 * @since 7.0
 */
@Entity(name = "mx")
@DiscriminatorValue("mx")
public class MxSwiftMessage extends AbstractSwiftMessage {
    private static final long serialVersionUID = -4394356007627575831L;
    private static final transient java.util.logging.Logger log = java.util.logging.Logger.getLogger(MxSwiftMessage.class.getName());

    @Enumerated(EnumType.STRING)
    @Column(length = 4, name = "business_process")
    private MxBusinessProcess businessProcess;

    @Column(length = 3)
    private String functionality;

    @Column(length = 3)
    private String variant;

    @Column(length = 2)
    private String version;

    public MxSwiftMessage() {
        super();
    }

    /**
     * Calls {@link #MxSwiftMessage(String, MessageMetadataStrategy)} with the {@link DefaultMxMetadataStrategy}
     */
    public MxSwiftMessage(final String xml) {
        this(xml, new DefaultMxMetadataStrategy());
    }

    /**
     * Creates a new message reading the message the content from a string.
     *
     * <p>Performs a fast parsing of the header to identify the message.
     * <p>
     * If the string contains several messages, the whole passed content will be save in the message attribute but
     * identification and metadata will be parser from the first one found only.
     *
     * @param xml              the plain ISO 20022 XML content, with or without the optional header
     * @param metadataStrategy a strategy for metadata extraction
     * @since 9.1.6
     */
    public MxSwiftMessage(final String xml, final MessageMetadataStrategy metadataStrategy) {
        super(xml, FileFormat.MX, metadataStrategy);
    }

    /**
     * Calls {@link #MxSwiftMessage(InputStream, MessageMetadataStrategy)} with the {@link DefaultMxMetadataStrategy}
     *
     * @since 7.7
     */
    public MxSwiftMessage(final InputStream stream) throws IOException {
        this(stream, new DefaultMxMetadataStrategy());
    }

    /**
     * Creates a new message reading the message the content from an input stream.
     *
     * @param stream           a stream containing the XML message
     * @param metadataStrategy a strategy for metadata extraction
     * @since 9.1.6
     */
    public MxSwiftMessage(final InputStream stream, final MessageMetadataStrategy metadataStrategy) throws IOException {
        super(stream, FileFormat.MX, metadataStrategy);
    }

    /**
     * Calls {@link #MxSwiftMessage(File, MessageMetadataStrategy)} with the {@link DefaultMxMetadataStrategy}
     *
     * @since 7.7
     */
    public MxSwiftMessage(final File file) throws IOException {
        this(file, new DefaultMxMetadataStrategy());
    }

    /**
     * Creates a new message reading the message the content from a file.
     *
     * @param file             an existing file containing the XML
     * @param metadataStrategy a strategy for metadata extraction
     * @since 9.1.6
     */
    public MxSwiftMessage(final File file, final MessageMetadataStrategy metadataStrategy) throws IOException {
        super(file, FileFormat.MX, metadataStrategy);
    }

    /**
     * Calls {@link #MxSwiftMessage(AbstractMX, MessageMetadataStrategy)} with the {@link DefaultMxMetadataStrategy}
     *
     * @param mx a message object
     */
    public MxSwiftMessage(final AbstractMX mx) {
        this(mx, new DefaultMxMetadataStrategy());
    }

    /**
     * Creates a new message serializing to xml the parameter message object.
     *
     * <p>If the business header is present, the sender and receiver attributes will be set with content from the
     * header; also the internal raw XML will include both 'AppHdr' and 'Document' under a default root element tag
     * as returned by {@link AbstractMX#message()}
     *
     * <br>If the header is not present, sender and receiver will be left null and the raw internal XML will include
     * just the 'Document' element.
     *
     * @param mx               a message object
     * @param metadataStrategy a strategy for metadata extraction
     * @since 9.1.6
     */
    public MxSwiftMessage(final AbstractMX mx, final MessageMetadataStrategy metadataStrategy) {
        // instead of reusing the constructor from XML with mx.message() as parameter
        // we set the message and run the update directly to avoid an unnecessary message type detection
        Validate.notNull(mx, "the message model cannot be null");
        Validate.notNull(metadataStrategy, "the strategy for metadata extraction cannot be null");
        setMessage(mx.message());
        _updateFromMessage(mx.getMxId(), metadataStrategy);
    }

    /**
     * Creates a new message reading the message the content from a string.
     * This is a static version of the constructor {@link #MxSwiftMessage(String)}
     *
     * @since 7.7
     */
    public static MxSwiftMessage parse(final String xml) {
        return new MxSwiftMessage(xml);
    }

    /**
     * Creates a new message reading the message the content from an input stream.
     * This is a static version of the constructor {@link #MxSwiftMessage(InputStream)}
     *
     * @since 7.7
     */
    public static MxSwiftMessage parse(final InputStream stream) throws IOException {
        return new MxSwiftMessage(stream);
    }

    /**
     * Creates a new message reading the message the content from a file.
     * This is a static version of the constructor {@link #MxSwiftMessage(File)}
     *
     * @since 7.7
     */
    public static MxSwiftMessage parse(final File file) throws IOException {
        return new MxSwiftMessage(file);
    }

    /**
     * This method deserializes the JSON data into an MX message object.
     *
     * @see #toJson()
     * @since 7.10.3
     */
    public static MxSwiftMessage fromJson(String json) {
        final Gson gson = new GsonBuilder().create();
        return gson.fromJson(json, MxSwiftMessage.class);
    }

    /**
     * Calls {@link #updateFromMessage(MessageMetadataStrategy)} with {@link DefaultMxMetadataStrategy}
     *
     * @since 7.7
     */
    @Override
    protected void updateFromMessage() {
        _updateFromMessage(null, new DefaultMxMetadataStrategy());
    }

    /**
     * Updates the object attributes with metadata parsed from the message raw content using the provided strategy
     * implementation for several of the metadata fields. The method is called during message creation or update.
     *
     * @since 9.1.6
     */
    @Override
    protected void updateFromMessage(final MessageMetadataStrategy metadataStrategy) {
        Validate.notNull(metadataStrategy, "the strategy for metadata extraction cannot be null");
        _updateFromMessage(null, metadataStrategy);
    }

    private void _updateFromMessage(final MxId id, final MessageMetadataStrategy metadataStrategy) {
        if (message() != null && message().length() > 0) {
            MxId identifier = id != null ? id : MxParseUtils.identifyMessage(this.message()).orElse(null);
            extractMetadata(identifier, getAppHdr(), metadataStrategy);
        }
    }

    /**
     * Updates the the attributes with the raw message and its metadata from the given raw (XML) message content.
     *
     * @param mx the new message content
     * @see #updateFromMessage()
     * @since 7.8.4
     */
    public void updateFromModel(final AbstractMX mx) {
        updateFromModel(mx, new DefaultMxMetadataStrategy());
    }

    public void updateFromModel(final AbstractMX mx, final MessageMetadataStrategy metadataStrategy) {
        Validate.notNull(mx, "the mx parameter cannot be null");
        Validate.notNull(metadataStrategy, "the strategy for metadata extraction cannot be null");
        setMessage(mx.message());
        setFileFormat(FileFormat.MX);
        extractMetadata(mx.getMxId(), mx.getAppHdr(), metadataStrategy);
    }

    private void extractMetadata(MxId identifier, AppHdr headerModel, MessageMetadataStrategy metadataStrategy) {
        MxNode parsedMessage = MxNode.parse(this.message());
        if (headerModel == null || !extractMetadata(headerModel)) {
            extractMetadata(parsedMessage);
        }

        if (identifier != null) {
            this.identifier = identifier.id();
            this.businessProcess = identifier.getBusinessProcess();
            this.functionality = identifier.getFunctionality();
            this.variant = identifier.getVariant();
            this.version = identifier.getVersion();
        }

        applyStrategy(getMessage(), metadataStrategy);
    }

    /**
     * Updates sender, receiver and reference from parameter header
     *
     * @return true if at least some property was updated
     */
    private boolean extractMetadata(AppHdr h) {
        boolean updated = false;
        if (h != null) {
            final String from = h.from();
            if (from != null) {
                super.sender = bic11(from);
                updated = true;
            }

            final String to = h.to();
            if (to != null) {
                super.receiver = bic11(to);
                updated = true;
            }
        }
        return updated;
    }

    /**
     * Updates sender and receiver from the group header element (only present in a subset of Mx messages)
     *
     * @return true if at least some property was updated
     */
    private boolean extractMetadata(MxNode n) {
        boolean updated = false;
        final MxNode groupHeader = n != null ? n.findFirstByName("GrpHdr") : null;
        if (groupHeader != null) {
            MxNode senderBic = groupHeader.findFirst("./InstgAgt/FinInstnId/BIC");
            if (senderBic != null) {
                sender = bic11(senderBic.getValue());
                updated = true;
            }
            MxNode receiverBic = groupHeader.findFirst("./InstdAgt/FinInstnId/BIC");
            if (receiverBic != null) {
                receiver = bic11(receiverBic.getValue());
                updated = true;
            }
        }
        return updated;
    }

    /**
     * Calls {@link #updateFromXML(String, MxId, MessageMetadataStrategy)} with {@link DefaultMxMetadataStrategy}
     *
     * @since 7.8.4
     */
    public void updateFromXML(final String xml) {
        updateFromXML(xml, null);
    }

    /**
     * Calls {@link #updateFromXML(String, MxId, MessageMetadataStrategy)} with {@link DefaultMxMetadataStrategy}
     *
     * @since 7.8.4
     */
    public void updateFromXML(final String xml, final MxId id) {
        updateFromXML(xml, id, new DefaultMxMetadataStrategy());
    }

    /**
     * Updates the the attributes with the raw message and its metadata from the given raw (XML) message content.
     * Wrapper around AppHdr/Document, if present, are preserved and ignored.
     *
     * @param xml              the XML content of an MX message containing the Document and optional AppHdr elements
     * @param id               the specific Mx type identification or null if message is unknown
     * @param metadataStrategy the strategy implementation to use for metadata extraction
     * @since 9.1.6
     */
    public void updateFromXML(final String xml, final MxId id, final MessageMetadataStrategy metadataStrategy) {
        Validate.notNull(xml, "the xml message parameter cannot be null");
        Validate.notNull(metadataStrategy, "the strategy for metadata extraction cannot be null");
        setMessage(xml);
        setFileFormat(FileFormat.MX);
        _updateFromMessage(id, metadataStrategy);
    }

    public MxBusinessProcess getBusinessProcess() {
        return businessProcess;
    }

    public void setBusinessProcess(MxBusinessProcess businessProcess) {
        this.businessProcess = businessProcess;
    }

    public String getFunctionality() {
        return functionality;
    }

    public void setFunctionality(String functionality) {
        this.functionality = functionality;
    }

    public String getVariant() {
        return variant;
    }

    public void setVariant(String variant) {
        this.variant = variant;
    }

    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        MxSwiftMessage that = (MxSwiftMessage) o;
        return businessProcess == that.businessProcess &&
                Objects.equals(functionality, that.functionality) &&
                Objects.equals(variant, that.variant) &&
                Objects.equals(version, that.version);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), businessProcess, functionality, variant, version);
    }

    /**
     * If present in the message content, returns the business header (SWIFT or ISO version)
     * Notice this header is optional and may not be present.
     *
     * @return found header or null if not present or cannot be parsed into a header object
     * @see AppHdrParser#parse(String)
     * @since 9.0.1
     */
    public AppHdr getAppHdr() {
        return AppHdrParser.parse(this.message()).orElse(null);
    }

    /**
     * Creates a full copy of the current message object into another message.
     *
     * @param msg target message
     * @see AbstractSwiftMessage#copyTo(AbstractSwiftMessage)
     * @since 7.7
     */
    public void copyTo(MxSwiftMessage msg) {
        super.copyTo(msg);
        msg.setBusinessProcess(getBusinessProcess());
        msg.setFunctionality(getFunctionality());
        msg.setVariant(getVariant());
        msg.setVersion(getVersion());
    }

    /**
     * @since 7.8.6
     */
    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("MxSwiftMessage id=").append(getId()).append(" message=").append(getMessage());
        return sb.toString();
    }

    /**
     * Returns this message MX identification
     *
     * @return the identification object for this message
     * @since 7.10.4
     */
    public MxId getMxId() {
        return new MxId(this.businessProcess, this.functionality, this.variant, this.version);
    }

    /**
     * For MT messages returns the category number and for MX messages return the business process.
     * For example for MT103 returns 1 and for pacs.004.001.06 returns pacs
     *
     * @return a string with the category or empty if the identifier is invalid or not present
     * @since 7.10.4
     */
    @Override
    public String getCategory() {
        if (!StringUtils.isBlank(this.identifier)) {
            MxBusinessProcess proc = (new MxId(this.identifier)).getBusinessProcess();
            if (proc != null) {
                return proc.name();
            }
        }
        return "";
    }

    /**
     * Enables injecting your own implementation for the entity metadata extraction, to set the generic properties
     * shared by all message types: main reference, main amount and currency, value date, trade date.
     *
     * @since 9.1.6
     */
    public void updateMetadata(MessageMetadataStrategy strategy) {
        Validate.notNull(strategy, "the strategy for metadata extraction cannot be null");
        applyStrategy(getMessage(), strategy);
    }

    private void applyStrategy(String xml, MessageMetadataStrategy strategy) {
        boolean isKnownType = this.businessProcess != null && this.functionality != null && this.variant != null && this.version != null;
        AbstractMX mx = isKnownType ? AbstractMX.parse(xml, getMxId()) : AbstractMX.parse(xml);

        if (mx == null) {
            // could not parse the XML into a message model
            return;
        }

        String reference = strategy.reference(mx).orElse(null);
        if (StringUtils.isNotBlank(reference)) {
            setReference(reference);
        }

        Optional<Money> money = strategy.amount(mx);
        if (money.isPresent()) {
            setCurrency(money.get().getCurrency());
            setAmount(money.get().getAmount());
        }

        Calendar valueDate = strategy.valueDate(mx).orElse(null);
        if (valueDate != null) {
            setValueDate(valueDate);
        }

        Calendar tradeDate = strategy.tradeDate(mx).orElse(null);
        if (tradeDate != null) {
            setTradeDate(tradeDate);
        }
    }

}