package com.zioinfo.kintex.work.approval;

import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.PageResponse;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalActionRequest;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalDetailDto;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalHistoryDto;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalLineDto;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalLineSaveRequest;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalListDto;
import com.zioinfo.kintex.work.approval.dto.ApprovalDtos.ApprovalSaveRequest;
import com.zioinfo.kintex.work.notification.NotificationService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * §5B 공통 전자결재(F028) 서비스 — 상태머신 서버 권위(WISE/UIWS approval 이식).
 *
 * <p>직렬 다단계(step 1→N 순차) 결재. 상태 전이:
 * DRAFT →(submit)→ SUBMITTED →(approve 중간)→ IN_PROGRESS →(approve 마지막)→ APPROVED,
 * (reject)→ REJECTED →(resubmit, version+1)→ SUBMITTED, (cancel)→ CANCELED.
 * 승인/반려는 <b>현재 단계 결재선 당사자</b>만 가능(그 외 403). 조회는 기안자·결재선 참여자만.
 * 알림은 {@link NotificationService#notify}(TYPE=APPROVAL) 재사용(신규 채널 없음).
 */
@Service
public class ApprovalService {

    private final ApprovalMapper mapper;
    private final NotificationService notificationService;

    public ApprovalService(ApprovalMapper mapper, NotificationService notificationService) {
        this.mapper = mapper;
        this.notificationService = notificationService;
    }

    // ─────────────────────────────── 결재함 목록
    public PageResponse<ApprovalListDto> list(KintexPrincipal principal, String box,
                                              String keyword, int page, int size) {
        int p = Math.max(page, 0);
        int s = size <= 0 ? 20 : Math.min(size, 200);
        String b = (box == null || box.isBlank()) ? "DRAFTED" : box.trim().toUpperCase();
        if (!List.of("DRAFT", "DRAFTED", "PENDING", "DONE").contains(b)) {
            throw new ApiException(ErrorCode.VALIDATION, "box 는 DRAFT/DRAFTED/PENDING/DONE 중 하나여야 합니다.");
        }
        Map<String, Object> q = new HashMap<>();
        q.put("tenantId", principal.tenantId());
        q.put("me", principal.userId());
        q.put("box", b);
        q.put("keyword", (keyword == null || keyword.isBlank()) ? null : keyword.trim());
        q.put("size", s);
        q.put("offset", p * s);
        List<Map<String, Object>> rows = mapper.searchBox(q);
        long total = mapper.countBox(q);
        return PageResponse.of(rows.stream().map(ApprovalService::toListDto).toList(), p, s, total);
    }

    // ─────────────────────────────── 상세
    public ApprovalDetailDto get(KintexPrincipal principal, String id) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        List<Map<String, Object>> lines = mapper.findLines(id);
        assertReadable(principal, a, lines);
        return toDetail(principal, a, lines, mapper.findHistory(id));
    }

    // ─────────────────────────────── 기안(DRAFT)
    @Transactional
    public ApprovalDetailDto create(KintexPrincipal principal, ApprovalSaveRequest req) {
        String id = "ap-" + UUID.randomUUID().toString().substring(0, 12);
        Map<String, Object> p = new HashMap<>();
        p.put("id", id);
        p.put("tenantId", principal.tenantId());
        p.put("eventId", req.eventId());
        p.put("docType", req.docType());
        p.put("title", req.title());
        p.put("content", req.content());
        p.put("refType", req.refType());
        p.put("refId", req.refId());
        p.put("drafterId", principal.userId());
        p.put("drafterName", principal.displayName());
        mapper.insert(p);
        replaceLines(id, req.lines());
        return get(principal, id);
    }

    // ─────────────────────────────── 상신(DRAFT→SUBMITTED)
    @Transactional
    public ApprovalDetailDto submit(KintexPrincipal principal, String id) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        assertDrafter(principal, a);
        if (!"DRAFT".equals(status(a))) {
            throw new ApiException(ErrorCode.CONFLICT, "임시저장(DRAFT) 상태만 상신할 수 있습니다.");
        }
        startPending(principal, id, "SUBMIT");
        return get(principal, id);
    }

    // ─────────────────────────────── 승인
    @Transactional
    public ApprovalDetailDto approve(KintexPrincipal principal, String id, ApprovalActionRequest req) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        assertInFlight(a);
        Map<String, Object> current = requireCurrentApprover(principal, id);
        String comment = req == null ? null : req.comment();

        Map<String, Object> up = new HashMap<>();
        up.put("id", str(current.get("id")));
        up.put("lineStatus", "APPROVED");
        up.put("comment", comment);
        up.put("acted", true);
        mapper.updateLine(up);
        addHistory(principal, id, intVal(current.get("stepNo")), "APPROVE", comment);

        Map<String, Object> next = nextWaiting(mapper.findLines(id), intVal(current.get("stepNo")));
        if (next != null) {
            Map<String, Object> nl = new HashMap<>();
            nl.put("id", str(next.get("id")));
            nl.put("lineStatus", "CURRENT");
            mapper.updateLine(nl);
            Map<String, Object> doc = new HashMap<>();
            doc.put("id", id);
            doc.put("status", "IN_PROGRESS");
            doc.put("curStep", intVal(next.get("stepNo")));
            mapper.updateDoc(doc);
            notify(str(next.get("approverId")), a, "[결재 요청] " + str(a.get("title")),
                    "결재할 문서가 도착했습니다.", id);
        } else {
            complete(a, id);
        }
        return get(principal, id);
    }

    // ─────────────────────────────── 반려(사유 필수)
    @Transactional
    public ApprovalDetailDto reject(KintexPrincipal principal, String id, ApprovalActionRequest req) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        assertInFlight(a);
        Map<String, Object> current = requireCurrentApprover(principal, id);
        String comment = req == null ? null : req.comment();
        if (comment == null || comment.isBlank()) {
            throw new ApiException(ErrorCode.VALIDATION, "반려 사유(의견)를 입력해야 합니다.");
        }
        Map<String, Object> up = new HashMap<>();
        up.put("id", str(current.get("id")));
        up.put("lineStatus", "REJECTED");
        up.put("comment", comment);
        up.put("acted", true);
        mapper.updateLine(up);
        addHistory(principal, id, intVal(current.get("stepNo")), "REJECT", comment);

        Map<String, Object> doc = new HashMap<>();
        doc.put("id", id);
        doc.put("status", "REJECTED");
        doc.put("completedAt", true);
        mapper.updateDoc(doc);
        notify(str(a.get("drafterId")), a, "[결재 반려] " + str(a.get("title")),
                "결재 문서가 반려되었습니다.", id);
        return get(principal, id);
    }

    // ─────────────────────────────── 재상신(REJECTED→SUBMITTED, version+1)
    @Transactional
    public ApprovalDetailDto resubmit(KintexPrincipal principal, String id, ApprovalSaveRequest req) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        assertDrafter(principal, a);
        if (!"REJECTED".equals(status(a))) {
            throw new ApiException(ErrorCode.CONFLICT, "반려(REJECTED) 상태만 재상신할 수 있습니다.");
        }
        Map<String, Object> doc = new HashMap<>();
        doc.put("id", id);
        doc.put("version", intVal(a.get("version")) + 1);
        doc.put("clearCompleted", true);
        if (req != null) {
            if (req.title() != null && !req.title().isBlank()) {
                doc.put("title", req.title().trim());
            }
            if (req.content() != null) {
                doc.put("content", req.content());
            }
            if (req.docType() != null && !req.docType().isBlank()) {
                doc.put("docType", req.docType().trim());
            }
        }
        mapper.updateDoc(doc);
        if (req != null && req.lines() != null && !req.lines().isEmpty()) {
            replaceLines(id, req.lines());
        }
        startPending(principal, id, "RESUBMIT");
        return get(principal, id);
    }

    // ─────────────────────────────── 회수(기안자 취소)
    @Transactional
    public ApprovalDetailDto cancel(KintexPrincipal principal, String id) {
        Map<String, Object> a = load(id);
        assertTenant(principal, a);
        assertDrafter(principal, a);
        String st = status(a);
        if (!"DRAFT".equals(st) && !"SUBMITTED".equals(st)) {
            throw new ApiException(ErrorCode.CONFLICT, "임시저장/상신 상태만 회수할 수 있습니다.");
        }
        Map<String, Object> doc = new HashMap<>();
        doc.put("id", id);
        doc.put("status", "CANCELED");
        doc.put("completedAt", true);
        mapper.updateDoc(doc);
        addHistory(principal, id, intVal(a.get("curStep")), "CANCEL", null);
        return get(principal, id);
    }

    // ─────────────────────────────── 상태머신 헬퍼

    /** DRAFT/REJECTED→SUBMITTED 공통: 결재선 필수·전 단계 리셋·step1 CURRENT·cur_step=1·알림. */
    private void startPending(KintexPrincipal principal, String id, String action) {
        List<Map<String, Object>> lines = mapper.findLines(id);
        if (lines.isEmpty()) {
            throw new ApiException(ErrorCode.VALIDATION, "결재선을 1명 이상 지정해야 합니다.");
        }
        mapper.resetLines(id);
        Map<String, Object> first = lines.get(0);
        Map<String, Object> fl = new HashMap<>();
        fl.put("id", str(first.get("id")));
        fl.put("lineStatus", "CURRENT");
        mapper.updateLine(fl);

        Map<String, Object> doc = new HashMap<>();
        doc.put("id", id);
        doc.put("status", "SUBMITTED");
        doc.put("curStep", intVal(first.get("stepNo")));
        doc.put("submittedAt", true);
        mapper.updateDoc(doc);
        addHistory(principal, id, intVal(first.get("stepNo")), action, null);

        Map<String, Object> a = load(id);
        notify(str(first.get("approverId")), a, "[결재 요청] " + str(a.get("title")),
                "결재할 문서가 도착했습니다.", id);
    }

    private void complete(Map<String, Object> a, String id) {
        Map<String, Object> doc = new HashMap<>();
        doc.put("id", id);
        doc.put("status", "APPROVED");
        doc.put("completedAt", true);
        mapper.updateDoc(doc);
        notify(str(a.get("drafterId")), a, "[결재 완료] " + str(a.get("title")),
                "결재 문서가 최종 승인되었습니다.", id);
    }

    private void replaceLines(String approvalId, List<ApprovalLineSaveRequest> lineReqs) {
        mapper.deleteLines(approvalId);
        if (lineReqs == null || lineReqs.isEmpty()) {
            return;
        }
        List<ApprovalLineSaveRequest> sorted = lineReqs.stream()
                .sorted((x, y) -> Integer.compare(step(x), step(y)))
                .toList();
        int expected = 1;
        for (ApprovalLineSaveRequest d : sorted) {
            if (step(d) != expected) {
                throw new ApiException(ErrorCode.VALIDATION,
                        "결재선 단계는 1부터 연속이어야 합니다(기대: " + expected + ").");
            }
            if (d.approverId() == null || d.approverId().isBlank()) {
                throw new ApiException(ErrorCode.VALIDATION, "결재자 ID가 비어 있습니다.");
            }
            Map<String, Object> p = new HashMap<>();
            p.put("id", "al-" + UUID.randomUUID().toString().substring(0, 12));
            p.put("approvalId", approvalId);
            p.put("stepNo", expected);
            p.put("approverId", d.approverId().trim());
            p.put("approverName", d.approverName());
            mapper.insertLine(p);
            expected++;
        }
    }

    private void addHistory(KintexPrincipal principal, String approvalId, Integer stepNo,
                            String action, String comment) {
        Map<String, Object> p = new HashMap<>();
        p.put("id", "ah-" + UUID.randomUUID().toString().substring(0, 12));
        p.put("approvalId", approvalId);
        p.put("stepNo", stepNo);
        p.put("actorId", principal.userId());
        p.put("actorName", principal.displayName());
        p.put("action", action);
        p.put("comment", comment);
        mapper.insertHistory(p);
    }

    private void notify(String recipientId, Map<String, Object> a, String title, String message, String id) {
        if (recipientId == null || recipientId.isBlank()) {
            return;
        }
        try {
            notificationService.notify(recipientId, str(a.get("eventId")), "APPROVAL",
                    title, message, "/work/approvals/" + id);
        } catch (RuntimeException ignore) {
            // 알림 실패는 결재 흐름을 막지 않는다(실패 격리).
        }
    }

    private Map<String, Object> nextWaiting(List<Map<String, Object>> lines, int afterStep) {
        return lines.stream()
                .filter(l -> intVal(l.get("stepNo")) > afterStep && "WAIT".equals(str(l.get("lineStatus"))))
                .min((x, y) -> Integer.compare(intVal(x.get("stepNo")), intVal(y.get("stepNo"))))
                .orElse(null);
    }

    private Map<String, Object> requireCurrentApprover(KintexPrincipal principal, String id) {
        Map<String, Object> current = mapper.findLines(id).stream()
                .filter(l -> "CURRENT".equals(str(l.get("lineStatus"))))
                .findFirst()
                .orElseThrow(() -> new ApiException(ErrorCode.CONFLICT, "현재 진행 단계가 없습니다."));
        if (!principal.userId().equals(str(current.get("approverId")))) {
            throw new ApiException(ErrorCode.FORBIDDEN, "현재 단계 결재자만 처리할 수 있습니다.");
        }
        return current;
    }

    // ─────────────────────────────── 권한/가시
    private void assertDrafter(KintexPrincipal principal, Map<String, Object> a) {
        if (!principal.userId().equals(str(a.get("drafterId")))) {
            throw new ApiException(ErrorCode.FORBIDDEN, "기안자만 수행할 수 있습니다.");
        }
    }

    private void assertInFlight(Map<String, Object> a) {
        String st = status(a);
        if (!"SUBMITTED".equals(st) && !"IN_PROGRESS".equals(st)) {
            throw new ApiException(ErrorCode.CONFLICT, "진행중인 문서만 결재할 수 있습니다.");
        }
    }

    private void assertTenant(KintexPrincipal principal, Map<String, Object> a) {
        String t = str(a.get("tenantId"));
        if (t != null && !t.equals(principal.tenantId())) {
            throw new ApiException(ErrorCode.FORBIDDEN);
        }
    }

    /** 조회 가능: 기안자 본인 또는 결재선 참여자만(은닉). */
    private void assertReadable(KintexPrincipal principal, Map<String, Object> a, List<Map<String, Object>> lines) {
        String me = principal.userId();
        if (me.equals(str(a.get("drafterId")))) {
            return;
        }
        boolean inLine = lines.stream().anyMatch(l -> me.equals(str(l.get("approverId"))));
        if (!inLine) {
            throw new ApiException(ErrorCode.NOT_FOUND);
        }
    }

    private Map<String, Object> load(String id) {
        Map<String, Object> a = mapper.findById(id);
        if (a == null) {
            throw new ApiException(ErrorCode.NOT_FOUND);
        }
        return a;
    }

    private static int step(ApprovalLineSaveRequest d) {
        return d.stepNo() == null ? 0 : d.stepNo();
    }

    // ─────────────────────────────── 매핑
    private static ApprovalListDto toListDto(Map<String, Object> r) {
        return new ApprovalListDto(
                str(r.get("id")), str(r.get("docType")), str(r.get("title")), str(r.get("status")),
                str(r.get("drafterId")), str(r.get("drafterName")),
                intVal(r.get("curStep")), intVal(r.get("totalSteps")), intVal(r.get("version")),
                str(r.get("eventId")), str(r.get("refType")), str(r.get("refId")),
                str(r.get("submittedAt")), str(r.get("createdAt")));
    }

    private ApprovalDetailDto toDetail(KintexPrincipal principal, Map<String, Object> a,
                                       List<Map<String, Object>> lineRows, List<Map<String, Object>> histRows) {
        String me = principal.userId();
        String st = str(a.get("status"));
        List<ApprovalLineDto> lines = lineRows.stream().map(l -> new ApprovalLineDto(
                str(l.get("id")), intVal(l.get("stepNo")), str(l.get("approverId")), str(l.get("approverName")),
                str(l.get("lineStatus")), str(l.get("actedAt")), str(l.get("comment")))).toList();
        List<ApprovalHistoryDto> history = histRows.stream().map(h -> new ApprovalHistoryDto(
                str(h.get("id")), h.get("stepNo") == null ? null : intVal(h.get("stepNo")),
                str(h.get("actorId")), str(h.get("actorName")),
                str(h.get("action")), str(h.get("comment")), str(h.get("actedAt")))).toList();

        boolean inFlight = "SUBMITTED".equals(st) || "IN_PROGRESS".equals(st);
        boolean canAct = inFlight && lineRows.stream()
                .anyMatch(l -> "CURRENT".equals(str(l.get("lineStatus"))) && me.equals(str(l.get("approverId"))));
        boolean isDrafter = me.equals(str(a.get("drafterId")));
        boolean canCancel = isDrafter && ("DRAFT".equals(st) || "SUBMITTED".equals(st));
        boolean canResubmit = isDrafter && "REJECTED".equals(st);

        return new ApprovalDetailDto(
                str(a.get("id")), str(a.get("docType")), str(a.get("title")), str(a.get("content")), st,
                str(a.get("drafterId")), str(a.get("drafterName")),
                intVal(a.get("curStep")), lineRows.size(), intVal(a.get("version")),
                str(a.get("eventId")), str(a.get("refType")), str(a.get("refId")),
                str(a.get("submittedAt")), str(a.get("completedAt")), str(a.get("createdAt")),
                lines, history, canAct, canCancel, canResubmit);
    }

    private static String status(Map<String, Object> a) {
        return str(a.get("status"));
    }

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

    private static int intVal(Object o) {
        if (o instanceof Number n) {
            return n.intValue();
        }
        try {
            return o == null ? 0 : Integer.parseInt(String.valueOf(o));
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}
