/*
 * Copyright 2025 Circle Internet Group, Inc. All rights reserved.
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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.cybavo.reactnative.wallet.service;

import android.location.Address;
import android.util.Log;

import com.cybavo.wallet.service.auth.PinSecret;
import com.cybavo.wallet.service.wallet.AddressTags;
import com.cybavo.wallet.service.wallet.EosResourceTransactionType;
import com.cybavo.wallet.service.wallet.FinancialBonus;
import com.cybavo.wallet.service.wallet.FinancialHistory;
import com.cybavo.wallet.service.wallet.FinancialProduct;
import com.cybavo.wallet.service.wallet.Transaction;
import com.cybavo.wallet.service.wallet.TransactionExplain;
import com.cybavo.wallet.service.wallet.results.TokenStandard;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableMapKeySetIterator;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public class BridgeHelper {

    private static String TAG = BridgeHelper.class.getSimpleName();

    static WritableMap objectToMap(Object object) {
        final WritableMap map = Arguments.createMap();
        for (Field field : object.getClass().getFields()) {
            try {
                final String name = field.getName();
                final Object value = field.get(object);
                if (Modifier.isStatic(field.getModifiers())) { // skip static field
                    continue;
                }
                if (!Modifier.isPublic(field.getModifiers())) { // skip non-public field
                    continue;
                }
                if (value == null) {
                    map.putNull(name);
                } else if (value instanceof Integer) {
                    map.putInt(name, (Integer) value);
                } else if (value instanceof Long) {
                    map.putDouble(name, (Long) value);
                } else if (value instanceof Float) {
                    map.putDouble(name, (Float) value);
                } else if (value instanceof Double) {
                    map.putDouble(name, (Double) value);
                } else if (value instanceof Boolean) {
                    map.putBoolean(name, (Boolean) value);
                } else if (value instanceof String) {
                    map.putString(name, (String) value);
                } else if (value.getClass().isArray()) {
                    map.putArray(name, objectToArray(value));
                } else if (value.getClass().isEnum()) {
                    if (value instanceof TokenStandard){
                        map.putInt(name, ((TokenStandard) value).getValue());
                    }else if(value instanceof FinancialProduct.Kind){
                        map.putInt(name, ((FinancialProduct.Kind) value).getValue());
                    }else if(value instanceof FinancialProduct.ListKind){
                        map.putInt(name, ((FinancialProduct.ListKind) value).getValue());
                    }else if(value instanceof FinancialHistory.ListKind){
                        map.putInt(name, ((FinancialHistory.ListKind) value).getValue());
                    }else if(value instanceof FinancialHistory.Status){
                        map.putInt(name, ((FinancialHistory.Status) value).getValue());
                    }else if(value instanceof FinancialBonus.Kind){
                        map.putInt(name, ((FinancialBonus.Kind) value).getValue());
                    }else if(value instanceof TransactionExplain.Kind){
                        map.putInt(name, ((TransactionExplain.Kind) value).getValue());
                    }else if(value instanceof Transaction.Type){
                        map.putInt(name, ((Transaction.Type) value).getValue());
                    }else{
                        map.putInt(name, ((Enum) value).ordinal());
                    }
                } else if (!value.getClass().isPrimitive()) {
                    map.putMap(name, objectToMap(value));
                } else {
                    throw new UnsupportedOperationException("Not implemented: " + value);
                }
            } catch (Exception e) {
                Log.w(TAG, "Read field failed", e);
            }
        }

        return map;
    }

    static WritableArray objectToArray(Object array) {
        final int N = Array.getLength(array);
        final WritableArray arr = Arguments.createArray();
        for (int i = 0; i < N; i++) {
            try {
                final Object elem = Array.get(array, i);
                if (elem == null) {
                    arr.pushNull();
                } else if (elem instanceof Integer) {
                    arr.pushInt((Integer) elem);
                } else if (elem instanceof Long) {
                    arr.pushDouble((Long) elem);
                } else if (elem instanceof Float) {
                    arr.pushDouble((Float) elem);
                } else if (elem instanceof Double) {
                    arr.pushDouble((Double) elem);
                } else if (elem instanceof Boolean) {
                    arr.pushBoolean((Boolean) elem);
                } else if (elem instanceof String) {
                    arr.pushString((String) elem);
                } else if (elem.getClass().isArray()) {
                    arr.pushArray(objectToArray(elem));
                } else if (!elem.getClass().isPrimitive()) {
                    arr.pushMap(objectToMap(elem));
                } else {
                    throw new UnsupportedOperationException("Not implemented: " + elem);
                }
            } catch (Exception e) {
                Log.w(TAG, "Read element failed", e);
            }
        }
        return arr;
    }

    static final String KEY_EOS_TRANSACTION_TYPE = "eos_transaction_type";
    static final String KEY_EOS_NUM_BYTES = "num_bytes";
    static final String KEY_MEMO = "memo";
    static final String KEY_INPUT_DATA = "input_data";
    static final String KEY_GAS_LIMIT = "gas_limit";
    static final String KEY_TOKEN_ID = "token_id";
    static final String KEY_KIND = "kind";
    static final String KEY_SELF_VERIFY = "self_verify";
    static final String KEY_TO_ADDRESS_TAG = "to_address_tag";
    static final String KEY_SOL_TOKEN_ID = "sol_token_id";
    static final String KEY_CUSTOM_NONCE = "custom_nonce";
    static final String KEY_CUSTOM_GAS_LIMIT = "custom_gas_limit";
    static final String KEY_FORCE_SEND = "force_send";

    static Map<String, Object> extractExtrasForTransaction(ReadableMap extraAttrs) {
        Map<String, Object> extras = new HashMap<>();

        if (extraAttrs == null) {
            return extras;
        }

        final ReadableMapKeySetIterator itor = extraAttrs.keySetIterator();
        while (itor.hasNextKey()) {
            final String key = itor.nextKey();
            switch (key) {
                case KEY_EOS_TRANSACTION_TYPE:
                    if (!extraAttrs.isNull(key)) {
                        final int txTypeInt = extraAttrs.getInt(key);
                        for (EosResourceTransactionType type : EosResourceTransactionType.values()) {
                            if (type.toInteger() == txTypeInt) {
                                extras.put(key, type);
                                break;
                            }
                        }
                    }
                    break;
                case KEY_EOS_NUM_BYTES:
                case KEY_CUSTOM_NONCE:
                case KEY_CUSTOM_GAS_LIMIT:
                    if (!extraAttrs.isNull(key)) {
                        extras.put(key, (long) (extraAttrs.getDouble(key)));
                    }
                    break;
                case KEY_MEMO:
                    if (!extraAttrs.isNull(key)) {
                        extras.put(key, extraAttrs.getString(key));
                    }
                    break;
                case KEY_INPUT_DATA:
                case KEY_TOKEN_ID:
                case KEY_KIND:
                case KEY_SOL_TOKEN_ID:
                    if (!extraAttrs.isNull(key)) {
                        extras.put(key, extraAttrs.getString(key));
                    }
                    break;
                case KEY_GAS_LIMIT:
                    if (!extraAttrs.isNull(key)) {
                        extras.put(key, extraAttrs.getDouble(key));
                    }
                    break;
                case KEY_SELF_VERIFY:
                case KEY_FORCE_SEND:
                    if (!extraAttrs.isNull(key)) {
                        extras.put(key, extraAttrs.getBoolean(key));
                    }
                    break;
                case KEY_TO_ADDRESS_TAG:
                    if (!extraAttrs.isNull(key) && extraAttrs.getType(key) == ReadableType.Array) {
                        ReadableArray readableArray = extraAttrs.getArray(key);
                        extras.put(key, getStringArrayFromReadableArray(readableArray));
                    }
                    break;
                default:
                    Log.w(TAG, "unsupported extra: " + key);
            }
        }

        return extras;
    }

    public static AddressTags getAddressTagsFromReadableMap(ReadableMap readableMap) {
        AddressTags tags = new AddressTags();
        if (!readableMap.isNull(KEY_BLACKLIST) && readableMap.getType(KEY_BLACKLIST) == ReadableType.Boolean) {
            tags.blackList = readableMap.getBoolean(KEY_BLACKLIST);
        }
        if (!readableMap.isNull(KEY_WHITELIST) && readableMap.getType(KEY_WHITELIST) == ReadableType.Boolean) {
            tags.whiteList = readableMap.getBoolean(KEY_WHITELIST);
        }
        if (!readableMap.isNull(KEY_TAGS) && readableMap.getType(KEY_TAGS) == ReadableType.Array) {
            ReadableArray readableArray = readableMap.getArray(KEY_TAGS);
            tags.tags = getStringArrayFromReadableArray(readableArray);
        }

        if (!readableMap.isNull(KEY_PROVIDERS) && readableMap.getType(KEY_PROVIDERS) == ReadableType.Map) {
            tags.providers = new HashMap<>();//{provider: tags}
            ReadableMap rnMap =  readableMap.getMap(KEY_PROVIDERS);
            ReadableMapKeySetIterator itor = rnMap.keySetIterator();
            while (itor.hasNextKey()) {
                final String key = itor.nextKey();
                if (!rnMap.isNull(key) && rnMap.getType(key) == ReadableType.Array) {
                    ReadableArray readableArray = rnMap.getArray(key);
                    tags.providers.put(key, getStringArrayFromReadableArray(readableArray));
                }
            }

        }

        if (!readableMap.isNull(KEY_PROVIDER) && readableMap.getType(KEY_PROVIDER) == ReadableType.String) {
            tags.provider = readableMap.getString(KEY_PROVIDER);
        }
        if (!readableMap.isNull(KEY_SCORE) && readableMap.getType(KEY_SCORE) == ReadableType.Number) {
            tags.score = readableMap.getDouble(KEY_SCORE);
        }
        return tags;
    }
    public static String[] getStringArrayFromReadableArray(ReadableArray readableArray){
        String[] strArr = new String[readableArray.size()];
        for(int i = 0; i < readableArray.size(); i++){
            if(readableArray.getType(i) == ReadableType.String){
                strArr[i] = readableArray.getString(i);
            }else{
                strArr[i] = "";
            }
        }
        return strArr;
    }
    static final String KEY_BLACKLIST = "blackList";
    static final String KEY_WHITELIST = "whiteList";
    static final String KEY_TAGS = "tags";
    static final String KEY_PROVIDERS = "providers";
    static final String KEY_PROVIDER = "provider";
    static final String KEY_SCORE = "score";
    static final String KEY_DIRECTION = "direction";
    static final String KEY_PENDING = "pending";
    static final String KEY_SUCCESS = "success";
    static final String KEY_START_TIME = "start_time";
    static final String KEY_END_TIME = "end_time";
    static final String KEY_API_NAME = "api_name";
    static final String KEY_TYPE = "type";
    static final String KEY_CURRENCY = "currency";
    static final String KEY_TOKEN_ADDRESS = "token_address";

    static Map<String, Map<String, Double>> extractApproximateRates(ReadableMap approximateRates) {
        Map<String, Map<String, Double>> ratesMap = new HashMap<>();
        Map<String, Double> ratesMap2;
        if(approximateRates == null){
            return ratesMap;
        }
        final ReadableMapKeySetIterator itor = approximateRates.keySetIterator();
        while (itor.hasNextKey()) {
            final String key = itor.nextKey();
            if (!approximateRates.isNull(key) && approximateRates.getType(key) == ReadableType.Map) {
                ratesMap2 = new HashMap<>();
                ReadableMap approximateRates2 = approximateRates.getMap(key);
                final ReadableMapKeySetIterator itor2 = approximateRates2.keySetIterator();
                while (itor2.hasNextKey()) {
                    final String key2 = itor2.nextKey();
                    if(!approximateRates2.isNull(key2) && approximateRates2.getType(key2) == ReadableType.Number){
                        ratesMap2.put(key2, approximateRates2.getDouble(key2));
                    }
                }
                ratesMap.put(key, ratesMap2);
            }
        }
        return ratesMap;
    }
    static Map<String, Object> extractFiltersForApiHistory(ReadableMap filterAttrs) {
        Map<String, Object> filters = new HashMap<>();

        if (filterAttrs == null) {
            return filters;
        }

        final ReadableMapKeySetIterator itor = filterAttrs.keySetIterator();
        while (itor.hasNextKey()) {
            final String key = itor.nextKey();
            switch (key) {
                case KEY_API_NAME:
                    filters.put(key, filterAttrs.getString(key));
                    break;
                case KEY_START_TIME:
                case KEY_END_TIME:
                    if (!filterAttrs.isNull(key)) {
                        filters.put(key, (long) (filterAttrs.getDouble(key)));
                    }
                    break;
                default:
                    Log.w(TAG, "unsupported filter: " + key);
            }
        }
        return filters;
    }
    static Map<String, Object> extractFiltersForHistory(ReadableMap filterAttrs) {
        Map<String, Object> filters = new HashMap<>();

        if (filterAttrs == null) {
            return filters;
        }

        final ReadableMapKeySetIterator itor = filterAttrs.keySetIterator();
        while (itor.hasNextKey()) {
            final String key = itor.nextKey();
            switch (key) {
                case KEY_DIRECTION:
                    if (!filterAttrs.isNull(key)) {
                        final int dirInt = filterAttrs.getInt(key);
                        for (Transaction.Direction dir : Transaction.Direction.values()) {
                            if (dir.ordinal() == dirInt) {
                                filters.put(key, dir);
                                break;
                            }
                        }
                    }
                    break;
                case KEY_TYPE:
                    if (!filterAttrs.isNull(key)) {
                        if(filterAttrs.getType(key) == ReadableType.Number){
                            final int intValue = filterAttrs.getInt(key);
                            filters.put(key, Transaction.Type.getType(intValue));
                        }

                        if (filterAttrs.getType(key) == ReadableType.Array) {
                            ReadableArray rnArray = filterAttrs.getArray(key);
                            final Transaction.Type[] array = new Transaction.Type[rnArray.size()];
                            for (int i = 0; i < rnArray.size(); i++) {
                                switch (rnArray.getType(i)) {
                                    case Number:
                                        array[i] = Transaction.Type.getType(rnArray.getInt(i));
                                        break;
                                    default:
                                        array[i] = Transaction.Type.Unknown;
                                }
                            }
                            filters.put(key, array);
                        }
                    }
                    break;
                case KEY_PENDING:
                case KEY_SUCCESS:
                    if (!filterAttrs.isNull(key)) {
                        filters.put(key, filterAttrs.getBoolean(key));
                    }
                    break;
                case KEY_START_TIME:
                case KEY_END_TIME:
                case KEY_CURRENCY:
                    if (!filterAttrs.isNull(key)) {
                        filters.put(key, (long) (filterAttrs.getDouble(key)));
                    }
                    break;
                case KEY_TOKEN_ADDRESS:
                    if (!filterAttrs.isNull(key)) {
                        filters.put(key, filterAttrs.getString(key));
                    }
                    break;
                default:
                    Log.w(TAG, "unsupported filter: " + key);
            }
        }

        return filters;
    }

    static Map<String, Object> reactNativeMapToNativeMap(ReadableMap rnMap) {
        Map<String, Object> map = new HashMap<>();

        if (rnMap == null) {
            return map;
        }

        final ReadableMapKeySetIterator itor = rnMap.keySetIterator();
        while (itor.hasNextKey()) {
            final String key = itor.nextKey();
            try{
                switch (rnMap.getType(key)) {
                    case Null:
                        map.put(key, null);
                    case Boolean:
                        map.put(key, rnMap.getBoolean(key));
                        break;
                    case Number:
                        map.put(key, rnMap.getDouble(key));
                        break;
                    case String:
                        map.put(key, rnMap.getString(key));
                        break;
                    case Map:
                        map.put(key, reactNativeMapToNativeMap(rnMap.getMap(key)));
                        break;
                    case Array:
                        map.put(key, reactNativeArrayToNativeArray(rnMap.getArray(key)));
                        break;
                }
            } catch(Exception e) {
                e.printStackTrace();
            }
        }

        return map;
    }

    static Object[] reactNativeArrayToNativeArray(ReadableArray rnArray) {

        if (rnArray == null) {
            return new Object[0];
        }

        final Object[] array = new Object[rnArray.size()];
        for (int i = 0; i < rnArray.size(); i++) {
            switch (rnArray.getType(i)) {
                case Null:
                    array[i] = null;
                case Boolean:
                    array[i] = rnArray.getBoolean(i);
                    break;
                case Number:
                    array[i] = rnArray.getDouble(i);
                    break;
                case String:
                    array[i] = rnArray.getString(i);
                    break;
                case Map:
                    array[i] = reactNativeMapToNativeMap(rnArray.getMap(i));
                    break;
                case Array:
                    array[i] = reactNativeArrayToNativeArray(rnArray.getArray(i));
                    break;
            }
        }

        return array;
    }
}
