package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.data.UserAuthIdentityData;
import com.pandoli365.bibimbap.data.UserData;
import com.pandoli365.bibimbap.mapper.UserAuthIdentitiesMapper;
import com.pandoli365.bibimbap.mapper.UserPermissionsMapper;
import com.pandoli365.bibimbap.mapper.UsersMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

@Controller
public class UserController {

    private static final String PROVIDER_EMAIL = "email";
    private static final String ROLE_USER = "USER";
    private static final String STATUS_ACTIVE = "ACTIVE";
    private static final int PASSWORD_MIN_LENGTH = 8;
    private static final int PASSWORD_ITERATIONS = 210_000;
    private static final int PASSWORD_KEY_LENGTH = 256;
    private static final int PASSWORD_SALT_BYTES = 16;
    private static final int DEFAULT_SESSION_SECONDS = 60 * 30;
    private static final int REMEMBER_SESSION_SECONDS = 60 * 60 * 24 * 30;
    private static final long PROFILE_AVATAR_MAX_BYTES = 5L * 1024 * 1024;

    private final UsersMapper usersMapper;
    private final UserAuthIdentitiesMapper userAuthIdentitiesMapper;
    private final UserPermissionsMapper userPermissionsMapper;
    private final SecureRandom secureRandom = new SecureRandom();

    @Value("${app.upload.game-storage-path:src/main/resources/static}")
    private String uploadStoragePath;

    public UserController(UsersMapper usersMapper, UserAuthIdentitiesMapper userAuthIdentitiesMapper,
                          UserPermissionsMapper userPermissionsMapper) {
        this.usersMapper = usersMapper;
        this.userAuthIdentitiesMapper = userAuthIdentitiesMapper;
        this.userPermissionsMapper = userPermissionsMapper;
    }

    @PostMapping("/signup")
    @Transactional
    public ResponseEntity<?> signup(
            @RequestParam(name = "displayName") String displayName,
            @RequestParam(name = "email") String email,
            @RequestParam(name = "password") String password,
            @RequestParam(name = "passwordConfirm") String passwordConfirm,
            @RequestParam(name = "termsAccepted", required = false) String termsAccepted,
            HttpServletRequest request
    ) {
        boolean json = wantsJson(request);
        if (!CsrfTokens.isValid(request)) {
            return authError(json, request, "/signup", "csrf", HttpStatus.FORBIDDEN, "요청 보안 토큰이 유효하지 않습니다.");
        }

        String normalizedEmail = normalizeEmail(email);
        String normalizedDisplayName = normalizeDisplayName(displayName, null);

        if (normalizedEmail == null
                || normalizedDisplayName == null
                || !isUsablePassword(password)
                || !password.equals(passwordConfirm)
                || termsAccepted == null) {
            return signupError(json, request, "invalid", HttpStatus.BAD_REQUEST, "입력값을 다시 확인해 주세요.");
        }

        UserAuthIdentityData existingIdentity = userAuthIdentitiesMapper.getUserAuthIdentityByProvider(
                PROVIDER_EMAIL,
                normalizedEmail
        );
        if (existingIdentity != null) {
            return signupError(json, request, "duplicate", HttpStatus.CONFLICT, "이미 가입된 이메일입니다.");
        }

        try {
            OffsetDateTime now = now();
            UserData user = createUser(normalizedDisplayName, normalizedEmail, null, now);
            createIdentity(
                    user.getId(),
                    PROVIDER_EMAIL,
                    normalizedEmail,
                    normalizedEmail,
                    hashPassword(password),
                    normalizedDisplayName,
                    null,
                    now
            );

            if (json) {
                return ResponseEntity.ok(new AuthResult(200, "계정 생성이 완료되었습니다."));
            }
            return redirect(request, "/signup?created=1");
        } catch (DataIntegrityViolationException e) {
            return signupError(json, request, "duplicate", HttpStatus.CONFLICT, "이미 가입된 이메일입니다.");
        }
    }

