package com.zcloud.personnel.positioning.command;

import com.alibaba.cola.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zcloud.personnel.positioning.domain.gateway.TerminalBindGateway;
import com.zcloud.personnel.positioning.domain.gateway.TerminalGateway;
import com.zcloud.personnel.positioning.domain.model.TerminalBindE;
import com.zcloud.personnel.positioning.domain.model.TerminalE;
import com.zcloud.personnel.positioning.persistence.dataobject.CorpInfoSnapshotDO;
import com.zcloud.personnel.positioning.persistence.dataobject.TerminalBindDO;
import com.zcloud.personnel.positioning.persistence.dataobject.TerminalDO;
import com.zcloud.personnel.positioning.persistence.dataobject.TerminalPersonSnapshotDO;
import com.zcloud.personnel.positioning.persistence.repository.TerminalBindRepository;
import com.zcloud.personnel.positioning.persistence.repository.TerminalRepository;
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Component
@AllArgsConstructor
public class TerminalBindingService {
    private static final String BOUND = "BOUND";
    private static final String UNBOUND = "UNBOUND";
    private static final String TRACK_TYPE_STAFF = "STAFF";
    private static final String SYNC_SUCCESS = "SUCCESS";
    private static final String SYNC_WAIT = "WAIT";

    private final TerminalRepository terminalRepository;
    private final TerminalBindRepository terminalBindRepository;
    private final TerminalGateway terminalGateway;
    private final TerminalBindGateway terminalBindGateway;

    public TerminalE findTerminalById(Long id) {
        if (id == null) {
            return null;
        }
        QueryWrapper<TerminalDO> wrapper = new QueryWrapper<>();
        wrapper.eq("id", id).eq("delete_enum", "FALSE").last("LIMIT 1");
        return toEntity(terminalRepository.getOne(wrapper, false));
    }

