package com.zioinfo.mes.uiws.worklog.service;

import com.zioinfo.mes.uiws.common.UiwsApiException;
import com.zioinfo.mes.uiws.common.UiwsCurrentUser;
import com.zioinfo.mes.uiws.common.UiwsDataScope;
import com.zioinfo.mes.uiws.common.UiwsErrorCode;
import com.zioinfo.mes.uiws.worklog.dto.WorklogDtos.*;
import com.zioinfo.mes.uiws.worklog.mapper.WorklogMapper;
import com.zioinfo.mes.uiws.worklog.model.UiwsWorklog;
import com.zioinfo.mes.uiws.worklog.model.UiwsWorklogCmt;
import com.zioinfo.mes.uiws.worklog.model.UiwsWorklogDtl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 업무일지(모듈 06) 서비스 — 원본 com.urp.uiws.worklog.service.WorklogService MyBatis 변환 이식.
 *
 * silo 단순화(코어 TB_CODE/TB_DEPT 미이식):
 *  - 근무상태/진행 코드 라벨: 코드값 그대로 노출(공통코드 미이식). 진행 라벨은 ONGOING/DONE 한글 매핑만 내장.
 *  - 진행상태 집계: ONGOING/DONE 고정 셋 0건 포함 반환(차트 범례 안정).
 *  - 댓글 권한: 원본 "관할 상무 이상"(부서계층+직급) → ERP RBAC MANAGER/ADMIN/CFO 로 완화 이식.
 *  - 댓글 알림: NaverWorks(외부) → MailSender 폴백(외부 API 금지 준수).
 */
@Service
@RequiredArgsConstructor
public class WorklogService {

    private static final String DEFAULT_PROGRESS = "ONGOING";
    private static final DateTimeFormatter DT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
    /** 진행 코드 → 라벨(공통코드 미이식 silo 보정). */
    private static final Map<String, String> PROGRESS_LABEL = Map.of("ONGOING", "진행중", "DONE", "종료");

    private final WorklogMapper mapper;
    private final WorklogNotifier notifier;

