package dev.awesomedev.mirim.service;

import dev.awesomedev.mirim.domain.CodingProblem;
import dev.awesomedev.mirim.domain.CodingSubmission;
import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.CodingProblemRepository;
import dev.awesomedev.mirim.repository.CodingSubmissionRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 이 파일이 하는 일:
 * 코딩 문제의 비즈니스 로직 — 문제 조회, 제출 저장, 진도 요약, 멘토 피드백.
 */
@Service
public class CodingService {

    private final CodingProblemRepository codingProblemRepository;
    private final CodingSubmissionRepository codingSubmissionRepository;
    private final TelegramNotifier telegramNotifier;

    public CodingService(CodingProblemRepository codingProblemRepository,
                         CodingSubmissionRepository codingSubmissionRepository,
                         TelegramNotifier telegramNotifier) {
        this.codingProblemRepository = codingProblemRepository;
        this.codingSubmissionRepository = codingSubmissionRepository;
        this.telegramNotifier = telegramNotifier;
    }

    /** 전체 문제 목록 (정렬 순서대로) */
    @Transactional(readOnly = true)
    public List<CodingProblem> allProblems() {
        return codingProblemRepository.findAllByOrderBySortOrderAsc();
    }

    /** 문제 한 건 — 없으면 404 */
    @Transactional(readOnly = true)
    public CodingProblem problem(Long id) {
        return codingProblemRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "문제를 찾을 수 없습니다."));
    }

    /** 이 학생의 제출물을 문제 id로 찾아보기 쉽게 Map으로 */
    @Transactional(readOnly = true)
    public Map<Long, CodingSubmission> mySubmissionsByProblemId(User user) {
        return codingSubmissionRepository.findByUser(user).stream()
                .collect(Collectors.toMap(s -> s.getProblem().getId(), Function.identity()));
    }

    /** 이 학생이 이 문제에 낸 제출물 (없을 수 있음) */
    @Transactional(readOnly = true)
    public Optional<CodingSubmission> mySubmission(User user, Long problemId) {
        return codingSubmissionRepository.findByProblemAndUser(problem(problemId), user);
    }

    /**
     * 코드를 제출한다. 이미 낸 적이 있으면 그 행을 갱신한다(새 행을 만들지 않는다).
     *
     * 학습 포인트: 채점 결과(passedCount/totalCount)를 브라우저가 계산해서 보내온다.
     * 즉 마음먹으면 "다 통과했다"고 거짓말할 수 있다 — 개발자도구로 요청을 조작하면 된다.
     * 그런데도 이렇게 만든 이유는, 이 값의 용도가 "성적"이 아니라 "즉시 피드백"이기 때문이다.
     * 진짜 판정은 멘토가 코드 원문을 읽고 내린다(그래서 code를 반드시 함께 저장한다).
     * — 대회 채점기라면 절대 이렇게 하면 안 된다. QuizQuestion을 보면 정답은 서버에만 두었다.
     *   "무엇을 지켜야 하는가"에 따라 설계가 달라진다는 걸 두 기능을 비교하며 보자.
     */
    @Transactional
    public CodingSubmission submit(User user, Long problemId, String code,
                                   Integer passedCount, Integer totalCount, String resultsJson) {
        if (code == null || code.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "코드가 비어 있습니다.");
        }
        CodingProblem problem = problem(problemId);

        // 자바 문제는 자동채점이 없다 — 브라우저가 뭘 보내오든 채점 값은 버린다. (JS·SQL만 저장)
        Integer passed = problem.isAutoGraded() ? passedCount : null;
        Integer total = problem.isAutoGraded() ? totalCount : null;
        String results = problem.isAutoGraded() ? resultsJson : null;

        CodingSubmission submission = codingSubmissionRepository.findByProblemAndUser(problem, user)
                .map(existing -> {
                    existing.submit(code, passed, total, results);
                    return existing;
                })
                .orElseGet(() -> codingSubmissionRepository.save(
                        new CodingSubmission(problem, user, code, passed, total, results)));

        // 멘토에게 텔레그램 알림 (실패해도 제출은 성공)
        String grading = problem.isAutoGraded()
                ? submission.getPassedCount() + "/" + submission.getTotalCount() + " 통과"
                : "멘토 리뷰 대기";
        telegramNotifier.send("💻 코딩 문제 제출\n"
                + user.getName() + " — [" + problem.getLanguage() + "] " + problem.getTitle()
                + "\n" + grading + " (" + submission.getAttempts() + "번째 시도)");

        return submission;
    }

    /** 멘토 피드백 등록 — 없으면 404 */
    @Transactional
    public CodingSubmission giveFeedback(Long submissionId, String feedback) {
        CodingSubmission submission = codingSubmissionRepository.findById(submissionId)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "제출물을 찾을 수 없습니다."));
        submission.review(feedback);
        return submission;
    }

    /** 전체 제출물 — 멘토 화면용 (최신순) */
    @Transactional(readOnly = true)
    public List<CodingSubmission> allSubmissions() {
        return codingSubmissionRepository.findAllByOrderBySubmittedAtDesc();
    }

    /** 학생 한 명의 진도 요약: 전체 문제 수, 제출 수, 통과 수 */
    public record ProgressSummary(int total, int submitted, int passed) {
    }

    /**
     * 이 학생의 진도 요약.
     * 학습 포인트: 문제 총수는 count() 한 번으로 얻고, 제출은 이 학생 것만 가져온다.
     * 학생이 4~5명이라 이 정도면 충분히 빠르다(MentorController의 N+1 주석과 같은 판단이다).
     */
    @Transactional(readOnly = true)
    public ProgressSummary progressOf(User user) {
        int total = (int) codingProblemRepository.count();
        List<CodingSubmission> mine = codingSubmissionRepository.findByUser(user);
        int passed = (int) mine.stream().filter(s -> "PASSED".equals(s.getStatus())).count();
        return new ProgressSummary(total, mine.size(), passed);
    }
}
