package rd.service.schedule;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.yitter.idgen.YitIdHelper;
import lombok.extern.slf4j.Slf4j;
import rd.dao.mapper.course.CourseSelectionMapper;
import rd.dao.mapper.schedule.CourseScheduleMapper;
import rd.data.dto.schedule.CourseScheduleDTO;
import rd.data.db.CourseScheduleWithCourseIdName;
import rd.data.db.table.CourseSchedule;
import rd.data.db.table.CourseSelection;
import rd.data.model.response.PaginationWrapper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class CourseScheduleService {
    private static final Cache<Long, CourseSchedule> courseScheduleIdcache = Caffeine.newBuilder()
            .expireAfterAccess(1, TimeUnit.HOURS)
            .initialCapacity(16)
            .maximumSize(64)
            .build();

    private static final Cache<String, List<CourseSchedule>> courseScheduleCache = Caffeine.newBuilder()
            .expireAfterAccess(1, TimeUnit.HOURS)
            .initialCapacity(16)
            .maximumSize(64)
            .build();

    private final CourseScheduleMapper courseScheduleMapper;
    private final CourseSelectionMapper courseSelectionMapper;

    public CourseScheduleService(CourseScheduleMapper courseScheduleMapper,
                                 CourseSelectionMapper courseSelectionMapper) {

        this.courseScheduleMapper = courseScheduleMapper;
        this.courseSelectionMapper = courseSelectionMapper;
    }

    public CourseScheduleDTO getScheduleDTO(Long id) {
        var schedule = getSchedule(id);
        return convert(schedule);
    }

    public CourseSchedule getSchedule(Long id) {
        return getSchedule(id, true);
    }

    private CourseSchedule getSchedule(Long scheduleId, boolean loadFromCache) {
        if (loadFromCache) {
            var courseSchedule = courseScheduleIdcache.getIfPresent(scheduleId);
            if (courseSchedule != null) {
                return courseSchedule;
            }
        }

        var courseSchedule = this.getScheduleFromDb(scheduleId);
        this.putCache(courseSchedule);
        return courseSchedule;
    }

    private CourseSchedule getScheduleFromDb(Long id) {
        return courseScheduleMapper.selectOne(new QueryWrapper<CourseSchedule>()
                .eq("id", id)
                .eq("deleted", false)
                .last("limit 1")
        );
    }

    private List<CourseSchedule> getScheduleOfCourse(Long courseId, String term) {
        String cacheKey = new StringJoiner(":").add(courseId.toString()).add(term).toString();
        return courseScheduleCache.get(cacheKey, k ->
                courseScheduleMapper.selectList(new LambdaQueryWrapper<CourseSchedule>()
                        .eq(CourseSchedule::getAcademicTerm, term)
                        .eq(CourseSchedule::getCourse, courseId)
                        .eq(CourseSchedule::getDeleted, false)
                )
        );
    }

    public List<CourseScheduleDTO> getTermCourseSchedule(Long courseId, String term) {
        var schedules = getScheduleOfCourse(courseId, term);
        return schedules.stream()
                .map(CourseScheduleService::convert)
                .toList();
    }

    public PaginationWrapper<List<CourseScheduleDTO>> getScheduleOfCourse(Long courseId, String term, int page, int pageSize) {
        if (pageSize == 0) {
            pageSize = 1024;
        }

        Page<CourseSchedule> queryPage = new Page<>(page, pageSize);
        var pageRecord = courseScheduleMapper.selectPage(queryPage, new LambdaQueryWrapper<CourseSchedule>()
                .eq(term != null && !term.isEmpty(), CourseSchedule::getAcademicTerm, term)
                .eq(CourseSchedule::getCourse, courseId)
                .eq(CourseSchedule::getDeleted, false)
        );

        return wrapPaginationResult(pageRecord);
    }

    public List<CourseScheduleWithCourseIdName> getStudentTermSchedule(Long studentUid, String term) {
        return courseSelectionMapper.getStudentSelectedSchedulesDetail(studentUid, term);
    }

    private PaginationWrapper<List<CourseScheduleDTO>> wrapPaginationResult(Page<CourseSchedule> pageRecord) {
        var records = pageRecord.getRecords();
        var resultList = new ArrayList<CourseScheduleDTO>(records.size());
        for (var schedule : records) {
            resultList.add(convert(schedule));
        }

        return new PaginationWrapper<>(
                (int) pageRecord.getCurrent(),
                (int) pageRecord.getSize(),
                (int) pageRecord.getTotal(),
                resultList
        );
    }

    private static CourseSchedule convert(CourseScheduleDTO dto) {
        if (dto == null) {
            return null;
        }

        CourseSchedule courseSchedule = new CourseSchedule();

        courseSchedule.setId(dto.getId());
        courseSchedule.setCourse(dto.getCourse());
        courseSchedule.setStartWeek(dto.getStartWeek());
        courseSchedule.setEndWeek(dto.getEndWeek());
        courseSchedule.setTime(dto.getTime());
        courseSchedule.setWeekday(dto.getWeekday());
        courseSchedule.setAcademicTerm(dto.getAcademicTerm());
        courseSchedule.setPlace(dto.getPlace());

        return courseSchedule;

    }

    private static CourseScheduleDTO convert(CourseSchedule courseSchedule) {
        if (courseSchedule == null) {
            return null;
        }

        CourseScheduleDTO courseScheduleDTO = new CourseScheduleDTO();

        courseScheduleDTO.setId(courseSchedule.getId());
        courseScheduleDTO.setCourse(courseSchedule.getCourse());
        courseScheduleDTO.setStartWeek(courseSchedule.getStartWeek());
        courseScheduleDTO.setEndWeek(courseSchedule.getEndWeek());
        courseScheduleDTO.setTime(courseSchedule.getTime());
        courseScheduleDTO.setWeekday(courseSchedule.getWeekday());
        courseScheduleDTO.setAcademicTerm(courseSchedule.getAcademicTerm());
        courseScheduleDTO.setPlace(courseSchedule.getPlace());

        return courseScheduleDTO;
    }

    private void putCache(CourseSchedule courseSchedule) {
        if (courseSchedule == null) {
            return;
        }

        courseScheduleIdcache.put(courseSchedule.getId(), courseSchedule);

        String key = new StringJoiner(":")
                .add(courseSchedule.getCourse().toString())
                .add(courseSchedule.getAcademicTerm())
                .toString();

        var list = courseScheduleCache.getIfPresent(key);
        if (list == null) {
            list = new ArrayList<>(8);
            list.add(courseSchedule);
            courseScheduleCache.put(key, list);
        }

        list.remove(courseSchedule);
        list.add(courseSchedule);
    }

    @Transactional
    public CourseSchedule add(Long courseId, CourseScheduleDTO dto) {
        Long id = YitIdHelper.nextId();

        CourseSchedule courseSchedule = convert(dto);
        courseSchedule.setId(id);
        courseSchedule.setCourse(courseId);

        courseScheduleMapper.insert(courseSchedule);

        return this.getSchedule(id, false);
    }

    @Transactional
    public CourseSchedule update(Long id, CourseScheduleDTO dto) {
        CourseSchedule courseSchedule = convert(dto);
        courseSchedule.setId(id);

        courseScheduleMapper.update(courseSchedule, new QueryWrapper<CourseSchedule>()
                .eq("id", id)
                .eq("deleted", false)
        );

        return this.getSchedule(id, false);
    }

    @Async
    @Transactional
    public void cloneSchedule(Long course, String sourceTerm, String targetTerm) {
        var scheduleList = this.getScheduleOfCourse(course, sourceTerm);
        if (scheduleList.isEmpty()) {
            return;
        }

        for (CourseSchedule schedule : scheduleList) {
            Long id = YitIdHelper.nextId();
            CourseSchedule clone = new CourseSchedule();
            clone.setId(id);
            clone.setCourse(course);
            clone.setStartWeek(schedule.getStartWeek());
            clone.setEndWeek(schedule.getEndWeek());
            clone.setTime(schedule.getTime());
            clone.setWeekday(schedule.getWeekday());
            clone.setAcademicTerm(targetTerm);
            clone.setPlace(schedule.getPlace());
            clone.setCreateTime(LocalDateTime.now());
            clone.setEditTime(LocalDateTime.now());
            clone.setDeleted(false);

            courseScheduleMapper.insert(clone);
        }
    }

    public void delete(Long id) {
        CourseSchedule schedule = getSchedule(id);
        if (schedule == null) {
            return;
        }

        schedule.setDeleted(true);
        schedule.setEditTime(LocalDateTime.now());

        courseScheduleMapper.update(schedule, new QueryWrapper<CourseSchedule>()
                .eq("id", id)
                .eq("deleted", false)
        );

        courseScheduleIdcache.invalidate(id);
        String key = new StringJoiner(":")
                .add(schedule.getCourse().toString())
                .add(schedule.getAcademicTerm())
                .toString();
        courseScheduleCache.invalidate(key);
    }

    public int getSelectedCount(Long scheduleId) {
        return courseSelectionMapper.selectCount(new LambdaQueryWrapper<CourseSelection>()
                .eq(CourseSelection::getSchedule, scheduleId)
                .eq(CourseSelection::getDeleted, false)
        ).intValue();
    }
}
