/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * 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.android.ims.rcs.uce.presence.publish;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.telephony.ims.RcsUceAdapter;
import android.telephony.ims.SipDetails;
import android.telephony.ims.aidl.IPublishResponseCallback;
import android.telephony.ims.stub.RcsCapabilityExchangeImplBase;
import android.text.TextUtils;
import android.util.Log;

import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
import com.android.ims.rcs.uce.util.NetworkSipCode;
import com.android.ims.rcs.uce.util.UceUtils;

import java.time.Instant;
import java.util.Optional;

/**
 * Receiving the result callback of the publish request.
 */
public class PublishRequestResponse {

    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishRequestResp";

    private final long mTaskId;
    private final String mPidfXml;
    private volatile boolean mNeedRetry;
    private volatile PublishControllerCallback mPublishCtrlCallback;

    private Optional<Integer> mCmdErrorCode;
    private Optional<Integer> mNetworkRespSipCode;
    private Optional<String> mReasonPhrase;
    private Optional<Integer> mReasonHeaderCause;
    private Optional<String> mReasonHeaderText;
    private Optional<SipDetails> mSipDetails;
    // The timestamp when receive the response from the network.
    private Instant mResponseTimestamp;

    public PublishRequestResponse(PublishControllerCallback publishCtrlCallback, long taskId,
            String pidfXml) {
        mTaskId = taskId;
        mPidfXml = pidfXml;
        mPublishCtrlCallback = publishCtrlCallback;
        mCmdErrorCode = Optional.empty();
        mNetworkRespSipCode = Optional.empty();
        mReasonPhrase = Optional.empty();
        mReasonHeaderCause = Optional.empty();
        mReasonHeaderText = Optional.empty();
        mSipDetails = Optional.empty();
    }

    public PublishRequestResponse(String pidfXml, @Nullable SipDetails details) {
        mTaskId = 0L;
        mPublishCtrlCallback = null;
        mCmdErrorCode = Optional.empty();

        mPidfXml = pidfXml;
        mResponseTimestamp = Instant.now();
        mNetworkRespSipCode = Optional.of(details.getResponseCode());
        mReasonPhrase = Optional.ofNullable(details.getResponsePhrase());
        if (details.getReasonHeaderCause() != 0) {
            mReasonHeaderCause = Optional.of(details.getReasonHeaderCause());
        } else {
            mReasonHeaderCause = Optional.empty();
        }
        if (TextUtils.isEmpty(details.getReasonHeaderText())) {
            mReasonHeaderText = Optional.empty();
        } else {
            mReasonHeaderText = Optional.ofNullable(details.getReasonHeaderText());
        }
        mSipDetails = Optional.ofNullable(details);
    }

    // The result callback of the publish capability request.
    private IPublishResponseCallback mResponseCallback = new IPublishResponseCallback.Stub() {
        @Override
        public void onCommandError(int code) {
            PublishRequestResponse.this.onCommandError(code);
        }

        @Override
        public void onNetworkResponse(@NonNull SipDetails details) {
            PublishRequestResponse.this.onNetworkResponse(details);
        }
    };

    public IPublishResponseCallback getResponseCallback() {
        return mResponseCallback;
    }

    public long getTaskId() {
        return mTaskId;
    }

    /**
     * Retrieve the command error code which received from the network.
     */
    public Optional<Integer> getCmdErrorCode() {
        return mCmdErrorCode;
    }

    /**
     * Retrieve the network response sip code which received from the network.
     */
    public Optional<Integer> getNetworkRespSipCode() {
        return mNetworkRespSipCode;
    }

    /**
     * Retrieve the reason phrase of the network response which received from the network.
     */
    public Optional<String> getReasonPhrase() {
        return mReasonPhrase;
    }

    /**
     * Retrieve the reason header from the network response.
     */
    public Optional<Integer> getReasonHeaderCause() {
        return mReasonHeaderCause;
    }

    /**
     * Retrieve the description of the reason header.
     */
    public Optional<String> getReasonHeaderText() {
        return mReasonHeaderText;
    }

    /**
     * Retrieve the sip information which received from the network.
     */
    public Optional<SipDetails> getSipDetails() {
        return mSipDetails;
    }
    /**
     * Retrieve the SIP code from the network response. It will get the value from the Reason
     * Header first. If the ReasonHeader is not present, it will get the value from the Network
     * response instead.
     */
    public Optional<Integer> getResponseSipCode() {
        return (mReasonHeaderCause.isPresent()) ? mReasonHeaderCause : mNetworkRespSipCode;
    }

    /**
     * Retrieve the REASON from the network response. It will get the value from the Reason Header
     * first. If the ReasonHeader is not present, it will get the value from the Network response
     * instead.
     */
    public Optional<String> getResponseReason() {
        return (mReasonHeaderText.isPresent()) ? mReasonHeaderText : mReasonPhrase;
    }

    /**
     * Get the timestamp of receiving the network response callback.
     */
    public @Nullable Instant getResponseTimestamp() {
        return mResponseTimestamp;
    }

    /**
     * @return the PIDF XML sent during this request.
     */
    public String getPidfXml() {
        return mPidfXml;
    }

    public void onDestroy() {
        mPublishCtrlCallback = null;
    }

    private void onCommandError(int errorCode) {
        mResponseTimestamp = Instant.now();
        mCmdErrorCode = Optional.of(errorCode);
        updateRetryFlagByCommandError();

        PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
        if (ctrlCallback != null) {
            ctrlCallback.onRequestCommandError(this);
        } else {
            Log.d(LOG_TAG, "onCommandError: already destroyed. error code=" + errorCode);
        }
    }

