package com.red.circle.console.app.service.app.sys;

import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.console.infra.database.rds.service.sys.SysPushRecordLogService;
import com.red.circle.external.inner.endpoint.push.GoogleFirebasePushClient;
import com.red.circle.external.inner.model.cmd.message.google.GooglePushCmd;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.dto.PageCommand;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.PageQuery;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.inner.endpoint.user.device.RegisterDeviceClient;
import com.red.circle.other.inner.enums.sys.MessageBusinessSceneEnum;
import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
import com.red.circle.other.inner.model.cmd.user.device.PageLatestMobileDeviceQryCmd;
import com.red.circle.other.inner.model.dto.user.UserLatestMobileDeviceHistoryDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 后台 Push 服务.
 *
 * <p>旧的推送实现已经失效，这里按当前可用的设备查询与推送客户端重建最小闭环：
 * 发送 Push、记录 Push 日志、为未迁移的定时任务返回明确的不可用状态。</p>
 */
@Slf4j
@RequiredArgsConstructor
@Service
public class SysPushServiceImpl implements SysPushService {

  private static final int DEFAULT_PAGE_LIMIT = 20;
  private static final int DEVICE_PAGE_LIMIT = 500;
  private static final String PUSH_ORIGIN_BACK = "BACK";
  private static final String PUSH_STATUS_SUCCESS = "SUCCESS";
  private static final String PUSH_STATUS_FAIL = "FAIL";
  private static final Set<String> PUSH_FAIL_STATUSES = Set.of(
      "UNKNOWN",
      PUSH_STATUS_FAIL,
      "IOS_FAIL",
      "ANDROID_FAIL");

  private final UserService userService;
  private final RegisterDeviceClient registerDeviceClient;
  private final GoogleFirebasePushClient googleFirebasePushClient;
  private final SysPushRecordLogService sysPushRecordLogService;