    @PostMapping("/login")
    @Transactional
    public ResponseEntity<?> login(
            @RequestParam(name = "email", required = false) String email,
            @RequestParam(name = "password", required = false) String password,
            @RequestParam(name = "remember", required = false) String remember,
            HttpServletRequest request
    ) {
        boolean json = wantsJson(request);
        if (!CsrfTokens.isValid(request)) {
            return authError(json, request, "/login", "csrf", HttpStatus.FORBIDDEN, "요청 보안 토큰이 유효하지 않습니다.");
        }

        String normalizedEmail = normalizeEmail(email);
        if (normalizedEmail == null || password == null || password.isBlank()) {
            return authError(json, request, "/login", "invalid", HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호를 확인해 주세요.");
        }

        UserAuthIdentityData identity = userAuthIdentitiesMapper.getUserAuthIdentityByProvider(
                PROVIDER_EMAIL,
                normalizedEmail
        );
        if (identity == null || identity.getPasswordHash() == null || !verifyPassword(password, identity.getPasswordHash())) {
            return authError(json, request, "/login", "invalid", HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호를 확인해 주세요.");
        }

        UserData user = usersMapper.getUser(identity.getUserId());
        if (user == null || !STATUS_ACTIVE.equals(user.getStatus())) {
            return authError(json, request, "/login", "inactive", HttpStatus.FORBIDDEN, "사용할 수 없는 계정입니다.");
        }

        OffsetDateTime loginAt = now();
        user.setLastLoginAt(loginAt);
        identity.setLastLoginAt(loginAt);
        usersMapper.updateUser(user);
        userAuthIdentitiesMapper.updateUserAuthIdentity(identity);

        HttpSession session = request.getSession();
        request.changeSessionId();
        applyRememberOption(session, remember);
        saveLoginSession(session, user, identity);

        if (json) {
            return ResponseEntity.ok(new AuthResult(200, "로그인되었습니다."));
        }
        return redirect(request, "/");
    }

    @PostMapping("/logout")
    public String logout(HttpServletRequest request, HttpSession session) {
        if (!CsrfTokens.isValid(request)) {
            return "redirect:/";
        }
        if (session != null) {
            session.invalidate();
        }
        return "redirect:/";
    }

    @PostMapping("/profile/nickname")
    @Transactional
    public ResponseEntity<?> updateProfileNickname(
            @RequestParam(name = "displayName", required = false) String displayName,
            HttpServletRequest request
    ) {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new AuthResult(403, "요청 보안 토큰이 유효하지 않습니다."));
        }
        HttpSession session = request.getSession(false);
        Long userId = sessionUserId(session);
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new AuthResult(HttpStatus.UNAUTHORIZED.value(), "로그인이 필요합니다."));
        }

        String normalizedDisplayName = normalizeDisplayName(displayName, null);
        if (normalizedDisplayName == null) {
            return ResponseEntity.badRequest()
                    .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "변경할 닉네임을 입력해 주세요."));
        }

        UserData user = usersMapper.getUser(userId);
        if (user == null || !STATUS_ACTIVE.equals(user.getStatus())) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(new AuthResult(HttpStatus.FORBIDDEN.value(), "사용할 수 없는 계정입니다."));
        }

        user.setDisplayName(normalizedDisplayName);
        usersMapper.updateUser(user);
        updateAuthIdentityDisplayName(session, normalizedDisplayName);
        updateDisplayNameSession(session, normalizedDisplayName);

        return ResponseEntity.ok(new ProfileNicknameResult(200, "닉네임이 변경되었습니다.", normalizedDisplayName));
    }

    @PostMapping("/profile/avatar")
    @Transactional
    public ResponseEntity<?> updateProfileAvatar(
            @RequestParam(name = "avatar", required = false) MultipartFile avatar,
            HttpServletRequest request
    ) {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new AuthResult(403, "요청 보안 토큰이 유효하지 않습니다."));
        }
        HttpSession session = request.getSession(false);
        Long userId = sessionUserId(session);
        if (userId == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new AuthResult(HttpStatus.UNAUTHORIZED.value(), "로그인이 필요합니다."));
        }
        if (avatar == null || avatar.isEmpty()) {
            return ResponseEntity.badRequest()
                    .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "프로필 이미지를 선택해 주세요."));
        }
        if (avatar.getSize() > PROFILE_AVATAR_MAX_BYTES) {
            return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE)
                    .body(new AuthResult(HttpStatus.PAYLOAD_TOO_LARGE.value(), "프로필 이미지는 5MB 이하로 올려 주세요."));
        }

        String extension = profileImageExtension(avatar);
        if (extension == null) {
            return ResponseEntity.badRequest()
                    .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "PNG, JPG, WEBP, GIF 이미지만 사용할 수 있습니다."));
        }

        UserData user = usersMapper.getUser(userId);
        if (user == null || !STATUS_ACTIVE.equals(user.getStatus())) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN)
                    .body(new AuthResult(HttpStatus.FORBIDDEN.value(), "사용할 수 없는 계정입니다."));
        }

        try {
            Path storageRoot = Paths.get(uploadStoragePath).toAbsolutePath().normalize();
            Path profileRoot = storageRoot.resolve("profile").normalize();
            if (!profileRoot.startsWith(storageRoot)) {
                return ResponseEntity.badRequest()
                        .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "저장 경로가 올바르지 않습니다."));
            }

            Files.createDirectories(profileRoot);
            Path targetFile = profileRoot.resolve(userId + extension).normalize();
            Path tempFile = profileRoot.resolve(userId + ".upload" + extension).normalize();
            if (!targetFile.startsWith(profileRoot)) {
                return ResponseEntity.badRequest()
                        .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "저장 경로가 올바르지 않습니다."));
            }
            if (!tempFile.startsWith(profileRoot)) {
                return ResponseEntity.badRequest()
                        .body(new AuthResult(HttpStatus.BAD_REQUEST.value(), "저장 경로가 올바르지 않습니다."));
            }

            Files.copy(avatar.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
            deleteExistingProfileAvatars(profileRoot, userId);
            Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
            String avatarUrl = "/profile/" + userId + extension;

            user.setAvatarUrl(avatarUrl);
            usersMapper.updateUser(user);
            updateAuthIdentityAvatar(session, avatarUrl);
            updateAvatarSession(session, avatarUrl);

            return ResponseEntity.ok(new ProfileAvatarResult(200, "프로필 이미지가 저장되었습니다.", avatarUrl));
        } catch (IOException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(new AuthResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), "프로필 이미지를 저장하지 못했습니다."));
        }
    }

    private UserData createUser(String displayName, String canonicalEmail, String avatarUrl, OffsetDateTime loginAt) {
        UserData user = new UserData();
        user.setDisplayName(displayName);
        user.setCanonicalEmail(canonicalEmail);
        user.setAvatarUrl(avatarUrl);
        user.setRole(ROLE_USER);
        user.setStatus(STATUS_ACTIVE);
        user.setLastLoginAt(loginAt);
        usersMapper.addUser(user);
        return user;
    }

    private UserAuthIdentityData createIdentity(
            Long userId,
            String provider,
            String providerUserId,
            String email,
            String passwordHash,
            String displayName,
            String avatarUrl,
            OffsetDateTime loginAt
    ) {
        UserAuthIdentityData identity = new UserAuthIdentityData();
        identity.setUserId(userId);
        identity.setProvider(provider);
        identity.setProviderUserId(providerUserId);
        identity.setEmail(email);
        identity.setPasswordHash(passwordHash);
        identity.setDisplayName(displayName);
        identity.setAvatarUrl(avatarUrl);
        identity.setLastLoginAt(loginAt);
        userAuthIdentitiesMapper.addUserAuthIdentity(identity);
        return identity;
    }

    private void applyRememberOption(HttpSession session, String remember) {
        session.setMaxInactiveInterval(isChecked(remember) ? REMEMBER_SESSION_SECONDS : DEFAULT_SESSION_SECONDS);
    }

    private boolean isChecked(String value) {
        return "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
    }

    private Long sessionUserId(HttpSession session) {
        if (session == null) {
            return null;
        }
        Object userId = session.getAttribute("userId");
        if (userId instanceof Number number) {
            return number.longValue();
        }
        if (userId instanceof String text) {
            try {
                return Long.parseLong(text);
            } catch (NumberFormatException e) {
                return null;
            }
        }
        return null;
    }

    private String profileImageExtension(MultipartFile file) {
        String contentType = file.getContentType();
        if (contentType != null) {
            switch (contentType.toLowerCase(Locale.ROOT)) {
                case "image/png":
                    return ".png";
                case "image/jpeg":
                case "image/jpg":
                    return ".jpg";
                case "image/webp":
                    return ".webp";
                case "image/gif":
                    return ".gif";
                default:
                    break;
            }
        }

        String originalName = file.getOriginalFilename();
        if (originalName == null) {
            return null;
        }
        String cleanName = Paths.get(originalName).getFileName().toString().toLowerCase(Locale.ROOT);
        if (cleanName.endsWith(".png")) {
            return ".png";
        }
        if (cleanName.endsWith(".jpg") || cleanName.endsWith(".jpeg")) {
            return ".jpg";
        }
        if (cleanName.endsWith(".webp")) {
            return ".webp";
        }
        if (cleanName.endsWith(".gif")) {
            return ".gif";
        }
        return null;
    }

    private void deleteExistingProfileAvatars(Path profileRoot, Long userId) throws IOException {
        if (!Files.exists(profileRoot)) {
            return;
        }

        String baseName = userId + ".";
        try (Stream<Path> paths = Files.list(profileRoot)) {
            for (Path path : paths.toList()) {
                String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT);
                if (Files.isRegularFile(path) && isProfileAvatarFile(fileName, baseName)) {
                    Files.deleteIfExists(path);
                }
            }
        }

        Path legacyProfileDir = profileRoot.resolve(String.valueOf(userId)).normalize();
        if (!legacyProfileDir.startsWith(profileRoot) || !Files.isDirectory(legacyProfileDir)) {
            return;
        }
        try (Stream<Path> paths = Files.walk(legacyProfileDir)) {
            for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
                Files.deleteIfExists(path);
            }
        }
    }

    private boolean isProfileAvatarFile(String fileName, String baseName) {
        return fileName.equals(baseName + "png")
                || fileName.equals(baseName + "jpg")
                || fileName.equals(baseName + "webp")
                || fileName.equals(baseName + "gif");
    }

    private void updateAuthIdentityAvatar(HttpSession session, String avatarUrl) {
        Object authIdentityId = session.getAttribute("authIdentityId");
        Long identityId = null;
        if (authIdentityId instanceof Number number) {
            identityId = number.longValue();
        } else if (authIdentityId instanceof String text) {
            try {
                identityId = Long.parseLong(text);
            } catch (NumberFormatException e) {
                identityId = null;
            }
        }
        if (identityId == null) {
            return;
        }

        UserAuthIdentityData identity = userAuthIdentitiesMapper.getUserAuthIdentity(identityId);
        if (identity == null) {
            return;
        }
        identity.setAvatarUrl(avatarUrl);
        userAuthIdentitiesMapper.updateUserAuthIdentity(identity);
    }

    private void updateAuthIdentityDisplayName(HttpSession session, String displayName) {
        Object authIdentityId = session.getAttribute("authIdentityId");
        Long identityId = null;
        if (authIdentityId instanceof Number number) {
            identityId = number.longValue();
        } else if (authIdentityId instanceof String text) {
            try {
                identityId = Long.parseLong(text);
            } catch (NumberFormatException e) {
                identityId = null;
            }
        }
        if (identityId == null) {
            return;
        }

        UserAuthIdentityData identity = userAuthIdentitiesMapper.getUserAuthIdentity(identityId);
        if (identity == null) {
            return;
        }
        identity.setDisplayName(displayName);
        userAuthIdentitiesMapper.updateUserAuthIdentity(identity);
    }

    private void updateAvatarSession(HttpSession session, String avatarUrl) {
        session.setAttribute("avatarUrl", avatarUrl);

        Object account = session.getAttribute("account");
        if (account instanceof Map<?, ?> existingAccount) {
            Map<String, Object> updatedAccount = new LinkedHashMap<>();
            existingAccount.forEach((key, value) -> {
                if (key != null) {
                    updatedAccount.put(String.valueOf(key), value);
                }
            });
            updatedAccount.put("avatarUrl", avatarUrl);
            session.setAttribute("account", updatedAccount);
        }
    }

    private void updateDisplayNameSession(HttpSession session, String displayName) {
        session.setAttribute("displayName", displayName);

        Object account = session.getAttribute("account");
        if (account instanceof Map<?, ?> existingAccount) {
            Map<String, Object> updatedAccount = new LinkedHashMap<>();
            existingAccount.forEach((key, value) -> {
                if (key != null) {
                    updatedAccount.put(String.valueOf(key), value);
                }
            });
            updatedAccount.put("displayName", displayName);
            session.setAttribute("account", updatedAccount);
        }
    }

    private void saveLoginSession(HttpSession session, UserData user, UserAuthIdentityData identity) {
        session.setAttribute("id", user.getId());
        session.setAttribute("userId", user.getId());
        session.setAttribute("displayName", user.getDisplayName());
        session.setAttribute("email", user.getCanonicalEmail());
        session.setAttribute("avatarUrl", user.getAvatarUrl());
        session.setAttribute("role", user.getRole());
        session.setAttribute("status", user.getStatus());
        session.setAttribute("authProvider", identity.getProvider());
        session.setAttribute("authIdentityId", identity.getId());
        session.setAttribute("lastLoginAt", user.getLastLoginAt());

        Map<String, Object> account = new LinkedHashMap<>();
        account.put("id", user.getId());
        account.put("displayName", user.getDisplayName());
        account.put("email", user.getCanonicalEmail());
        account.put("avatarUrl", user.getAvatarUrl());
        account.put("role", user.getRole());
        account.put("status", user.getStatus());
        account.put("authProvider", identity.getProvider());
        account.put("authIdentityId", identity.getId());
        account.put("lastLoginAt", user.getLastLoginAt());
        session.setAttribute("account", account);

        // RBAC 권한 스냅샷 (결정4 — 세션 캐시. 인터셉터/게이트가 epoch 대조로 갱신)
        Set<String> permissions = new HashSet<>(userPermissionsMapper.listKeys(user.getId()));
        session.setAttribute("permissions", permissions);
        long permsEpoch = user.getPermissionsEpoch() != null ? user.getPermissionsEpoch() : 0L;
        session.setAttribute("permsEpoch", permsEpoch);
    }

    private String normalizeEmail(String email) {
        if (email == null) {
            return null;
        }
        String normalized = email.trim().toLowerCase(Locale.ROOT);
        return normalized.isBlank() ? null : normalized;
    }

    private String normalizeDisplayName(String displayName, String defaultValue) {
        String normalized = displayName == null ? "" : displayName.trim();
        if (normalized.isBlank()) {
            normalized = defaultValue == null ? "" : defaultValue;
        }
        if (normalized.isBlank()) {
            return null;
        }
        return normalized.length() > 32 ? normalized.substring(0, 32) : normalized;
    }

    private boolean isUsablePassword(String password) {
        return password != null && password.length() >= PASSWORD_MIN_LENGTH;
    }

    private String hashPassword(String password) {
        byte[] salt = new byte[PASSWORD_SALT_BYTES];
        secureRandom.nextBytes(salt);
        byte[] hash = pbkdf2(password.toCharArray(), salt, PASSWORD_ITERATIONS, PASSWORD_KEY_LENGTH);
        return "pbkdf2_sha256$" + PASSWORD_ITERATIONS + "$"
                + Base64.getEncoder().encodeToString(salt) + "$"
                + Base64.getEncoder().encodeToString(hash);
    }

    private boolean verifyPassword(String password, String passwordHash) {
        String[] parts = passwordHash.split("\\$");
        if (parts.length != 4 || !"pbkdf2_sha256".equals(parts[0])) {
            return false;
        }

        try {
            int iterations = Integer.parseInt(parts[1]);
            byte[] salt = Base64.getDecoder().decode(parts[2]);
            byte[] expected = Base64.getDecoder().decode(parts[3]);
            byte[] actual = pbkdf2(password.toCharArray(), salt, iterations, expected.length * 8);
            return MessageDigest.isEqual(expected, actual);
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    private byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLength) {
        try {
            PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            return factory.generateSecret(spec).getEncoded();
        } catch (Exception e) {
            throw new IllegalStateException("Cannot process password", e);
        }
    }

    private OffsetDateTime now() {
        return OffsetDateTime.now(ZoneOffset.UTC);
    }

    private boolean wantsJson(HttpServletRequest request) {
        String accept = request.getHeader(HttpHeaders.ACCEPT);
        String requestedWith = request.getHeader("X-Requested-With");
        return (accept != null && accept.contains("application/json"))
                || "XMLHttpRequest".equalsIgnoreCase(requestedWith);
    }

    private ResponseEntity<?> signupError(
            boolean json,
            HttpServletRequest request,
            String code,
            HttpStatus status,
            String message
    ) {
        return authError(json, request, "/signup", code, status, message);
    }

    private ResponseEntity<?> authError(
            boolean json,
            HttpServletRequest request,
            String path,
            String code,
            HttpStatus status,
            String message
    ) {
        if (json) {
            return ResponseEntity.status(status).body(new AuthResult(status.value(), message));
        }
        return redirect(request, path + "?error=" + code);
    }

    private ResponseEntity<Void> redirect(HttpServletRequest request, String path) {
        return ResponseEntity
                .status(HttpStatus.SEE_OTHER)
                .header(HttpHeaders.LOCATION, request.getContextPath() + path)
                .build();
    }

    public record AuthResult(int status, String message) {
    }

    public record ProfileAvatarResult(int status, String message, String avatarUrl) {
    }

    public record ProfileNicknameResult(int status, String message, String displayName) {
    }
}