    // ------------------------------------------------------------------ 목록(리스트형)
    @Transactional(readOnly = true)
    public WorklogPage list(LocalDate fromDate, LocalDate toDate, String writerId, String progressCd, int page, int size) {
        LocalDate to = (toDate != null) ? toDate : LocalDate.now();
        LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1);
        UiwsDataScope.Scope scope = UiwsDataScope.current();
        String wid = blankToNull(writerId);
        String pc = blankToNull(progressCd);
        long total = mapper.countList(from, to, wid, pc, scope.all(), scope.ownerIds());
        List<UiwsWorklog> rows = mapper.searchList(from, to, wid, pc, scope.all(), scope.ownerIds(), size, page * size);
        return new WorklogPage(toListDtos(rows), total, page, size, totalPages(total, size));
    }

    // ------------------------------------------------------------------ 목록(달력형)
    @Transactional(readOnly = true)
    public List<WorklogCalendarDto> calendar(String yearMonth, String writerId) {
        YearMonth ym = parseYearMonth(yearMonth);
        LocalDate from = ym.atDay(1);
        LocalDate to = ym.atEndOfMonth();
        UiwsDataScope.Scope scope = UiwsDataScope.current();
        List<UiwsWorklog> rows = mapper.findByMonth(from, to, blankToNull(writerId), scope.all(), scope.ownerIds());
        // 공휴일 마스터 미연동 → isHoliday=false. 근무상태 라벨은 코드값 노출(공통코드 미이식).
        return rows.stream()
                .map(w -> new WorklogCalendarDto(w.getWorkDate().toString(), w.getWorklogId(),
                        w.getWorkStatusCd(), false, null))
                .toList();
    }

    // ------------------------------------------------------------------ 조회 팝업
    @Transactional(readOnly = true)
    public List<WorklogListDto> search(String keyword, String writerId) {
        UiwsDataScope.Scope scope = UiwsDataScope.current();
        List<UiwsWorklog> rows = mapper.searchPopup(blankToNull(keyword), blankToNull(writerId), scope.all(), scope.ownerIds());
        return toListDtos(rows);
    }

    // ------------------------------------------------------------------ 대시보드: 진행상태별 집계
    @Transactional(readOnly = true)
    public List<ProgressSummaryDto> progressSummary(LocalDate fromDate, LocalDate toDate) {
        LocalDate to = (toDate != null) ? toDate : LocalDate.now();
        LocalDate from = (fromDate != null) ? fromDate : to.minusMonths(1);
        UiwsDataScope.Scope scope = UiwsDataScope.current();
        Map<String, Long> counts = new LinkedHashMap<>();
        for (Map<String, Object> r : mapper.progressSummary(from, to, scope.all(), scope.ownerIds())) {
            counts.put(str(r.get("progressCd")), toLong(r.get("cnt")));
        }
        // 코드 정의 순서(진행중→종료)로 0건 포함 반환.
        List<ProgressSummaryDto> out = new ArrayList<>();
        for (String code : List.of("ONGOING", "DONE")) {
            out.add(new ProgressSummaryDto(code, PROGRESS_LABEL.getOrDefault(code, code), counts.getOrDefault(code, 0L)));
        }
        // 정의 외 코드값도 누락 없이 추가
        counts.forEach((k, v) -> {
            if (!"ONGOING".equals(k) && !"DONE".equals(k)) {
                out.add(new ProgressSummaryDto(k, k, v));
            }
        });
        return out;
    }

    // ------------------------------------------------------------------ 등록
    @Transactional
    public WorklogDetailDto create(WorklogSaveDto dto) {
        String actor = UiwsCurrentUser.id();
        UiwsWorklog w = new UiwsWorklog();
        applyHeader(w, dto);
        w.setWriterId((dto.writerId() != null && !dto.writerId().isBlank()) ? dto.writerId() : actor);
        w.setCreatedBy(actor);
        w.setCreatedAt(LocalDateTime.now());
        mapper.insertWorklog(w);

        List<WorklogDtlDto> toInsert = (dto.details() == null) ? List.of()
                : dto.details().stream().filter(d -> !"DEL".equalsIgnoreCase(safeState(d.state()))).toList();
        validateNoOverlap(toInsert);
        for (WorklogDtlDto d : toInsert) {
            mapper.insertDtl(newDtl(w.getWorklogId(), d, actor));
        }
        return detail(w.getWorklogId());
    }

    // ------------------------------------------------------------------ 상세
    @Transactional(readOnly = true)
    public WorklogDetailDto detail(Long worklogId) {
        return toDetail(findWorklog(worklogId));
    }

    // ------------------------------------------------------------------ 수정(시간대별 일괄, _state)
    @Transactional
    public WorklogDetailDto update(Long worklogId, WorklogSaveDto dto) {
        String actor = UiwsCurrentUser.id();
        UiwsWorklog w = findWorklog(worklogId);
        applyHeader(w, dto);
        if (dto.writerId() != null && !dto.writerId().isBlank()) {
            w.setWriterId(dto.writerId());
        }
        w.setUpdatedBy(actor);
        w.setUpdatedAt(LocalDateTime.now());
        mapper.updateWorklog(w);

        List<UiwsWorklogDtl> existing = mapper.findDtlByWorklogId(worklogId);
        Map<Long, UiwsWorklogDtl> existingById = existing.stream()
                .collect(Collectors.toMap(UiwsWorklogDtl::getDtlId, d -> d));

        List<WorklogDtlDto> details = (dto.details() == null) ? List.of() : dto.details();
        List<WorklogDtlDto> survivors = new ArrayList<>();
        for (WorklogDtlDto d : details) {
            String st = safeState(d.state());
            if ("DEL".equalsIgnoreCase(st)) {
                if (d.dtlId() != null && existingById.containsKey(d.dtlId())) {
                    mapper.deleteDtlById(d.dtlId());
                }
                continue;
            }
            if ("MOD".equalsIgnoreCase(st) && d.dtlId() != null) {
                UiwsWorklogDtl tgt = existingById.get(d.dtlId());
                if (tgt == null) {
                    throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND, "수정 대상 상세를 찾을 수 없습니다.");
                }
                applyDtl(tgt, d);
                tgt.setUpdatedBy(actor);
                tgt.setUpdatedAt(LocalDateTime.now());
                mapper.updateDtl(tgt);
                survivors.add(d);
                continue;
            }
            // _state 미표기 + 기존 dtlId 보유 = 미변경 유지행 → INSERT 금지
            if (d.dtlId() != null && existingById.containsKey(d.dtlId())) {
                survivors.add(d);
                continue;
            }
            // ADD 또는 진짜 신규 → INSERT
            mapper.insertDtl(newDtl(worklogId, d, actor));
            survivors.add(d);
        }
        validateNoOverlap(survivors);
        return detail(worklogId);
    }

    // ------------------------------------------------------------------ 삭제
    @Transactional
    public void delete(Long worklogId) {
        if (!mapper.existsWorklogById(worklogId)) {
            throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND);
        }
        // 상세/댓글은 DB ON DELETE CASCADE. 헤더 삭제로 일괄 제거.
        mapper.deleteWorklogById(worklogId);
    }

    // ------------------------------------------------------------------ 댓글 등록(+알림)
    @Transactional
    public WorklogCommentDto addComment(Long worklogId, CommentRequest req) {
        String actor = UiwsCurrentUser.id();
        UiwsWorklog w = findWorklog(worklogId);
        assertCanComment();

        UiwsWorklogCmt c = new UiwsWorklogCmt();
        c.setWorklogId(worklogId);
        c.setCmtContent(req.cmtContent());
        c.setWriterId(actor);
        c.setKakaoSentYn("N");
        c.setConfirmYn("N");
        c.setCreatedBy(actor);
        c.setCreatedAt(LocalDateTime.now());
        mapper.insertCmt(c);

        // 알림: 일지 작성자에게 메일 폴백(외부 메신저 미사용).
        String writerEmail = userEmail(w.getWriterId());
        String subject = "[UIWS] 업무일지 새 댓글: " + w.getTitle();
        String body = "업무일지에 새 댓글이 등록되었습니다.\n내용: " + req.cmtContent();
        boolean sent = notifier.notifyComment(w.getWriterId(), writerEmail, subject, body);
        if (sent) {
            mapper.updateCmtKakaoSent(c.getCmtId());
            c.setKakaoSentYn("Y");
        }

        String writerNm = userNames(List.of(actor)).getOrDefault(actor, actor);
        return new WorklogCommentDto(c.getCmtId(), c.getCmtContent(), writerNm,
                fmt(c.getCreatedAt()), c.getKakaoSentYn(), c.getConfirmYn());
    }

    // ------------------------------------------------------------------ 댓글 확인(작성자 전용)
    @Transactional
    public void confirmComment(Long cmtId) {
        String actor = UiwsCurrentUser.id();
        UiwsWorklogCmt c = mapper.findCmtById(cmtId);
        if (c == null) {
            throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND, "댓글을 찾을 수 없습니다.");
        }
        UiwsWorklog w = findWorklog(c.getWorklogId());
        if (!w.getWriterId().equals(actor)) {
            throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "본인 업무일지의 댓글만 확인 처리할 수 있습니다.");
        }
        mapper.confirmCmt(cmtId, actor);
    }

    // ------------------------------------------------------------------ 내가 단 댓글 중 작성자 미확인 목록
    @Transactional(readOnly = true)
    public List<UnconfirmedCommentDto> unconfirmedByMe() {
        String me = UiwsCurrentUser.id();
        List<UiwsWorklogCmt> rows = mapper.findUnconfirmedByWriter(me);
        if (rows.isEmpty()) {
            return List.of();
        }
        List<Long> worklogIds = rows.stream().map(UiwsWorklogCmt::getWorklogId).distinct().toList();
        Map<Long, UiwsWorklog> worklogs = mapper.findWorklogsByIds(worklogIds).stream()
                .collect(Collectors.toMap(UiwsWorklog::getWorklogId, wl -> wl));
        Map<String, String> writerNames = userNames(
                worklogs.values().stream().map(UiwsWorklog::getWriterId).toList());
        return rows.stream().map(c -> {
            UiwsWorklog wl = worklogs.get(c.getWorklogId());
            return new UnconfirmedCommentDto(c.getCmtId(), c.getWorklogId(),
                    wl != null ? wl.getTitle() : null,
                    wl != null ? writerNames.getOrDefault(wl.getWriterId(), wl.getWriterId()) : null,
                    c.getCmtContent(), fmt(c.getCreatedAt()));
        }).toList();
    }

    // ================================================================== helpers

    /** 댓글 권한(silo 완화): ERP RBAC MANAGER/ADMIN/CFO 만 작성 가능. */
    private void assertCanComment() {
        if (!UiwsCurrentUser.isManagerOrAbove()) {
            throw new UiwsApiException(UiwsErrorCode.FORBIDDEN, "댓글은 관리자/매니저 권한만 작성할 수 있습니다.");
        }
    }

    private List<WorklogListDto> toListDtos(List<UiwsWorklog> rows) {
        Map<Long, Long> cmtCounts = commentCounts(rows.stream().map(UiwsWorklog::getWorklogId).toList());
        Map<String, String> writerNames = userNames(rows.stream().map(UiwsWorklog::getWriterId).toList());
        return rows.stream()
                .map(w -> new WorklogListDto(w.getWorklogId(), w.getTitle(),
                        writerNames.getOrDefault(w.getWriterId(), w.getWriterId()),
                        w.getWorkDate().toString(), w.getProgressCd(),
                        cmtCounts.getOrDefault(w.getWorklogId(), 0L)))
                .toList();
    }

    private WorklogDetailDto toDetail(UiwsWorklog w) {
        List<UiwsWorklogDtl> dtls = mapper.findDtlByWorklogId(w.getWorklogId());
        List<UiwsWorklogCmt> cmts = mapper.findCmtByWorklogId(w.getWorklogId());
        List<String> nameIds = new ArrayList<>();
        nameIds.add(w.getWriterId());
        cmts.forEach(c -> nameIds.add(c.getWriterId()));
        Map<String, String> writerNames = userNames(nameIds);

        List<WorklogDtlDto> details = dtls.stream().map(WorklogDtlDto::from).toList();
        List<WorklogCommentDto> comments = cmts.stream()
                .map(c -> new WorklogCommentDto(c.getCmtId(), c.getCmtContent(),
                        writerNames.getOrDefault(c.getWriterId(), c.getWriterId()),
                        fmt(c.getCreatedAt()), c.getKakaoSentYn(), c.getConfirmYn()))
                .toList();
        return new WorklogDetailDto(w.getWorklogId(), w.getTitle(), w.getWriterId(),
                writerNames.getOrDefault(w.getWriterId(), w.getWriterId()), w.getWorkDate().toString(),
                w.getWorkStatusCd(), w.getProgressCd(), w.getRepeatYn(),
                w.getRepeatStartDate() != null ? w.getRepeatStartDate().toString() : null,
                w.getRepeatEndDate() != null ? w.getRepeatEndDate().toString() : null,
                details, comments);
    }

    private void applyHeader(UiwsWorklog w, WorklogSaveDto dto) {
        w.setTitle(dto.title());
        w.setWorkDate(parseDate(dto.workDate()));
        w.setWorkStatusCd(dto.workStatusCd());
        w.setProgressCd((dto.progressCd() != null && !dto.progressCd().isBlank()) ? dto.progressCd() : DEFAULT_PROGRESS);
        w.setRepeatYn((dto.repeatYn() != null && !dto.repeatYn().isBlank()) ? dto.repeatYn() : "N");
        w.setRepeatStartDate(parseDateNullable(dto.repeatStartDate()));
        w.setRepeatEndDate(parseDateNullable(dto.repeatEndDate()));
    }

    private UiwsWorklogDtl newDtl(Long worklogId, WorklogDtlDto d, String actor) {
        UiwsWorklogDtl e = new UiwsWorklogDtl();
        e.setWorklogId(worklogId);
        applyDtl(e, d);
        e.setCreatedBy(actor);
        e.setCreatedAt(LocalDateTime.now());
        return e;
    }

    private void applyDtl(UiwsWorklogDtl e, WorklogDtlDto d) {
        if (d.startHour() == null || d.endHour() == null
                || d.startHour() < 0 || d.endHour() > 24 || d.endHour() < d.startHour()) {
            throw new UiwsApiException(UiwsErrorCode.WORKLOG_TIME_INVALID);
        }
        e.setStartHour(d.startHour());
        e.setEndHour(d.endHour());
        e.setWorkTypeCd(d.workTypeCd());
        e.setCompanyId(d.companyId());
        e.setWorkContent(d.workContent());
        e.setIssueContent(d.issueContent());
        e.setSortOrd(d.sortOrd() != null ? d.sortOrd() : 0);
    }

    private void validateNoOverlap(List<WorklogDtlDto> details) {
        List<WorklogDtlDto> rows = details.stream()
                .filter(d -> d.startHour() != null && d.endHour() != null)
                .sorted((a, b) -> Integer.compare(a.startHour(), b.startHour()))
                .toList();
        for (int i = 1; i < rows.size(); i++) {
            WorklogDtlDto prev = rows.get(i - 1);
            WorklogDtlDto cur = rows.get(i);
            if (cur.startHour() < prev.endHour()) {
                throw new UiwsApiException(UiwsErrorCode.WORKLOG_TIME_OVERLAP);
            }
        }
    }

    private UiwsWorklog findWorklog(Long id) {
        UiwsWorklog w = mapper.findWorklogById(id);
        if (w == null) {
            throw new UiwsApiException(UiwsErrorCode.WORKLOG_NOT_FOUND);
        }
        return w;
    }

    private Map<Long, Long> commentCounts(List<Long> worklogIds) {
        if (worklogIds == null || worklogIds.isEmpty()) {
            return Map.of();
        }
        Map<Long, Long> result = new LinkedHashMap<>();
        for (Map<String, Object> cc : mapper.countCommentsByWorklogIds(worklogIds)) {
            result.put(toLong(cc.get("worklogId")), toLong(cc.get("cnt")));
        }
        return result;
    }

    private Map<String, String> userNames(List<String> userIds) {
        List<String> ids = userIds.stream().filter(s -> s != null && !s.isBlank()).distinct().toList();
        if (ids.isEmpty()) {
            return Map.of();
        }
        return mapper.findUserNames(ids).stream()
                .collect(Collectors.toMap(r -> r.get("username"), r -> r.get("fullName"), (a, b) -> a));
    }

    /** writerId(username) → email (댓글 알림 메일 발송용). 미보유 시 null. */
    private String userEmail(String writerId) {
        if (writerId == null || writerId.isBlank()) {
            return null;
        }
        return mapper.findUserEmail(writerId);
    }

    private static int totalPages(long total, int size) {
        return size <= 0 ? 0 : (int) ((total + size - 1) / size);
    }

    private static String blankToNull(String s) {
        return (s == null || s.isBlank()) ? null : s;
    }

    private static String safeState(String s) {
        return s == null ? "" : s.trim();
    }

    private static String str(Object o) {
        return o == null ? "" : o.toString();
    }

    private static long toLong(Object o) {
        if (o == null) {
            return 0L;
        }
        return (o instanceof Number n) ? n.longValue() : Long.parseLong(o.toString());
    }

    private static String fmt(LocalDateTime dt) {
        return dt != null ? dt.format(DT) : null;
    }

    private static LocalDate parseDate(String s) {
        try {
            return LocalDate.parse(s);
        } catch (Exception e) {
            throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "날짜 형식이 올바르지 않습니다(yyyy-MM-dd).");
        }
    }

    private static LocalDate parseDateNullable(String s) {
        return (s == null || s.isBlank()) ? null : parseDate(s);
    }

    private static YearMonth parseYearMonth(String s) {
        try {
            if (s == null || s.isBlank()) {
                return YearMonth.now();
            }
            return YearMonth.parse(s);
        } catch (Exception e) {
            throw new UiwsApiException(UiwsErrorCode.INVALID_REQUEST, "yearMonth 형식이 올바르지 않습니다(yyyy-MM).");
        }
    }
}
