/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015 NBCO Yandex.Money LLC
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package ru.yandex.money.android;

import android.annotation.SuppressLint;
import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;

import com.yandex.money.api.methods.InstanceId;
import com.yandex.money.api.methods.payment.BaseProcessPayment;
import com.yandex.money.api.methods.payment.BaseRequestPayment;
import com.yandex.money.api.methods.payment.ProcessExternalPayment;
import com.yandex.money.api.methods.payment.RequestExternalPayment;
import com.yandex.money.api.methods.payment.params.PaymentParams;
import com.yandex.money.api.methods.payment.params.ShopParams;
import com.yandex.money.api.model.Error;
import com.yandex.money.api.model.ExternalCard;
import com.yandex.money.api.model.MoneySource;
import com.yandex.money.api.net.clients.ApiClient;
import com.yandex.money.api.net.clients.DefaultApiClient;
import com.yandex.money.api.net.providers.DefaultApiV1HostsProvider;
import com.yandex.money.api.processes.ExternalPaymentProcess;

import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import ru.yandex.money.android.database.DatabaseStorage;
import ru.yandex.money.android.fragments.CardsFragment;
import ru.yandex.money.android.fragments.CscFragment;
import ru.yandex.money.android.fragments.ErrorFragment;
import ru.yandex.money.android.fragments.SuccessFragment;
import ru.yandex.money.android.fragments.WebFragment;
import ru.yandex.money.android.parcelables.ExternalCardParcelable;
import ru.yandex.money.android.parcelables.ExternalPaymentProcessSavedStateParcelable;
import ru.yandex.money.android.utils.Keyboards;

/**
 * <p>Main activity for a payment process. It guides a user through all payment steps and returns information whether
 * payment was successful or not.</p>
 *
 * <p>To explicitly start this activity you may want to use {@link #getBuilder(Context)} method to create an
 * {@link Intent} object, that can be passed to {@link #startActivity(Intent)} or
 * {@link #startActivityForResult(Intent, int)} methods. See the description of allowed parameters in
 * {@link PaymentParamsBuilder} and {@link Builder} interfaces.</p>
 *
 * <p>If the activity was started using {@link #startActivityForResult(Intent, int)} method, then it will return result
 * of a payment to a calling activity. If payment was successful, then result code will be {@link #RESULT_OK} and
 * {@link #EXTRA_INVOICE_ID} will be present in returned {@code data} object. If payment was canceled by a user or
 * rejected by payment system, then result code {@link #RESULT_CANCELED} will be returned.</p>
 */
public final class PaymentActivity extends Activity implements ExternalPaymentProcess.ParameterProvider {

    /**
     * An instance of {@link String} representing instance id.
     */
    public static final String EXTRA_INVOICE_ID = "ru.yandex.money.android.extra.INVOICE_ID";

    private static final String EXTRA_ARGUMENTS = "ru.yandex.money.android.extra.ARGUMENTS";
    private static final String EXTRA_HOST = "ru.yandex.money.android.extra.HOST";
    private static final String EXTRA_CLIENT_ID = "ru.yandex.money.android.extra.CLIENT_ID";

    private static final String KEY_PROCESS_SAVED_STATE = "processSavedState";
    private static final String KEY_SELECTED_CARD = "selectedCard";

    private static final String PRODUCTION_HOST = "https://money.yandex.ru";

    private final ExecutorService backgroundService = Executors.newSingleThreadExecutor();

    private ExternalPaymentProcess process;

    private PaymentParams arguments;

    private DatabaseStorage databaseStorage;
    private List<ExternalCard> cards;

    private boolean immediateProceed = true;

    @Nullable
    private ExternalCard selectedCard;
    @Nullable
    private AsyncTask<Void, Void, ?> task;

    private boolean isStateSaved = false;

    /**
     * Returns intent builder used for launch this activity.
     *
     * @param context current context
     * @return intent builder
     */
    @NonNull
    public static PaymentParamsBuilder getBuilder(@NonNull Context context) {
        return new IntentBuilder(context);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); // todo show ongoing progress some other way
        setContentView(R.layout.ym_payment_activity);

        ActionBar actionBar = getActionBar();
        if (actionBar != null) {
            actionBar.setDisplayHomeAsUpEnabled(true);
        }

        // we hide progress bar because on some devices we have it shown right from the start
        hideProgressBar();

        arguments = PaymentExtras.fromBundle(getIntent().getBundleExtra(EXTRA_ARGUMENTS));

        databaseStorage = new DatabaseStorage(this);
        cards = databaseStorage.selectExternalCards();