    private void onNetworkResponse(@NonNull SipDetails details) {
        // When we send a request to PUBLISH and there is no change to the UCE capabilities, we
        // expected onCommandError() with COMMAND_CODE_NO_CHANGE.
        // But some of the vendor will instead send SIP code 999.
        if (details.getResponseCode() == 999) {
            onCommandError(RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE);
            return;
        }
        mResponseTimestamp = Instant.now();
        mNetworkRespSipCode = Optional.of(details.getResponseCode());
        mReasonPhrase = Optional.ofNullable(details.getResponsePhrase());
        if (details.getReasonHeaderCause() != 0) {
            mReasonHeaderCause = Optional.of(details.getReasonHeaderCause());
        }
        if (TextUtils.isEmpty(details.getReasonHeaderText())) {
            mReasonHeaderText = Optional.empty();
        } else {
            mReasonHeaderText = Optional.ofNullable(details.getReasonHeaderText());
        }
        mSipDetails = Optional.ofNullable(details);
        updateRetryFlagByNetworkResponse();

        PublishControllerCallback ctrlCallback = mPublishCtrlCallback;
        if (ctrlCallback != null) {
            ctrlCallback.onRequestNetworkResp(this);
        } else {
            Log.d(LOG_TAG, "onNetworkResponse: already destroyed. sip code="
                    + details.getResponseCode());
        }
    }

    private void updateRetryFlagByCommandError() {
        switch(getCmdErrorCode().orElse(-1)) {
            case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT:
            case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY:
            case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION:
            case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE:
                mNeedRetry = true;
                break;
        }
    }

    private void updateRetryFlagByNetworkResponse() {
        // Disable retry flag because the retry mechanism is implemented in the ImsService.
        mNeedRetry = false;
    }

    /*
     * Check whether the publishing request is successful.
     */
    public boolean isRequestSuccess() {
        if (isCommandError()) {
            return false;
        }
        // The result of the request was treated as successful if the command error code is present
        // and its value is COMMAND_CODE_NO_CHANGE.
        if (isCommandCodeNoChange()) {
            return true;
        }

        final int sipCodeOk = NetworkSipCode.SIP_CODE_OK;
        if (getNetworkRespSipCode().filter(c -> c == sipCodeOk).isPresent() &&
                (!getReasonHeaderCause().isPresent()
                        || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) {
            return true;
        }
        return false;
    }

    /**
     * Check if the PUBLISH request is failed with receiving the command error.
     * @return true if the command is failure.
     */
    private boolean isCommandError() {
        // The request is failed if the command error code is present and its value is not
        // COMMAND_CODE_NO_CHANGE.
        if (getCmdErrorCode().isPresent() && !isCommandCodeNoChange()) {
            return true;
        }
        return false;
    }

    // @return true If it received the command code COMMAND_CODE_NO_CHANGE
    private boolean isCommandCodeNoChange() {
        if (getCmdErrorCode().filter(code ->
                code == RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE).isPresent()) {
            return true;
        }
        return false;
    }

    /**
     * Check whether the publishing request needs to be retried.
     */
    public boolean needRetry() {
        return mNeedRetry;
    }

    /**
     * @return The publish state when the publish request is finished.
     */
     public int getPublishState() {
         if (isCommandError()) {
             return getPublishStateByCmdErrorCode();
         } else {
             return getPublishStateByNetworkResponse();
         }
     }

    /**
     * Convert the command error code to the publish state
     */
    private int getPublishStateByCmdErrorCode() {
        if (getCmdErrorCode().orElse(-1) ==
                RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT) {
            return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
        }
        return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
    }

    /**
     * Convert the network sip code to the publish state
     */
    private int getPublishStateByNetworkResponse() {
        int respSipCode;
        if (isCommandCodeNoChange()) {
            // If the command code is COMMAND_CODE_NO_CHANGE, it should be treated as successful.
            respSipCode = NetworkSipCode.SIP_CODE_OK;
        } else if (getReasonHeaderCause().isPresent()) {
            respSipCode = getReasonHeaderCause().get();
        } else {
            respSipCode = getNetworkRespSipCode().orElse(-1);
        }

        switch (respSipCode) {
            case NetworkSipCode.SIP_CODE_OK:
                return RcsUceAdapter.PUBLISH_STATE_OK;
            case NetworkSipCode.SIP_CODE_FORBIDDEN:
            case NetworkSipCode.SIP_CODE_NOT_FOUND:
            case NetworkSipCode.SIP_CODE_SERVER_TIMEOUT:
                return RcsUceAdapter.PUBLISH_STATE_RCS_PROVISION_ERROR;
            case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT:
                return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT;
            default:
                return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
        }
    }

    /**
     * Get the information of the publish request response.
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("taskId=").append(mTaskId)
                .append(", CmdErrorCode=").append(getCmdErrorCode().orElse(-1))
                .append(", NetworkRespSipCode=").append(getNetworkRespSipCode().orElse(-1))
                .append(", ReasonPhrase=").append(getReasonPhrase().orElse(""))
                .append(", ReasonHeaderCause=").append(getReasonHeaderCause().orElse(-1))
                .append(", ReasonHeaderText=").append(getReasonHeaderText().orElse(""))
                .append(", ResponseTimestamp=").append(mResponseTimestamp)
                .append(", isRequestSuccess=").append(isRequestSuccess())
                .append(", needRetry=").append(mNeedRetry);
        return builder.toString();
    }
}
