package dev.awesomedev.mirim.service;

import dev.awesomedev.mirim.domain.StudentProfile;
import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.StudentProfileRepository;
import dev.awesomedev.mirim.repository.UserRepository;
import dev.awesomedev.mirim.web.dto.SignupRequest;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

import java.util.ArrayList;
import java.util.List;

/**
 * 이 파일이 하는 일:
 * 로그인 검증(아이디+비밀번호 확인), 회원가입, 아이디 찾기, 비밀번호 재설정,
 * 그리고 "현재 로그인한 사용자 찾기"를 담당하는 서비스다.
 */
@Service
public class AuthService {

    private final UserRepository userRepository;
    private final StudentProfileRepository studentProfileRepository;
    private final PasswordEncoder passwordEncoder;
    private final TelegramNotifier telegramNotifier;

    public AuthService(UserRepository userRepository,
                       StudentProfileRepository studentProfileRepository,
                       PasswordEncoder passwordEncoder,
                       TelegramNotifier telegramNotifier) {
        this.userRepository = userRepository;
        this.studentProfileRepository = studentProfileRepository;
        this.passwordEncoder = passwordEncoder;
        this.telegramNotifier = telegramNotifier;
    }

    /**
     * 아이디와 비밀번호를 검증한다. 틀리면 401을 던진다.
     *
     * 학습 포인트: "아이디가 없음"과 "비밀번호가 틀림"을 구분해서 알려주지 않는다.
     * 구분해서 알려주면 공격자가 어떤 아이디가 존재하는지 알아낼 수 있기 때문이다.
     */
    public User authenticate(String username, String rawPassword) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 올바르지 않습니다."));

        // 학습 포인트: BCrypt 해시는 복호화가 불가능하다.
        // 원문을 다시 해시해서 비교하는 게 아니라 matches()가 해시 내부의 salt를 이용해 검증한다.
        if (!passwordEncoder.matches(rawPassword, user.getPasswordHash())) {
            throw new ResponseStatusException(
                    HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 올바르지 않습니다.");
        }
        return user;
    }

    /**
     * 회원가입: 새 수습생 계정을 만든다. 아이디가 이미 사용 중이면 409 Conflict를 던진다.
     *
     * role은 항상 "STUDENT"로 고정한다.
     * 이유: 역할을 클라이언트 입력으로 받으면, 프론트 화면을 우회해서 API를 직접 호출하는
     * 것만으로 누구나 멘토 권한(전체 제출물 열람, 피드백 작성)을 가질 수 있게 된다.
     * 권한은 "요청자가 달라고 하는 값"이 아니라 "서버가 부여하는 값"이어야 한다.
     * 멘토 계정이 필요하면 관리자가 별도 절차(현재는 시드 데이터)로 만든다.
     */
    @Transactional
    public User signup(SignupRequest req) {
        // 개인정보 동의는 DTO의 @AssertTrue가 먼저 거르지만, 서비스에서도 한 번 더 확인한다.
        // 학습 포인트: 법적 요건처럼 "절대 뚫리면 안 되는 규칙"은 겹으로 지킨다(defense in depth).
        // 나중에 누가 다른 경로(배치, 관리자 API 등)로 이 메서드를 불러도 규칙이 지켜진다.
        if (!Boolean.TRUE.equals(req.privacyConsent())) {
            throw new ResponseStatusException(
                    HttpStatus.BAD_REQUEST, "개인정보 수집·이용에 동의해야 가입할 수 있습니다.");
        }

        // 학습 포인트: 중복 검사와 저장 사이에 다른 요청이 끼어들 수도 있지만(경쟁 상태),
        // unique 제약이 걸려 있어 최악의 경우에도 DB가 중복을 막아 준다.
        // 이 사전 검사는 사용자에게 친절한 에러 메시지를 주기 위한 것이다.
        if (userRepository.existsByUsername(req.username())) {
            throw new ResponseStatusException(
                    HttpStatus.CONFLICT, "이미 사용 중인 아이디입니다.");
        }
        // 회사 이메일 아이디도 중복될 수 없다 — hong@awesomedev.dev를 두 명이 쓸 수 없으니까.
        if (studentProfileRepository.existsByCompanyEmailLocal(req.companyEmailLocal())) {
            throw new ResponseStatusException(
                    HttpStatus.CONFLICT, "이미 사용 중인 회사 이메일 아이디입니다.");
        }

        // 비밀번호는 반드시 BCrypt로 해시해서 저장한다. 원문은 DB 어디에도 남지 않는다.
        String passwordHash = passwordEncoder.encode(req.password());

        // 학습 포인트: 한 트랜잭션 안에서 users와 student_profile을 함께 저장한다.
        // 둘 중 하나만 저장되고 끊기면 "계정은 있는데 개인정보가 없는" 반쪽 상태가 된다.
        // @Transactional이 "둘 다 되거나, 둘 다 안 되거나"를 보장한다(원자성).
        User user = userRepository.save(
                new User(req.username(), passwordHash, req.name(), "STUDENT", req.track()));

        // 동의 "시각"은 클라이언트가 아니라 서버 시계로 기록한다 — 증빙은 조작 불가능해야 하니까.
        studentProfileRepository.save(new StudentProfile(
                user, req.companyEmailLocal(), req.birthDate(), req.phone(), req.address(),
                req.school(), req.major(),
                req.emergencyName(), req.emergencyRelation(), req.emergencyPhone(),
                java.time.Instant.now()));

        // 멘토에게 텔레그램 알림. 실패해도 가입은 성공한다(TelegramNotifier가 예외를 삼킨다).
        telegramNotifier.send("🎓 새 수습생 가입\n"
                + user.getName() + " (" + ("DESIGN".equals(user.getTrack()) ? "디자인" : "개발") + " 트랙)\n"
                + req.companyEmailLocal() + "@" + StudentProfile.COMPANY_DOMAIN);

        return user;
    }

