package dev.awesomedev.mirim.config;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.awesomedev.mirim.domain.Assignment;
import dev.awesomedev.mirim.domain.ChecklistItem;
import dev.awesomedev.mirim.domain.CodingProblem;
import dev.awesomedev.mirim.domain.Document;
import dev.awesomedev.mirim.domain.QuizQuestion;
import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.AssignmentRepository;
import dev.awesomedev.mirim.repository.ChecklistItemRepository;
import dev.awesomedev.mirim.repository.CodingProblemRepository;
import dev.awesomedev.mirim.repository.DocumentRepository;
import dev.awesomedev.mirim.repository.QuizQuestionRepository;
import dev.awesomedev.mirim.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * 이 파일이 하는 일:
 * 앱이 시작될 때 한 번 실행되어, 테이블이 비어 있으면 초기 데이터를 넣는다.
 *  - 사용자 5명 (수습생 4명 + 멘토 1명)
 *  - classpath의 seed/documents.json, seed/checklist.json, seed/assignments.json
 *
 * 학습 포인트: ApplicationRunner는 스프링 컨테이너가 완전히 뜬 뒤 run()을 호출해 주는
 * 인터페이스다. "서버 시작 직후 한 번 할 일"을 넣기에 알맞다.
 * count() == 0 조건 덕분에 서버를 여러 번 재시작해도 데이터가 중복으로 쌓이지 않는다.
 */
@Component
public class SeedDataLoader implements ApplicationRunner {

    private static final Logger log = LoggerFactory.getLogger(SeedDataLoader.class);

    private final UserRepository userRepository;
    private final DocumentRepository documentRepository;
    private final ChecklistItemRepository checklistItemRepository;
    private final AssignmentRepository assignmentRepository;
    private final QuizQuestionRepository quizQuestionRepository;
    private final CodingProblemRepository codingProblemRepository;
    private final PasswordEncoder passwordEncoder;
    private final ObjectMapper objectMapper;

    public SeedDataLoader(UserRepository userRepository,
                          DocumentRepository documentRepository,
                          ChecklistItemRepository checklistItemRepository,
                          AssignmentRepository assignmentRepository,
                          QuizQuestionRepository quizQuestionRepository,
                          CodingProblemRepository codingProblemRepository,
                          PasswordEncoder passwordEncoder,
                          ObjectMapper objectMapper) {
        this.userRepository = userRepository;
        this.documentRepository = documentRepository;
        this.checklistItemRepository = checklistItemRepository;
        this.assignmentRepository = assignmentRepository;
        this.quizQuestionRepository = quizQuestionRepository;
        this.codingProblemRepository = codingProblemRepository;
        this.passwordEncoder = passwordEncoder;
        this.objectMapper = objectMapper;
    }

    @Override
    public void run(ApplicationArguments args) {
        seedUsers();
        seedDocuments();
        seedChecklist();
        seedAssignments();
        seedQuizzes();
        seedCodingProblems();
    }

    /**
     * 초기 사용자: 멘토 계정 하나만 만든다. 학생은 각자 회원가입한다.
     *
     * 학습 포인트: 왜 학생 계정을 시드로 안 만드나?
     *  - 학생은 첫날 직접 가입하며 "받은 비밀번호를 바꾸는 것이 첫 일"을 몸으로 배운다.
     *  - 개인정보(이름·이메일·연락처)를 본인이 입력하는 것이 정확하다(대신 넣으면 오타·불일치).
     * 멘토만 시드로 두는 이유: 학생을 관리할 사람이 처음부터 있어야 하기 때문이다.
     *
     * ⚠️ MENTOR_INITIAL_PASSWORD 환경변수로 초기 비밀번호를 넣을 수 있다(운영 권장).
     *    없으면 개발용 기본값을 쓰되, 이 값은 소스에 보이므로 첫 로그인 후 반드시 바꿔야 한다.
     */
    private void seedUsers() {
        if (userRepository.count() > 0) {
            return; // 이미 사용자가 있으면 건너뛴다
        }
        String initialPassword = System.getenv().getOrDefault("MENTOR_INITIAL_PASSWORD", "mirim2026!");
        String passwordHash = passwordEncoder.encode(initialPassword);

        userRepository.save(new User("mentor1", passwordHash, "멘토", "MENTOR", "NONE"));

        log.info("멘토 계정(mentor1)을 생성했습니다. 학생은 회원가입으로 등록합니다.");
    }

    private void seedDocuments() {
        if (documentRepository.count() > 0) {
            return;
        }
        List<DocumentSeed> seeds = readSeedFile("seed/documents.json", new TypeReference<>() {});
        if (seeds == null) {
            return;
        }
        for (DocumentSeed seed : seeds) {
            documentRepository.save(new Document(seed.slug(), seed.title(), seed.category(),
                    seed.week(), seed.audience(), seed.filePath(), seed.sortOrder()));
        }
        log.info("문서 시드 {}건을 저장했습니다.", seeds.size());
    }

    private void seedChecklist() {
        if (checklistItemRepository.count() > 0) {
            return;
        }
        List<ChecklistSeed> seeds = readSeedFile("seed/checklist.json", new TypeReference<>() {});
        if (seeds == null) {
            return;
        }
        for (ChecklistSeed seed : seeds) {
            checklistItemRepository.save(new ChecklistItem(seed.week(), seed.day(),
                    seed.label(), seed.sortOrder()));
        }
        log.info("체크리스트 시드 {}건을 저장했습니다.", seeds.size());
    }