  @Override
  public void create(CreateRecordCmd createRecordParam) {
    validateCreateParam(createRecordParam);

    List<UserLatestMobileDeviceHistoryDTO> targetDevices = listTargetDevices(createRecordParam);
    if (CollectionUtils.isEmpty(targetDevices)) {
      ResponseAssert.failure(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
          "No matching push devices.");
    }

    SysPushRecordLog pushLog = buildPushLog(createRecordParam);
    try {
      pushGoogle(createRecordParam, targetDevices);
      pushLog.setPushStatus(PUSH_STATUS_SUCCESS);
      pushLog.setSuccessQuantity(targetDevices.size());
      pushLog.setFailQuantity(0);
    } catch (RuntimeException ex) {
      pushLog.setPushStatus(PUSH_STATUS_FAIL);
      pushLog.setSuccessQuantity(0);
      pushLog.setFailQuantity(targetDevices.size());
      log.warn("Push broadcast failed, sysOrigin={}, title={}", createRecordParam.getSysOrigin(),
          createRecordParam.getTitle(), ex);
      throw ex;
    } finally {
      ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE, sysPushRecordLogService.save(pushLog));
      createRecordParam.setId(pushLog.getId());
    }
  }

  @Override
  public PageResult<SysPushRecordLogCO> pageLogs(SysPushRecordLogQryCmd query) {
    PageResult<SysPushRecordLog> page = sysPushRecordLogService.query()
        .eq(StringUtils.isNotBlank(query.getBusinessScene()), SysPushRecordLog::getBusinessScene,
            query.getBusinessScene())
        .eq(StringUtils.isNotBlank(query.getPlatform()), SysPushRecordLog::getPlatform,
            query.getPlatform())
        .eq(StringUtils.isNotBlank(query.getDeviceType()), SysPushRecordLog::getDeviceType,
            query.getDeviceType())
        .eq(StringUtils.isNotBlank(query.getSysOrigin()), SysPushRecordLog::getSysOrigin,
            query.getSysOrigin())
        .ge(Objects.nonNull(query.getStartTime()), SysPushRecordLog::getCreateTime,
            new Timestamp(query.getStartTime()))
        .le(Objects.nonNull(query.getEndTime()), SysPushRecordLog::getCreateTime,
            new Timestamp(query.getEndTime()))
        .eq(isSuccessStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
            PUSH_STATUS_SUCCESS)
        .in(isFailStatusQuery(query.getPushStatus()), SysPushRecordLog::getPushStatus,
            PUSH_FAIL_STATUSES)
        .orderByDesc(SysPushRecordLog::getId)
        .page(ensurePageQuery(query));

    if (CollectionUtils.isEmpty(page.getRecords())) {
      return page.convert(this::toPushLogCO);
    }

    Map<Long, String> sendUserNameMap = userService.mapBackUserNickname(
        page.getRecords().stream()
            .map(SysPushRecordLog::getCreateUser)
            .filter(Objects::nonNull)
            .collect(Collectors.toSet()));

    return page.convert(record -> toPushLogCO(record, sendUserNameMap));
  }

  @Override
  public PageResult<SysPushTaskCO> pageTask(SysPushTaskQryCmd query) {
    return PageResult.newPageResult(resolvePageLimit(ensurePageQuery(query)));
  }

  @Override
  public void saveTask(SysPushTaskCmd param) {
    ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
        "Push task is not available in the migrated admin yet.");
  }

  @Override
  public void deletePushTask(Long id) {
    ResponseAssert.failure(CommonErrorCode.API_DEPRECATED,
        "Push task is not available in the migrated admin yet.");
  }

  private void validateCreateParam(CreateRecordCmd createRecordParam) {
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR, createRecordParam.getTitle());
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        createRecordParam.getContent());
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        createRecordParam.getSysOrigin());
    ResponseAssert.notBlank(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        createRecordParam.getDeviceType());
    ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()));
    ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        createRecordParam.isGoogleDevice());
  }

  private void pushGoogle(CreateRecordCmd createRecordParam,
      List<UserLatestMobileDeviceHistoryDTO> targetDevices) {
    GooglePushCmd pushCmd = new GooglePushCmd()
        .setSysOrigin(SysOriginPlatformEnum.toEnum(createRecordParam.getSysOrigin()))
        .setTitle(createRecordParam.getTitle())
        .setBody(createRecordParam.getContent());

    for (UserLatestMobileDeviceHistoryDTO device : targetDevices) {
      if (isIosPlatform(device.getRequestClient())) {
        pushCmd.addIosDevice(device.getDeviceId());
        continue;
      }
      pushCmd.addAndroidDevice(device.getDeviceId());
    }

    ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
        pushCmd.isNotEmptyAndroidDevices() || pushCmd.isNotEmptyIosDevices());
    ResponseAssert.checkSuccess(googleFirebasePushClient.broadcast(pushCmd));
  }

  private List<UserLatestMobileDeviceHistoryDTO> listTargetDevices(CreateRecordCmd createRecordParam) {
    List<UserLatestMobileDeviceHistoryDTO> devices = createRecordParam.isFixedUserIdSend()
        ? listFixedTargetDevices(createRecordParam)
        : listBroadcastTargetDevices(createRecordParam);
    return distinctByDeviceId(devices);
  }

  private List<UserLatestMobileDeviceHistoryDTO> listFixedTargetDevices(
      CreateRecordCmd createRecordParam) {
    List<Long> fixedUserIds = createRecordParam.getQualifiedFixedUserIdList();
    if (CollectionUtils.isEmpty(fixedUserIds)) {
      return CollectionUtils.newArrayList();
    }

    List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>(fixedUserIds.size());
    for (Long userId : fixedUserIds) {
      PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
      query.setUserId(userId);
      PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
          registerDeviceClient.pageLatestMobileDevice(query));
      if (CollectionUtils.isEmpty(page.getRecords())) {
        continue;
      }
      UserLatestMobileDeviceHistoryDTO latestDevice = page.getRecords().iterator().next();
      if (matchesDevice(latestDevice, createRecordParam, true)) {
        devices.add(latestDevice);
      }
    }
    return devices;
  }

  private List<UserLatestMobileDeviceHistoryDTO> listBroadcastTargetDevices(
      CreateRecordCmd createRecordParam) {
    List<UserLatestMobileDeviceHistoryDTO> devices = new ArrayList<>();
    PageLatestMobileDeviceQryCmd query = buildDeviceQuery(createRecordParam);
    PageQuery pageQuery = query.getPageQuery();

    while (true) {
      PageResult<UserLatestMobileDeviceHistoryDTO> page = ResponseAssert.requiredSuccess(
          registerDeviceClient.pageLatestMobileDevice(query));
      Collection<UserLatestMobileDeviceHistoryDTO> records = page.getRecords();
      if (CollectionUtils.isEmpty(records)) {
        break;
      }

      records.stream()
          .filter(device -> matchesDevice(device, createRecordParam, false))
          .forEach(devices::add);

      if (records.size() < resolvePageLimit(pageQuery)) {
        break;
      }
      pageQuery.setCursor(pageQuery.getCursor() + 1);
    }
    return devices;
  }

  private PageLatestMobileDeviceQryCmd buildDeviceQuery(CreateRecordCmd createRecordParam) {
    PageLatestMobileDeviceQryCmd query = new PageLatestMobileDeviceQryCmd();
    query.setSysOrigin(createRecordParam.getSysOrigin());
    PageQuery pageQuery = new PageQuery();
    pageQuery.setCursor(1);
    pageQuery.setLimit(DEVICE_PAGE_LIMIT);
    query.setPageQuery(pageQuery);
    return query;
  }

  private List<UserLatestMobileDeviceHistoryDTO> distinctByDeviceId(
      List<UserLatestMobileDeviceHistoryDTO> devices) {
    if (CollectionUtils.isEmpty(devices)) {
      return CollectionUtils.newArrayList();
    }

    Map<String, UserLatestMobileDeviceHistoryDTO> distinctMap = new LinkedHashMap<>();
    for (UserLatestMobileDeviceHistoryDTO device : devices) {
      if (Objects.isNull(device) || StringUtils.isBlank(device.getDeviceId())) {
        continue;
      }
      distinctMap.putIfAbsent(device.getDeviceId(), device);
    }
    return new ArrayList<>(distinctMap.values());
  }

  private boolean matchesDevice(UserLatestMobileDeviceHistoryDTO device,
      CreateRecordCmd createRecordParam, boolean ignoreLanguage) {
    if (Objects.isNull(device) || StringUtils.isBlank(device.getDeviceId())) {
      return false;
    }
    if (StringUtils.isNotBlank(createRecordParam.getDeviceType())
        && !equalsIgnoreCase(createRecordParam.getDeviceType(), device.getDeviceType())) {
      return false;
    }
    if (StringUtils.isNotBlank(createRecordParam.getPlatform())
        && !isSamePlatform(createRecordParam.getPlatform(), device.getRequestClient())) {
      return false;
    }
    return ignoreLanguage || StringUtils.isBlank(createRecordParam.getLanguage())
        || equalsIgnoreCase(createRecordParam.getLanguage(), device.getLanguage());
  }

  private boolean isSuccessStatusQuery(String pushStatus) {
    String normalized = normalizeStatusQuery(pushStatus);
    return Objects.equals(normalized, "0") || Objects.equals(normalized,
        PUSH_STATUS_SUCCESS);
  }

  private boolean isFailStatusQuery(String pushStatus) {
    String normalized = normalizeStatusQuery(pushStatus);
    return Objects.equals(normalized, "1")
        || (StringUtils.isNotBlank(normalized) && !Objects.equals(normalized,
        PUSH_STATUS_SUCCESS));
  }

  private String normalizeStatusQuery(String pushStatus) {
    if (StringUtils.isBlank(pushStatus)) {
      return "";
    }
    String normalized = pushStatus.trim();
    if (Objects.equals(normalized, "0")) {
      return "0";
    }
    if (Objects.equals(normalized, "1")) {
      return "1";
    }
    return normalized.toUpperCase();
  }

  private boolean isSamePlatform(String expected, String actual) {
    return Objects.equals(normalizePlatform(expected), normalizePlatform(actual));
  }

  private boolean isIosPlatform(String platform) {
    return Objects.equals(normalizePlatform(platform), "ios");
  }

  private String normalizePlatform(String platform) {
    if (StringUtils.isBlank(platform)) {
      return "";
    }
    String normalized = platform.trim().toLowerCase();
    if (Objects.equals(normalized, "ios")) {
      return "ios";
    }
    if (Objects.equals(normalized, "android")) {
      return "android";
    }
    return normalized;
  }

  private boolean equalsIgnoreCase(String left, String right) {
    if (StringUtils.isBlank(left) || StringUtils.isBlank(right)) {
      return false;
    }
    return Objects.equals(left.trim().toUpperCase(), right.trim().toUpperCase());
  }

  private SysPushRecordLog buildPushLog(CreateRecordCmd createRecordParam) {
    Timestamp now = TimestampUtils.now();
    SysPushRecordLog pushLog = new SysPushRecordLog();
    pushLog.setTitle(createRecordParam.getTitle());
    pushLog.setContent(createRecordParam.getContent());
    pushLog.setPlatform(createRecordParam.getPlatform());
    pushLog.setSysOrigin(createRecordParam.getSysOrigin());
    pushLog.setBusinessScene(createRecordParam.getBusinessScene());
    pushLog.setDeviceType(createRecordParam.getDeviceType());
    pushLog.setFixedUserIds(createRecordParam.getFixedUserIds());
    pushLog.setLink(createRecordParam.getLink());
    pushLog.setOrigin(PUSH_ORIGIN_BACK);
    pushLog.setCreateUser(createRecordParam.getCreateUser());
    pushLog.setUpdateUser(createRecordParam.getCreateUser());
    pushLog.setCreateTime(now);
    pushLog.setUpdateTime(now);
    return pushLog;
  }

  private SysPushRecordLogCO toPushLogCO(SysPushRecordLog record) {
    return toPushLogCO(record, CollectionUtils.newHashMap());
  }

  private SysPushRecordLogCO toPushLogCO(SysPushRecordLog record, Map<Long, String> sendUserNameMap) {
    SysPushRecordLogCO result = new SysPushRecordLogCO();
    result.setId(record.getId());
    result.setTitle(record.getTitle());
    result.setContent(record.getContent());
    result.setSysOrigin(record.getSysOrigin());
    result.setPlatform(record.getPlatform());
    result.setBusinessScene(getBusinessSceneDesc(record.getBusinessScene()));
    result.setFixedUserIds(record.getFixedUserIds());
    result.setLink(record.getLink());
    result.setOrigin(record.getOrigin());
    result.setPushStatus(getPushStatusDesc(record.getPushStatus()));
    result.setDeviceType(record.getDeviceType());
    result.setFailQuantity(record.getFailQuantity());
    result.setSuccessQuantity(record.getSuccessQuantity());
    result.setSendUserName(sendUserNameMap.get(record.getCreateUser()));
    result.setCreateTime(record.getCreateTime());
    return result;
  }

  private String getBusinessSceneDesc(String businessScene) {
    String desc = MessageBusinessSceneEnum.getDesc(businessScene);
    return StringUtils.isBlank(desc) ? businessScene : desc;
  }

  private String getPushStatusDesc(String pushStatus) {
    if (Objects.equals(PUSH_STATUS_SUCCESS, pushStatus)) {
      return "成功";
    }
    if (PUSH_FAIL_STATUSES.contains(pushStatus)) {
      return "失败";
    }
    return pushStatus;
  }

  private PageQuery ensurePageQuery(PageCommand pageCommand) {
    PageQuery pageQuery = pageCommand.getPageQuery();
    if (Objects.isNull(pageQuery)) {
      pageQuery = new PageQuery();
      pageCommand.setPageQuery(pageQuery);
    }
    if (Objects.isNull(pageQuery.getCursor()) || pageQuery.getCursor() < 1) {
      pageQuery.setCursor(1);
    }
    if (Objects.isNull(pageQuery.getLimit()) || pageQuery.getLimit() < 1) {
      pageQuery.setLimit(DEFAULT_PAGE_LIMIT);
    }
    return pageQuery;
  }

  private int resolvePageLimit(PageQuery pageQuery) {
    if (Objects.isNull(pageQuery) || Objects.isNull(pageQuery.getLimit())
        || pageQuery.getLimit() < 1) {
      return DEFAULT_PAGE_LIMIT;
    }
    return pageQuery.getLimit();
  }
}