    /**
     * 아이디 찾기: 이름이 일치하는 계정들의 아이디를 "가려서(마스킹)" 돌려준다.
     *
     * ⚠️ 보안 리뷰에서 실제로 잡힌 문제 — 이 코드가 살아있는 교훈이다.
     * 원래 이 메서드는 아이디 전문(mentor1)을 그대로 돌려줬다. 그 자체로도 정보 노출이지만,
     * 진짜 문제는 옆에 있던 "아이디+이름으로 비밀번호 재설정" API와 결합됐을 때였다:
     *   1) find-id {"name":"멘토"} → "mentor1"  (아이디를 서버가 알려줌)
     *   2) reset-password {"username":"mentor1","name":"멘토",...} → 비밀번호 교체
     *   3) 로그인 → 멘토 권한 획득
     * 각각은 "학습용 단순화"였지만 둘을 이으니 인증이 통째로 사라졌다.
     *
     * 학습 포인트: 취약점은 기능 하나가 아니라 기능의 "조합"에서 태어나는 일이 많다.
     * 그래서 보안 검토는 파일 하나가 아니라 공격자의 경로 전체를 따라가야 한다.
     *
     * 지금은 (1) 아이디를 마스킹하고 (2) 무인증 재설정 API를 아예 없애서 이 연쇄를 두 곳에서 끊었다.
     * 방어를 한 겹만 두지 않는 것을 심층 방어(defense in depth)라고 한다.
     */
    @Transactional(readOnly = true)
    public List<String> findUsernamesByName(String name) {
        List<User> users = userRepository.findByName(name);

        // 스트림 체이닝 대신 for문으로 풀어쓴다 — 무슨 일이 일어나는지 한 줄씩 보이도록.
        List<String> usernames = new ArrayList<>();
        for (User user : users) {
            usernames.add(maskUsername(user.getUsername()));
        }
        return usernames;
    }

    /**
     * 아이디를 가린다: "mentor1" → "me****1" (앞 2글자 + 마지막 1글자만 남김).
     * 본인은 "아, 내 아이디가 그거였지" 하고 떠올릴 수 있지만, 남이 그대로 쓸 수는 없다.
     *
     * 학습 포인트: 보안과 편의는 늘 저울질이다. 아이디를 아예 안 알려주면 가장 안전하지만
     * 정말 잊어버린 사람이 곤란해진다. "떠올리기엔 충분하고 악용하기엔 부족한" 지점을 고른 것이다.
     */
    private String maskUsername(String username) {
        if (username.length() <= 3) {
            // 너무 짧으면 첫 글자만 남긴다 (가릴 게 없으면 더 많이 가린다)
            return username.charAt(0) + "**";
        }
        String head = username.substring(0, 2);
        String tail = username.substring(username.length() - 1);
        String stars = "*".repeat(username.length() - 3);
        return head + stars + tail;
    }