    private void seedAssignments() {
        if (assignmentRepository.count() > 0) {
            return;
        }
        List<AssignmentSeed> seeds = readSeedFile("seed/assignments.json", new TypeReference<>() {});
        if (seeds == null) {
            return;
        }
        for (AssignmentSeed seed : seeds) {
            assignmentRepository.save(new Assignment(seed.week(), seed.day(), seed.kind(),
                    seed.title(), seed.summary()));
        }
        log.info("과제 시드 {}건을 저장했습니다.", seeds.size());
    }

    private void seedQuizzes() {
        // 학습 포인트: 각 테이블이 독립적으로 count()를 검사하므로,
        // 이미 운영 중인 DB에 새 기능(퀴즈)의 시드만 추가로 들어갈 수 있다.
        if (quizQuestionRepository.count() > 0) {
            return;
        }
        List<QuizSeed> seeds = readSeedFile("seed/quizzes.json", new TypeReference<>() {});
        if (seeds == null) {
            return;
        }
        int sortOrder = 0;
        for (QuizSeed seed : seeds) {
            quizQuestionRepository.save(new QuizQuestion(seed.courseSlug(), seed.question(),
                    seed.option1(), seed.option2(), seed.option3(), seed.option4(),
                    seed.answerIndex(), seed.explanation(), sortOrder++));
        }
        log.info("퀴즈 시드 {}건을 저장했습니다.", seeds.size());
    }

    /**
     * 코딩 문제 시드.
     * 학습 포인트: 테스트케이스는 JSON 배열 그대로 문자열로 넣는다(CodingProblem.testsJson).
     * 시드 파일에서는 사람이 읽기 좋은 객체로 두고, 저장할 때 다시 문자열로 직렬화한다 —
     * "파일에서 편한 모양"과 "DB에 담는 모양"이 달라도 되며, 그 변환이 여기서 일어난다.
     */
    private void seedCodingProblems() {
        if (codingProblemRepository.count() > 0) {
            return;
        }
        List<CodingProblemSeed> seeds = readSeedFile("seed/coding-problems.json", new TypeReference<>() {});
        if (seeds == null) {
            return;
        }
        int sortOrder = 0;
        for (CodingProblemSeed seed : seeds) {
            // 채점 데이터는 언어마다 모양이 다르다 — JS는 테스트케이스 배열, SQL은 기대 결과 객체.
            // 어느 쪽이든 DB에는 JSON 문자열 하나(testsJson)로 들어간다.
            String testsJson = null;
            try {
                if (seed.sqlExpected() != null) {
                    testsJson = objectMapper.writeValueAsString(seed.sqlExpected());
                } else if (seed.tests() != null) {
                    testsJson = objectMapper.writeValueAsString(seed.tests());
                }
            } catch (Exception e) {
                log.error("채점 데이터 직렬화 실패 — 건너뜁니다: {}", seed.title(), e);
                continue;
            }
            codingProblemRepository.save(new CodingProblem(
                    seed.language(), seed.title(), seed.difficulty(), seed.topic(),
                    seed.description(), seed.starterCode(), seed.functionName(),
                    seed.setupSql(), testsJson, sortOrder++));
        }
        log.info("코딩 문제 시드 {}건을 저장했습니다.", seeds.size());
    }

    /**
     * classpath에서 시드 JSON 파일을 읽는다.
     * 학습 포인트: 파일이 없어도 앱이 죽지 않고 경고 로그만 남기고 넘어간다.
     * 시드 파일을 만드는 작업과 이 코드를 만드는 작업이 서로를 기다리지 않아도 되게 하기 위해서다.
     */
    private <T> List<T> readSeedFile(String classpathLocation, TypeReference<List<T>> type) {
        ClassPathResource resource = new ClassPathResource(classpathLocation);
        if (!resource.exists()) {
            log.warn("시드 파일이 없어 건너뜁니다: {}", classpathLocation);
            return null;
        }
        try (InputStream inputStream = resource.getInputStream()) {
            return objectMapper.readValue(inputStream, type);
        } catch (Exception e) {
            log.error("시드 파일을 읽는 중 오류가 발생해 건너뜁니다: {}", classpathLocation, e);
            return null;
        }
    }

    // --- 시드 JSON의 한 줄을 담는 record들 (JSON 필드명과 record 컴포넌트명이 그대로 대응된다) ---

    /** documents.json 한 건 */
    record DocumentSeed(String slug, String title, String category, Integer week,
                        String audience, String filePath, Integer sortOrder) {
    }

    /** checklist.json 한 건 */
    record ChecklistSeed(Integer week, Integer day, String label, Integer sortOrder) {
    }

    /** assignments.json 한 건 */
    record AssignmentSeed(Integer week, Integer day, String kind, String title, String summary) {
    }

    /** quizzes.json 한 건 */
    record QuizSeed(String courseSlug, String question,
                    String option1, String option2, String option3, String option4,
                    Integer answerIndex, String explanation) {
    }

    /**
     * coding-problems.json 한 건.
     * tests는 JS 전용, setupSql·sqlExpected는 SQL 전용, JAVA는 셋 다 null.
     */
    record CodingProblemSeed(String language, String title, String difficulty, String topic,
                             String description, String starterCode, String functionName,
                             List<TestCaseSeed> tests,
                             String setupSql, Map<String, Object> sqlExpected) {
    }

    /** 테스트케이스 한 건 — args를 함수에 넣었을 때 expected가 나와야 한다 */
    record TestCaseSeed(String name, List<Object> args, Object expected) {
    }
}
