package org.dromara.app.task;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.app.domain.AppDevice;
import org.dromara.app.domain.AppSchedule;
import org.dromara.app.domain.AppScheduleDetail;
import org.dromara.app.domain.AppSchedulingDevice;
import org.dromara.app.domain.bo.AppWateringLogBo;
import org.dromara.app.mapper.AppDeviceMapper;
import org.dromara.app.mapper.AppScheduleDetailMapper;
import org.dromara.app.mapper.AppScheduleMapper;
import org.dromara.app.mapper.AppSchedulingDeviceMapper;
import org.dromara.app.service.IAppWateringLogService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 设备离线期间排程浇水记录补记任务。
 * <p>
 * 设备断网后可能仍按本地排程浇水，但服务端收不到完成上报。本任务按已开启排程生成预估历史记录，
 * 设备恢复联网后上报同一记录时会在 {@link org.dromara.app.service.IAppWateringLogService#confirmScheduleLog}
 * 中更新为设备确认记录。
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class OfflineScheduleWateringLogTask {

    private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm");

    private final AppDeviceMapper deviceMapper;
    private final AppSchedulingDeviceMapper schedulingDeviceMapper;
    private final AppScheduleMapper scheduleMapper;
    private final AppScheduleDetailMapper scheduleDetailMapper;
    private final IAppWateringLogService wateringLogService;

    @Value("${mqtt.offline-schedule-log.enabled:true}")
    private boolean enabled;

    @Value("${mqtt.offline-schedule-log.lookback-days:7}")
    private int lookbackDays;

    @Scheduled(fixedDelayString = "${mqtt.offline-schedule-log.interval-ms:300000}")
    public void fillOfflineScheduleLogs() {
        if (!enabled) {
            return;
        }

        List<AppDevice> offlineDevices = deviceMapper.selectList(
            Wrappers.<AppDevice>lambdaQuery()
                .eq(AppDevice::getStatus, "0")
                .isNotNull(AppDevice::getUserId)
                .select(AppDevice::getDeviceNo, AppDevice::getUserId)
        );
        if (offlineDevices.isEmpty()) {
            return;
        }

        int saved = 0;
        for (AppDevice device : offlineDevices) {
            saved += fillDeviceScheduleLogs(device);
        }
        if (saved > 0) {
            log.info("[离线排程补记] 已补记离线排程浇水记录 数量={}", saved);
        }
    }

    private int fillDeviceScheduleLogs(AppDevice device) {
        List<AppSchedulingDevice> bindings = schedulingDeviceMapper.selectList(
            Wrappers.<AppSchedulingDevice>lambdaQuery()
                .eq(AppSchedulingDevice::getDeviceNo, device.getDeviceNo())
        );
        if (bindings.isEmpty()) {
            return 0;
        }

        int saved = 0;
        LocalDateTime now = LocalDateTime.now();
        LocalDate startDate = LocalDate.now().minusDays(Math.max(lookbackDays, 1));
        for (AppSchedulingDevice binding : bindings) {
            AppSchedule schedule = scheduleMapper.selectById(binding.getScheduleId());
            if (schedule == null || !"1".equals(schedule.getStatus())) {
                continue;
            }
            List<AppScheduleDetail> details = scheduleDetailMapper.selectList(
                Wrappers.<AppScheduleDetail>lambdaQuery()
                    .eq(AppScheduleDetail::getScheduleId, schedule.getId())
            );
            for (AppScheduleDetail detail : details) {
                saved += fillDetailLogs(device, schedule, detail, startDate, now);
            }
        }
        return saved;
    }

    private int fillDetailLogs(
        AppDevice device,
        AppSchedule schedule,
        AppScheduleDetail detail,
        LocalDate startDate,
        LocalDateTime now
    ) {
        if (detail == null || "0".equals(detail.getStatus()) || detail.getWeekday() == null) {
            return 0;
        }

        List<TimeSlot> timeSlots = parseTimeSlots(detail.getTimeData());
        if (timeSlots.isEmpty()) {
            return 0;
        }

        int saved = 0;
        for (LocalDate date = startDate; !date.isAfter(LocalDate.now()); date = date.plusDays(1)) {
            if (toScheduleWeekday(date.getDayOfWeek()) != detail.getWeekday()) {
                continue;
            }
            for (TimeSlot timeSlot : timeSlots) {
                AppWateringLogBo logBo = buildLog(device, schedule, detail, date, timeSlot, now);
                if (logBo != null && Boolean.TRUE.equals(wateringLogService.saveEstimatedScheduleLog(logBo))) {
                    saved++;
                }
            }
        }
        return saved;
    }

    private AppWateringLogBo buildLog(
        AppDevice device,
        AppSchedule schedule,
        AppScheduleDetail detail,
        LocalDate date,
        TimeSlot timeSlot,
        LocalDateTime now
    ) {
        if (StringUtils.isBlank(timeSlot.getStartTime()) || timeSlot.getDurationMin() == null || timeSlot.getDurationMin() <= 0) {
            return null;
        }

        LocalTime startLocalTime;
        try {
            startLocalTime = LocalTime.parse(timeSlot.getStartTime(), TIME_FORMATTER);
        } catch (DateTimeParseException e) {
            log.warn("[离线排程补记] 排程开始时间格式错误 设备编号={} 排程ID={} 开始时间={}",
                device.getDeviceNo(), schedule.getId(), timeSlot.getStartTime());
            return null;
        }

        LocalDateTime startTime = LocalDateTime.of(date, startLocalTime);
        LocalDateTime endTime = startTime.plusMinutes(timeSlot.getDurationMin());
        if (endTime.isAfter(now)) {
            return null;
        }

        AppWateringLogBo logBo = new AppWateringLogBo();
        logBo.setDeviceNo(device.getDeviceNo());
        logBo.setUserId(device.getUserId());
        logBo.setScheduleId(schedule.getId());
        logBo.setStartTime(toDate(startTime));
        logBo.setEndTime(toDate(endTime));
        logBo.setDurationMin(String.valueOf(timeSlot.getDurationMin()));
        logBo.setZones(detail.getZones() == null ? null : String.valueOf(detail.getZones()));
        logBo.setTriggerType("0");
        logBo.setCreateTime(new Date());
        logBo.setRemark("设备离线，按排程自动补记");
        return logBo;
    }

    private List<TimeSlot> parseTimeSlots(String timeData) {
        if (StringUtils.isBlank(timeData)) {
            return new ArrayList<>();
        }
        try {
            return JsonUtils.parseArray(timeData, TimeSlot.class);
        } catch (RuntimeException e) {
            log.warn("[离线排程补记] 排程时间数据解析失败 timeData={}", timeData);
            return new ArrayList<>();
        }
    }

    private int toScheduleWeekday(DayOfWeek dayOfWeek) {
        return dayOfWeek.getValue();
    }

    private Date toDate(LocalDateTime dateTime) {
        return Date.from(dateTime.atZone(ZoneId.systemDefault()).toInstant());
    }

    @Data
    public static class TimeSlot {
        private String startTime;
        private Integer durationMin;
    }
}