    public TerminalE findTerminalByNo(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return null;
        }
        QueryWrapper<TerminalDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo.trim()).eq("delete_enum", "FALSE").last("LIMIT 1");
        return toEntity(terminalRepository.getOne(wrapper, false));
    }

    public TerminalE findAppByAccount(String appAccount) {
        if (!StringUtils.hasText(appAccount)) {
            return null;
        }
        QueryWrapper<TerminalDO> wrapper = new QueryWrapper<>();
        wrapper.eq("app_account", appAccount.trim()).eq("device_type", "APP")
                .eq("delete_enum", "FALSE").last("LIMIT 1");
        return toEntity(terminalRepository.getOne(wrapper, false));
    }

    public void addTerminal(TerminalE terminal) {
        if (!terminalGateway.add(terminal)) {
            throw new BizException("Terminal save failed");
        }
    }

    public void updateTerminal(TerminalE terminal) {
        if (!terminalGateway.update(terminal)) {
            throw new BizException("Terminal update failed");
        }
    }

    public TerminalPersonSnapshotDO resolvePerson(String idCardNo, String staffNo, String staffName) {
        if (!StringUtils.hasText(idCardNo) && !StringUtils.hasText(staffNo) && !StringUtils.hasText(staffName)) {
            return null;
        }
        Map<String, Object> params = new HashMap<>();
        putText(params, "idCardNo", idCardNo);
        putText(params, "staffNo", staffNo);
        putText(params, "staffName", staffName);
        TerminalPersonSnapshotDO person = terminalRepository.findPerson(params);
        TerminalPersonSnapshotDO fallback = new TerminalPersonSnapshotDO();
        fallback.setIdCardNo(clean(idCardNo));
        fallback.setStaffNo(clean(staffNo));
        fallback.setStaffName(clean(staffName));
        return mergePerson(person, fallback);
    }

    public CorpInfoSnapshotDO resolveCorpInfo(String corpinfoId, String corpinfoName,
                                               String orgCode, String orgName) {
        if (corpinfoId == null && !StringUtils.hasText(orgCode)
                && !StringUtils.hasText(corpinfoName) && !StringUtils.hasText(orgName)) {
            return null;
        }
        Map<String, Object> params = new HashMap<>();
        putText(params, "corpinfoId", corpinfoId);
        putText(params, "orgCode", orgCode);
        putText(params, "corpinfoName", firstText(corpinfoName, orgName));
        return terminalRepository.findCorpInfo(params);
    }

    public boolean upsertActiveBinding(TerminalE terminal, TerminalPersonSnapshotDO person,
                                       boolean localChange, LocalDateTime now) {
        if (!hasPerson(person)) {
            return false;
        }
        if (!localChange && hasPendingBinding(terminal.getTerminalNo())) {
            return false;
        }
        TerminalPersonSnapshotDO normalized = mergePerson(person,
                resolvePerson(person.getIdCardNo(), person.getStaffNo(), person.getStaffName()));
        TerminalBindE active = findActiveBinding(terminal.getTerminalNo());
        if (active != null && samePerson(active, normalized)) {
            active.setIdCardNo(firstText(normalized.getIdCardNo(), active.getIdCardNo()));
            active.setStaffNoSnapshot(firstText(normalized.getStaffNo(), active.getStaffNoSnapshot()));
            active.setStaffNameSnapshot(firstText(normalized.getStaffName(), active.getStaffNameSnapshot()));
            active.setFindsTrackId(firstLong(normalized.getFindsTrackId(), active.getFindsTrackId()));
            active.setFindsTerminalId(firstLong(terminal.getFindsTerminalId(), active.getFindsTerminalId()));
            markBindingSyncStatus(active, localChange, now);
            updateBinding(active);
            updateTerminalBinding(terminal, active.getIdCardNo());
            return true;
        }
        if (active != null) {
            active.setBindStatus(UNBOUND);
            active.setUnbindTime(now);
            markBindingSyncStatus(active, localChange, now);
            updateBinding(active);
        }
        TerminalBindE binding = new TerminalBindE();
        binding.setIdCardNo(normalized.getIdCardNo());
        binding.setStaffNoSnapshot(normalized.getStaffNo());
        binding.setStaffNameSnapshot(normalized.getStaffName());
        binding.setTerminalId(terminal.getId());
        binding.setTerminalNo(terminal.getTerminalNo());
        binding.setDeviceType(terminal.getDeviceType());
        binding.setCorpinfoId(terminal.getCorpinfoId());
        binding.setCorpinfoName(terminal.getCorpinfoName());
        binding.setTrackType(TRACK_TYPE_STAFF);
        binding.setCheckCode(terminal.getCheckCode());
        binding.setBindStatus(BOUND);
        binding.setBindTime(now);
        binding.setFindsTrackId(normalized.getFindsTrackId());
        binding.setFindsTerminalId(terminal.getFindsTerminalId());
        markBindingSyncStatus(binding, localChange, now);
        if (!terminalBindGateway.add(binding)) {
            throw new BizException("Terminal binding save failed");
        }
        updateTerminalBinding(terminal, normalized.getIdCardNo());
        return true;
    }

    public void unbindActiveBinding(TerminalE terminal, boolean localChange, LocalDateTime now) {
        TerminalBindE active = findActiveBinding(terminal.getTerminalNo());
        if (active == null) {
            updateTerminalBinding(terminal, null);
            return;
        }
        active.setBindStatus(UNBOUND);
        active.setUnbindTime(now);
        markBindingSyncStatus(active, localChange, now);
        updateBinding(active);
        updateTerminalBinding(terminal, null);
    }

    public void moveActiveBindingTerminalNo(String oldTerminalNo, TerminalE terminal) {
        TerminalBindE active = findActiveBinding(oldTerminalNo);
        if (active == null) {
            return;
        }
        active.setTerminalNo(terminal.getTerminalNo());
        active.setTerminalId(terminal.getId());
        active.setFindsTerminalId(terminal.getFindsTerminalId());
        updateBinding(active);
    }

    public boolean hasPendingBinding(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return false;
        }
        QueryWrapper<TerminalBindDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo)
                .in("sync_status", FindsPendingSyncService.STATUS_WAIT, FindsPendingSyncService.STATUS_FAIL,
                        FindsPendingSyncService.STATUS_DELETE_WAIT, FindsPendingSyncService.STATUS_DELETE_FAIL)
                .last("LIMIT 1");
        return terminalBindRepository.getOne(wrapper, false) != null;
    }

    public TerminalBindE findActiveBinding(String terminalNo) {
        if (!StringUtils.hasText(terminalNo)) {
            return null;
        }
        QueryWrapper<TerminalBindDO> wrapper = new QueryWrapper<>();
        wrapper.eq("terminal_no", terminalNo).eq("bind_status", BOUND).eq("delete_enum", "FALSE")
                .orderByDesc("bind_time", "id").last("LIMIT 1");
        TerminalBindDO dataObject = terminalBindRepository.getOne(wrapper, false);
        if (dataObject == null) {
            return null;
        }
        TerminalBindE entity = new TerminalBindE();
        BeanUtils.copyProperties(dataObject, entity);
        return entity;
    }

    private void updateBinding(TerminalBindE binding) {
        if (!terminalBindGateway.update(binding)) {
            throw new BizException("Terminal binding update failed");
        }
    }

    private void updateTerminalBinding(TerminalE terminal, String idCardNo) {
        terminalRepository.updateBindIdCardNo(terminal.getId(), idCardNo);
        terminal.setBindIdCardNo(idCardNo);
    }

    private void markBindingSyncStatus(TerminalBindE binding, boolean localChange, LocalDateTime now) {
        binding.setSyncStatus(localChange ? SYNC_WAIT : SYNC_SUCCESS);
        binding.setSyncToken(localChange ? FindsSyncToken.next() : null);
        binding.setLastSyncTime(localChange ? null : now);
    }

    private boolean samePerson(TerminalBindE binding, TerminalPersonSnapshotDO person) {
        if (StringUtils.hasText(person.getIdCardNo())) {
            return person.getIdCardNo().equals(binding.getIdCardNo());
        }
        return StringUtils.hasText(person.getStaffNo()) && person.getStaffNo().equals(binding.getStaffNoSnapshot());
    }

    private TerminalPersonSnapshotDO mergePerson(TerminalPersonSnapshotDO preferred,
                                                  TerminalPersonSnapshotDO fallback) {
        if (preferred == null) {
            return fallback;
        }
        if (fallback == null) {
            return preferred;
        }
        TerminalPersonSnapshotDO result = new TerminalPersonSnapshotDO();
        result.setIdCardNo(firstText(preferred.getIdCardNo(), fallback.getIdCardNo()));
        result.setStaffNo(firstText(preferred.getStaffNo(), fallback.getStaffNo()));
        result.setStaffName(firstText(preferred.getStaffName(), fallback.getStaffName()));
        result.setMobileNo(firstText(preferred.getMobileNo(), fallback.getMobileNo()));
        result.setCorpinfoId(firstText(preferred.getCorpinfoId(), fallback.getCorpinfoId()));
        result.setCorpinfoName(firstText(preferred.getCorpinfoName(), fallback.getCorpinfoName()));
        result.setFindsTrackId(firstLong(preferred.getFindsTrackId(), fallback.getFindsTrackId()));
        return result;
    }

    private TerminalE toEntity(TerminalDO dataObject) {
        if (dataObject == null) {
            return null;
        }
        TerminalE entity = new TerminalE();
        BeanUtils.copyProperties(dataObject, entity);
        return entity;
    }

    private boolean hasPerson(TerminalPersonSnapshotDO person) {
        return person != null && (StringUtils.hasText(person.getIdCardNo())
                || StringUtils.hasText(person.getStaffNo()) || StringUtils.hasText(person.getStaffName()));
    }

    private void putText(Map<String, Object> params, String key, String value) {
        if (StringUtils.hasText(value)) {
            params.put(key, value.trim());
        }
    }

    private String clean(String value) {
        return StringUtils.hasText(value) ? value.trim() : null;
    }

    private String firstText(String... values) {
        if (values != null) {
            for (String value : values) {
                if (StringUtils.hasText(value)) {
                    return value;
                }
            }
        }
        return null;
    }

    private Long firstLong(Long... values) {
        if (values != null) {
            for (Long value : values) {
                if (value != null) {
                    return value;
                }
            }
        }
        return null;
    }
}
