package cn.vertxup.erp.service;

import cn.vertxup.erp.domain.tables.daos.EEmployeeDao;
import cn.vertxup.erp.domain.tables.pojos.EEmployee;
import io.horizon.spi.business.ExUser;
import io.horizon.spi.feature.Trash;
import io.horizon.spi.modeler.Indent;
import io.vertx.core.Future;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.mod.ke.cv.em.BizInternal;
import io.vertx.up.eon.KName;
import io.vertx.up.fn.Fn;
import io.vertx.up.unity.Ux;
import io.vertx.up.util.Ut;

import java.util.Set;
import java.util.function.BiFunction;

public class EmployeeService implements EmployeeStub {
    @Override
    public Future<JsonObject> createAsync(final JsonObject data) {
        final EEmployee employee = Ut.deserialize(data, EEmployee.class);
        if (Ut.isNil(employee.getWorkNumber())) {
            return Ux.channelA(Indent.class, () -> this.insertAsync(employee, data),
                serial -> serial.indent("NUM.EMPLOYEE", data.getString(KName.SIGMA)).compose(workNum -> {
                    employee.setWorkNumber(workNum);
                    return this.insertAsync(employee, data);
                }));
        } else {
            return this.insertAsync(employee, data);
        }
    }

    private Future<JsonObject> insertAsync(final EEmployee employee, final JsonObject data) {
        return Ux.Jooq.on(EEmployeeDao.class).insertAsync(employee)
            .compose(Ux::futureJ)
            .compose(inserted -> {
                /*
                 * If data contains `userId`, it means that current employee will relate to
                 * an account
                 */
                if (data.containsKey(KName.USER_ID)) {
                    /*
                     * Create new relation here.
                     */
                    final String key = data.getString(KName.USER_ID);
                    return this.updateReference(key, inserted);
                } else {
                    return Ux.future(data);
                }
            });
    }

    @Override
    public Future<JsonObject> fetchAsync(final String key) {
        return Ux.Jooq.on(EEmployeeDao.class).fetchByIdAsync(key)
            .compose(Ux::futureJ)
            .compose(this::fetchRef);
    }

    @Override
    public Future<JsonArray> fetchAsync(final Set<String> keys) {
        return Ux.Jooq.on(EEmployeeDao.class).fetchInAsync(KName.KEY, Ut.toJArray(keys))
            .compose(Ux::futureA)
            .compose(this::fetchRef);
    }

    @Override
    public Future<JsonArray> fetchAsync(final JsonObject condition) {
        return Ux.Jooq.on(EEmployeeDao.class).fetchAsync(condition)
            .compose(Ux::futureA)
            .compose(this::fetchRef);
    }

    @Override
    public Future<JsonObject> updateAsync(final String key, final JsonObject data) {
        return this.fetchAsync(key)
            .compose(Fn.ofJObject(original -> {
                final String userId = original.getString(KName.USER_ID);
                final String current = data.getString(KName.USER_ID);
                if (Ut.isNil(userId) && Ut.isNil(current)) {
                    /*
                     * Old null, new null
                     * Relation keep
                     */
                    return this.updateEmployee(key, data);
                } else if (Ut.isNil(userId) && Ut.isNotNil(current)) {
                    /*
                     * Old null, new <value>
                     * Create relation with new
                     */
                    return this.updateEmployee(key, data)
                        .compose(response -> this.updateReference(current, response));
                } else if (Ut.isNotNil(userId) && Ut.isNil(current)) {
                    /*
                     * Old <value>, new <null>
                     * Clear relation with old
                     */
                    return this.updateEmployee(key, data)
                        .compose(response -> this.updateReference(userId, new JsonObject())
                            .compose(nil -> Ux.future(response))
                        );
                } else {
                    /*
                     * Old <value>, new <value>
                     */
                    if (userId.equals(current)) {
                        /*
                         * Old = New
                         * Relation keep
                         */
                        return this.updateEmployee(key, data);
                    } else {
                        return this.updateEmployee(key, data)
                            /*
                             * Clear first
                             */
                            .compose(response -> this.updateReference(userId, new JsonObject())
                                /*
                                 * Then update
                                 */
                                .compose(nil -> this.updateReference(current, response)));
                    }
                }
            }));
    }