        if (!initPaymentProcess()) return;

        if (savedInstanceState == null) {
            proceed();
        } else {
            ExternalPaymentProcessSavedStateParcelable savedStateParcelable =
                    savedInstanceState.getParcelable(KEY_PROCESS_SAVED_STATE);
            if (savedStateParcelable != null) {
                process.restoreSavedState(savedStateParcelable.value);
            }

            ExternalCardParcelable externalCardParcelable = savedInstanceState.getParcelable(KEY_SELECTED_CARD);
            if (externalCardParcelable != null) {
                selectedCard = externalCardParcelable.value;
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        isStateSaved = true;
        outState.putParcelable(KEY_PROCESS_SAVED_STATE,
                new ExternalPaymentProcessSavedStateParcelable(process.getSavedState()));
        if (selectedCard != null) {
            outState.putParcelable(KEY_SELECTED_CARD, new ExternalCardParcelable(selectedCard));
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        isStateSaved = false;
    }

    @Override
    protected void onResume() {
        super.onResume();
        isStateSaved = false;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                hideKeyboard();
                onBackPressed();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onBackPressed() {
        cancel();
        applyResult();

        Fragment fragment = getCurrentFragment();
        super.onBackPressed();

        Fragment currentFragment = getCurrentFragment();
        if (currentFragment instanceof CscFragment) {
            super.onBackPressed();
            currentFragment = getCurrentFragment();
        }
        if (fragment instanceof WebFragment && currentFragment instanceof CardsFragment) {
            if (cards.size() == 0) {
                immediateProceed = false;
            }
            getFragmentManager()
                    .beginTransaction()
                    .remove(currentFragment)
                    .commit();
            reset();
        }
    }

    @Override
    public String getPatternId() {
        return arguments.patternId;
    }

    @Override
    public Map<String, String> getPaymentParameters() {
        return arguments.paymentParams;
    }

    @Override
    public MoneySource getMoneySource() {
        return selectedCard;
    }

    @Override
    public String getCsc() {
        Fragment fragment = getCurrentFragment();
        return fragment instanceof CscFragment ? ((CscFragment) fragment).getCsc() : null;
    }

    @Override
    public String getExtAuthSuccessUri() {
        return Constants.EXT_AUTH_SUCCESS_URI;
    }

    @Override
    public String getExtAuthFailUri() {
        return Constants.EXT_AUTH_FAIL_URI;
    }

    @Override
    public boolean isRequestToken() {
        Fragment fragment = getCurrentFragment();
        return fragment instanceof SuccessFragment;
    }

    /**
     * Gets an instance of {@link DatabaseStorage}.
     *
     * @return instance of {@link DatabaseStorage}
     */
    @NonNull
    public DatabaseStorage getDatabaseStorage() {
        return databaseStorage;
    }

    /**
     * Gets list of saved cards.
     *
     * @return list of saved card
     */
    @NonNull
    public List<ExternalCard> getCards() {
        return cards;
    }

    /**
     * Shows {@link WebFragment} clearing back stack if needed.
     *
     * @param url url to open
     * @param postData data to post
     */
    public void showWeb(@NonNull String url, @NonNull Map<String, String> postData) {
        Fragment fragment = getCurrentFragment();
        boolean clearBackStack = !(fragment instanceof CardsFragment || fragment instanceof CscFragment);
        replaceFragment(WebFragment.newInstance(url, postData), clearBackStack);
    }

    /**
     * Shows {@link CardsFragment}.
     */
    public void showCards() {
        BaseRequestPayment rep = process.getRequestPayment();
        replaceFragment(CardsFragment.newInstance(rep.title, rep.contractAmount), true);
    }

    /**
     * Shows {@link ErrorFragment}.
     *
     * @param error known error
     * @param status known status
     */
    public void showError(@Nullable Error error, @Nullable String status) {
        replaceFragment(ErrorFragment.newInstance(error, status), true);
    }

    /**
     * Shows {@link ErrorFragment} for unknown error or status.
     */
    public void showUnknownError() {
        replaceFragment(ErrorFragment.newInstance(), true);
    }

    /**
     * Shows {@link SuccessFragment}.
     *
     * @param moneySource saved card that was used to do a payment or {@code null}
     */
    public void showSuccess(@Nullable ExternalCard moneySource) {
        replaceFragment(SuccessFragment.newInstance(process.getRequestPayment().contractAmount, moneySource), true);
    }

    /**
     * Shows {@link CscFragment}.
     *
     * @param externalCard selected card
     */
    public void showCsc(@NonNull ExternalCard externalCard) {
        selectedCard = externalCard;
        replaceFragment(CscFragment.newInstance(externalCard), false);
    }

    /**
     * Shows progress bar.
     */
    public void showProgressBar() {
        setProgressBarIndeterminateVisibility(true);
    }

    /**
     * Hides progress bar.
     */
    public void hideProgressBar() {
        setProgressBarIndeterminateVisibility(false);
    }

    /**
     * Proceeds to the next step of a payment.
     */
    public void proceed() {
        task = performPaymentOperation(process::proceed);
    }

    /**
     * Repeats current step of a payment.
     */
    public void repeat() {
        task = performPaymentOperation(process::repeat);
    }

    /**
     * Resets current payment process.
     */
    public void reset() {
        selectedCard = null;
        process.reset();
        proceed();
    }

    /**
     * Cancels payment process.
     */
    public void cancel() {
        selectedCard = null;
        if (task != null && task.getStatus() == AsyncTask.Status.RUNNING) {
            task.cancel(true);
            task = null;
        }
    }

    @NonNull
    private AsyncTask<Void, Void, OperationResult<Boolean>> performPaymentOperation(
            @NonNull Callable<Boolean> operation) {
        return perform(operation, aBoolean -> handleProcess());
    }

    @NonNull
    private <T> AsyncTask<Void, Void, OperationResult<T>> perform(
            @NonNull Callable<T> operation, @NonNull Consumer<T> consumer) {

        showProgressBar();
        return new AsyncTask<Void, Void, OperationResult<T>>() {
            @Override
            protected OperationResult<T> doInBackground(Void... params) {
                try {
                    return new OperationResult<>(operation.call());
                } catch (Exception e) {
                    return new OperationResult<>(e);
                }
            }

            @Override
            protected void onPostExecute(OperationResult<T> result) {
                if (isCancelled()) return;
                if (result.operation != null) {
                    consumer.consume(result.operation);
                    hideProgressBar();
                } else {
                    onOperationFailed();
                }
            }
        }.executeOnExecutor(backgroundService);
    }

    private void handleProcess() {
        BaseProcessPayment processPayment = process.getProcessPayment();
        if (processPayment != null) {
            onExternalPaymentProcessed((ProcessExternalPayment) processPayment);
            return;
        }

        BaseRequestPayment requestPayment = process.getRequestPayment();
        if (requestPayment != null) {
            onExternalPaymentReceived((RequestExternalPayment) requestPayment);
        }
    }

    private boolean initPaymentProcess() {
        final Intent intent = getIntent();
        final String clientId = intent.getStringExtra(EXTRA_CLIENT_ID);

        final String host = intent.getStringExtra(EXTRA_HOST);
        final ApiClient client = new DefaultApiClient.Builder()
                .setClientId(clientId)
                .setHostsProvider(new DefaultApiV1HostsProvider(true) {
                    @Override
                    public String getMoney() {
                        return host;
                    }
                })
                .setDebugMode(!PRODUCTION_HOST.equals(host))
                .create();

        process = new ExternalPaymentProcess(client, this);

        final Prefs prefs = new Prefs(this);
        String instanceId = prefs.restoreInstanceId();
        if (TextUtils.isEmpty(instanceId)) {
            perform(() -> client.execute(new InstanceId.Request(clientId)), response -> {
                if (response.isSuccessful()) {
                    prefs.storeInstanceId(response.instanceId);
                    process.setInstanceId(response.instanceId);
                    proceed();
                } else {
                    showError(response.error, response.status.toString());
                }
            });
            return false;
        }

        process.setInstanceId(instanceId);
        return true;
    }

    private void onExternalPaymentReceived(@NonNull RequestExternalPayment rep) {
        if (rep.status == BaseRequestPayment.Status.SUCCESS) {
            if (immediateProceed && cards.size() == 0) {
                proceed();
            } else {
                showCards();
            }
        } else {
            showError(rep.error, rep.status.toString());
        }
    }

    private void onExternalPaymentProcessed(@NonNull ProcessExternalPayment pep) {
        switch (pep.status) {
            case SUCCESS:
                Fragment fragment = getCurrentFragment();
                if (!(fragment instanceof SuccessFragment)) {
                    showSuccess((ExternalCard) getMoneySource());
                } else if (pep.externalCard != null) {
                    ((SuccessFragment) fragment).saveCard(pep.externalCard);
                }
                break;
            case EXT_AUTH_REQUIRED:
                showWeb(pep.acsUri, pep.acsParams);
                break;
            default:
                showError(pep.error, pep.status.toString());
        }
    }

    void onOperationFailed() {
        showUnknownError();
        hideProgressBar();
    }

    private void replaceFragment(@Nullable Fragment fragment, boolean clearBackStack) {
        if (fragment == null || isStateSaved) {
            return;
        }

        Fragment currentFragment = getCurrentFragment();
        FragmentManager manager = getFragmentManager();
        if (clearBackStack) {
            manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }

        @SuppressLint("CommitTransaction")
        FragmentTransaction transaction = manager.beginTransaction()
                .replace(R.id.ym_container, fragment);
        if (!clearBackStack && currentFragment != null) {
            transaction.addToBackStack(fragment.getTag());
        }
        transaction.commit();
        hideKeyboard();
    }

    @Nullable
    private Fragment getCurrentFragment() {
        return getFragmentManager().findFragmentById(R.id.ym_container);
    }

    private void hideKeyboard() {
        final View currentFocus = getCurrentFocus();
        if (currentFocus != null) {
            currentFocus.clearFocus();
        }
    }

    private void applyResult() {
        BaseProcessPayment pp = process.getProcessPayment();
        if (pp != null && pp.status == BaseProcessPayment.Status.SUCCESS) {
            Intent intent = new Intent();
            intent.putExtra(EXTRA_INVOICE_ID, pp.invoiceId);
            setResult(RESULT_OK, intent);
        } else {
            setResult(RESULT_CANCELED);
        }
    }

    /**
     * Implementations of this interface sets payment parameters.
     */
    @SuppressWarnings("WeakerAccess")
    public interface PaymentParamsBuilder extends Builder {

        /**
         * Sets raw payment parameters.
         *
         * @param patternId pattern id
         * @param paymentParams payment parameters
         * @return {@link Builder}
         */
        @NonNull
        Builder setPaymentParams(@NonNull String patternId, @NonNull Map<String, String> paymentParams);

        /**
         * Sets payment parameters.
         *
         * @param paymentParams instance of {@link PaymentParams}
         * @return {@link Builder}
         */
        @NonNull
        Builder setPaymentParams(@Nullable PaymentParams paymentParams);
    }

    /**
     * Implementation of this interface add additional info
     */
    public interface Builder {

        /**
         * Sets client id.
         *
         * @param clientId client id
         * @return itself
         */
        @NonNull
        Builder setClientId(@Nullable String clientId);

        /**
         * Sets desired host for testing purposes.
         *
         * @param host host to use
         * @return itself
         */
        @NonNull
        Builder setHost(@Nullable String host);

        /**
         * Creates an intent that can be used to start payment process.
         *
         * @return {@link Intent} object
         */
        @NonNull
        Intent build();
    }

    private final static class IntentBuilder implements PaymentParamsBuilder {

        @NonNull
        private final Context context;

        private PaymentParams params;

        private String host = PRODUCTION_HOST;
        private String clientId;

        IntentBuilder(@NonNull Context context) {
            this.context = context;
        }

        @NonNull
        @Override
        public Builder setPaymentParams(@NonNull String patternId,
                                                   @NonNull Map<String, String> paymentParams) {
            this.params = new ShopParams(patternId, paymentParams);
            return this;
        }

        @NonNull
        @Override
        public Builder setPaymentParams(@Nullable PaymentParams paymentParams) {
            this.params = paymentParams;
            return this;
        }

        @NonNull
        @Override
        public Builder setClientId(@Nullable String clientId) {
            this.clientId = clientId;
            return this;
        }

        @NonNull
        @Override
        public Builder setHost(@Nullable String host) {
            this.host = host;
            return this;
        }

        @NonNull
        @Override
        public Intent build() {
            return createIntent()
                    .putExtra(EXTRA_ARGUMENTS, PaymentExtras.toBundle(params))
                    .putExtra(EXTRA_HOST, host)
                    .putExtra(EXTRA_CLIENT_ID, clientId);
        }

        @NonNull
        private Intent createIntent() {
            return new Intent(context, PaymentActivity.class);
        }
    }

    private interface Consumer<T> {
        void consume(T value);
    }

    private static final class OperationResult<T> {

        @Nullable
        final T operation;
        @Nullable
        final Exception exception;

        OperationResult(@Nullable T operation) {
            this.operation = operation;
            this.exception = null;
        }

        OperationResult(@Nullable Exception exception) {
            this.operation = null;
            this.exception = exception;
        }
    }
}