    /**
     * 비밀번호 변경 (로그인한 본인). 현재 비밀번호를 확인한 뒤에만 바꿔 준다.
     *
     * 학습 포인트: 왜 "현재 비밀번호"를 또 묻나? 이미 로그인했는데?
     * 로그인한 브라우저를 잠깐 빌린 사람(자리를 비운 사이)이 비밀번호를 바꿔
     * 계정을 통째로 뺏는 것을 막기 위해서다. 이걸 재인증(re-authentication)이라 하고,
     * 비밀번호 변경·결제·탈퇴처럼 되돌리기 어려운 동작 앞에 한 번 더 세우는 관문이다.
     */
    @Transactional
    public void changePassword(User loginUser, String currentRawPassword, String newRawPassword) {
        // ⚠️ 실제로 났던 버그 — 여기서 배우는 "managed vs detached 엔티티".
        //
        // 원래 이 코드는 넘어온 loginUser의 필드를 바로 바꿨다:
        //     loginUser.changePassword(...)  // ← 이러면 "성공"인데 DB가 안 바뀐다!
        //
        // 왜? loginUser는 requireUser()가 트랜잭션 밖에서 조회한 "분리된(detached)" 객체다.
        // JPA의 변경 감지(더티 체킹)는 "지금 이 트랜잭션이 관리하는(managed)" 엔티티에만 작동한다.
        // 분리된 객체의 필드를 바꿔봐야 JPA는 알아채지 못해 UPDATE가 나가지 않는다.
        //
        // 해결: 이 트랜잭션 안에서 다시 조회한다. 그러면 managed 상태가 되어 더티 체킹이 작동한다.
        // (멘토 재설정 resetPasswordByMentor는 findById를 트랜잭션 안에서 불러서 처음부터 정상이었다 —
        //  그 차이가 이 버그의 힌트였다.)
        //
        // 교훈: "더티 체킹이 자동으로 저장해 준다"에는 숨은 전제가 있다 — "지금 트랜잭션이 관리하는 엔티티라면".
        User user = userRepository.findById(loginUser.getId())
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."));

        if (!passwordEncoder.matches(currentRawPassword, user.getPasswordHash())) {
            throw new ResponseStatusException(
                    HttpStatus.UNAUTHORIZED, "현재 비밀번호가 올바르지 않습니다.");
        }
        user.changePassword(passwordEncoder.encode(newRawPassword));
        // 이제 user는 managed 상태라, 커밋 시점에 JPA가 변경을 감지해 UPDATE 한다.
    }

    /**
     * 비밀번호 재설정 (멘토가 학생 계정에 대해 수행). 컨트롤러가 MENTOR 권한을 이미 확인한다.
     *
     * 학습 포인트: 진짜로 비밀번호를 잊은 사람은 어떻게 하나?
     * 실서비스는 "등록된 이메일로 만료되는 재설정 링크"를 보낸다 — 메일함에 접근할 수 있는
     * 사람이 본인이라는 강한 증거이기 때문이다. 우리는 메일 시스템이 없으므로,
     * "옆자리 멘토에게 직접 요청한다"는 오프라인 본인 확인으로 대신한다.
     * 사람이 눈으로 확인하는 것도 훌륭한 인증이다 — 5명이 한 사무실에 있다면 더더욱.
     */
    @Transactional
    public void resetPasswordByMentor(Long userId, String newRawPassword) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."));

        // 멘토가 다른 멘토(또는 자기 자신)의 비밀번호를 바꾸는 것은 막는다 — 학생 계정만 대상.
        if (!"STUDENT".equals(user.getRole())) {
            throw new ResponseStatusException(
                    HttpStatus.FORBIDDEN, "학생 계정만 재설정할 수 있습니다.");
        }

        user.changePassword(passwordEncoder.encode(newRawPassword));
    }

    /**
     * 현재 요청의 인증 정보(Authentication)로부터 User 엔티티를 찾아온다.
     * 인증 정보가 없거나 사용자를 찾을 수 없으면 401을 던진다.
     * 컨트롤러들이 "지금 누가 요청했는지" 알아낼 때 공통으로 사용한다.
     */
    public User requireUser(Authentication authentication) {
        if (authentication == null || authentication.getName() == null) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
        }
        return userRepository.findByUsername(authentication.getName())
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."));
    }
}