    private Future<JsonObject> updateEmployee(final String key, final JsonObject data) {
        final JsonObject uniques = new JsonObject();
        uniques.put(KName.KEY, key);
        final EEmployee employee = Ut.deserialize(data, EEmployee.class);
        return Ux.Jooq.on(EEmployeeDao.class)
            .upsertAsync(uniques, employee)
            .compose(Ux::futureJ);
    }

    @Override
    public Future<Boolean> deleteAsync(final String key) {
        return this.fetchAsync(key)
            .compose(Fn.ifNil(() -> Boolean.TRUE, item -> Ux.channelA(Trash.class,
                () -> this.deleteAsync(key, item),
                tunnel -> tunnel.backupAsync("res.employee", item)
                    .compose(backup -> this.deleteAsync(key, item)))));
    }

    private Future<Boolean> deleteAsync(final String key, final JsonObject item) {
        final String userId = item.getString(KName.USER_ID);
        return this.updateReference(userId, new JsonObject())
            .compose(nil -> Ux.Jooq.on(EEmployeeDao.class)
                .deleteByIdAsync(key));
    }

    private Future<JsonObject> updateReference(final String key, final JsonObject data) {
        return this.switchJ(data, (user, filters) -> user.rapport(key, filters)
            .compose(Fn.ofJObject(response ->
                Ux.future(data.put(KName.USER_ID, response.getString(KName.KEY))))));
    }

    private Future<JsonObject> fetchRef(final JsonObject input) {
        return this.switchJ(input, ExUser::rapport).compose(userJ -> {
            // Fix: https://gitee.com/silentbalanceyh/vertx-zero-scaffold/issues/I6W2KQ
            if (Ut.isNotNil(userJ)) {
                final String userId = Ut.valueString(userJ, KName.KEY);
                if (Ut.isNotNil(userId)) {
                    input.put(KName.USER_ID, userId);
                }
            }
            return Ux.future(input);
            /*
             Fn.ofJObject(response -> {
            final String userId = response.getString(KName.KEY);
            if (Ut.notNil(userId)) {
                return Ux.future(input.put(KName.USER_ID, userId));
            } else {
                return Ux.future(input);
            }
        })
             */
        });
    }

    private Future<JsonArray> fetchRef(final JsonArray input) {
        return Ux.channel(ExUser.class, JsonArray::new, user -> {
            final Set<String> keys = Ut.valueSetString(input, KName.KEY);
            return user.rapport(keys);
        }).compose(employee -> {
            final JsonArray merged = Ut.elementJoin(input, employee, KName.KEY, KName.MODEL_KEY);
            return Ux.future(merged);
        });
    }

    private Future<JsonObject> switchJ(final JsonObject input,
                                       final BiFunction<ExUser, JsonObject, Future<JsonObject>> executor) {
        return Ux.channel(ExUser.class, JsonObject::new, user -> {
            if (Ut.isNil(input)) {
                // fix issue: https://gitee.com/silentbalanceyh/vertx-zero-scaffold/issues/I6W2L9
                final JsonObject filters = new JsonObject();
                filters.put(KName.IDENTIFIER, BizInternal.TypeUser.employee.name());
                return executor.apply(user, filters);
                //return Ux.future(new JsonObject());
            } else {
                final JsonObject filters = new JsonObject();
                filters.put(KName.IDENTIFIER, BizInternal.TypeUser.employee.name());
                filters.put(KName.SIGMA, input.getString(KName.SIGMA));
                filters.put(KName.KEY, input.getString(KName.KEY));
                return executor.apply(user, filters);
            }
        });
    }
}
