package com.pandoli365.bibimbap.controller.api;

import com.pandoli365.bibimbap.data.GameReviewData;
import com.pandoli365.bibimbap.data.ReviewAxisRow;
import com.pandoli365.bibimbap.mapper.GameReviewAxesMapper;
import com.pandoli365.bibimbap.mapper.GameReviewStatsMapper;
import com.pandoli365.bibimbap.mapper.GameReviewsMapper;
import com.pandoli365.bibimbap.mapper.GamesMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionGate;
import com.pandoli365.bibimbap.util.TextNormalizer;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Controller
public class GameReviewController {

    private static final int RATING_MIN = 1;
    private static final int RATING_MAX = 5;
    private static final int BODY_MIN = 10;
    private static final int BODY_MAX = 1000;
    private static final int PAGE_SIZE = 20;
    private static final String[] AXIS_KEYS = {"immersion", "creativity", "controls", "completeness", "sound", "visual"};

    private final GameReviewsMapper gameReviewsMapper;
    private final GameReviewAxesMapper gameReviewAxesMapper;
    private final GameReviewStatsMapper gameReviewStatsMapper;
    private final GamesMapper gamesMapper;
    private final PermissionGate permissionGate;

    public GameReviewController(GameReviewsMapper gameReviewsMapper,
                                GameReviewAxesMapper gameReviewAxesMapper,
                                GameReviewStatsMapper gameReviewStatsMapper,
                                GamesMapper gamesMapper,
                                PermissionGate permissionGate) {
        this.gameReviewsMapper = gameReviewsMapper;
        this.gameReviewAxesMapper = gameReviewAxesMapper;
        this.gameReviewStatsMapper = gameReviewStatsMapper;
        this.gamesMapper = gamesMapper;
        this.permissionGate = permissionGate;
    }

    @GetMapping("/game/{id}/reviews")
    public ResponseEntity<Map<String, Object>> listReviews(
            @PathVariable("id") long id,
            @RequestParam(name = "page", defaultValue = "0") int page,
            @RequestParam(name = "sort", required = false) String sort
    ) {
        if (gamesMapper.getGame(id) == null) {
            return response(HttpStatus.NOT_FOUND, "게임을 찾을 수 없습니다.");
        }

        String sortEnum = normalizeReviewSort(sort);
        int offset = Math.max(page, 0) * PAGE_SIZE;
        List<GameReviewData> rows = gameReviewsMapper.listGameReviews(id, sortEnum, offset, PAGE_SIZE + 1);
        boolean hasMore = rows.size() > PAGE_SIZE;
        int count = Math.min(rows.size(), PAGE_SIZE);
        List<GameReviewData> pageRows = rows.subList(0, count);

        // axes batch 조회(N+1 회피). 빈 페이지 가드.
        if (!pageRows.isEmpty()) {
            List<Long> ids = new ArrayList<>();
            for (GameReviewData r : pageRows) {
                ids.add(r.getId());
            }
            Map<Long, Map<String, Integer>> axesByReview = new LinkedHashMap<>();
            for (ReviewAxisRow ax : gameReviewAxesMapper.listAxesByReviewIds(ids)) {
                axesByReview.computeIfAbsent(ax.getReviewId(), k -> new LinkedHashMap<>())
                        .put(ax.getAxisKey(), ax.getScore());
            }
            for (GameReviewData r : pageRows) {
                r.setAxes(axesByReview.get(r.getId()));
            }
        }

        List<Map<String, Object>> reviews = new ArrayList<>();
        for (GameReviewData r : pageRows) {
            reviews.add(reviewView(r));
        }

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", 200);
        body.put("reviews", reviews);
        body.put("hasMore", hasMore);
        body.put("summary", buildSummary(id));
        return ResponseEntity.ok(body);
    }

    @GetMapping("/game/{id}/reviews/{reviewId}")
    public ResponseEntity<Map<String, Object>> getReview(
            @PathVariable("id") long id,
            @PathVariable("reviewId") long reviewId
    ) {
        GameReviewData review = gameReviewsMapper.getGameReview(reviewId);
        if (review == null || !Long.valueOf(id).equals(review.getGameId())) {
            return response(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다.");
        }
        review.setAxes(loadAxes(reviewId));

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", 200);
        body.put("review", reviewView(review));
        return ResponseEntity.ok(body);
    }

    @PostMapping("/game/{id}/reviews")
    @Transactional
    public ResponseEntity<Map<String, Object>> createReview(
            @PathVariable("id") long id,
            @RequestParam(name = "rating", required = false) String rating,
            @RequestParam(name = "body", required = false) String body,
            @RequestParam(name = "immersion", required = false) String immersion,
            @RequestParam(name = "creativity", required = false) String creativity,
            @RequestParam(name = "controls", required = false) String controls,
            @RequestParam(name = "completeness", required = false) String completeness,
            @RequestParam(name = "sound", required = false) String sound,
            @RequestParam(name = "visual", required = false) String visual,
            HttpServletRequest request,
            HttpSession session
    ) {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        Long userId = sessionUserId(session);
        if (userId == null) {
            return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
        }
        if (gamesMapper.getGame(id) == null) {
            return response(HttpStatus.NOT_FOUND, "게임을 찾을 수 없습니다.");
        }

        Map<String, Integer> axes = parseAxes(immersion, creativity, controls, completeness, sound, visual);
        if (axes == null) {
            return response(HttpStatus.BAD_REQUEST, "세부 평가 항목은 모두 1~5 사이로 선택해 주세요.");
        }
        String normalizedBody = trimToNull(TextNormalizer.normalize(body));
        if (normalizedBody == null || normalizedBody.length() < BODY_MIN) {
            return response(HttpStatus.BAD_REQUEST, "평가는 최소 10자 이상 입력해 주세요.");
        }
        if (normalizedBody.length() > BODY_MAX) {
            return response(HttpStatus.BAD_REQUEST, "평가는 1,000자 이내로 입력해 주세요.");
        }

        Integer overall;
        boolean ratingManual;
        if (trimToNull(rating) == null) {
            overall = averageOf(axes);
            ratingManual = false;
        } else {
            overall = parseRating(rating);
            if (overall == null) {
                return response(HttpStatus.BAD_REQUEST, "별점은 1~5 사이로 선택해 주세요.");
            }
            ratingManual = true;
        }

        if (gameReviewsMapper.getActiveReviewByGameAndUser(id, userId) != null) {
            return response(HttpStatus.CONFLICT, "이미 이 게임에 리뷰를 작성하셨습니다.");
        }

        GameReviewData review = new GameReviewData();
        review.setGameId(id);
        review.setUserId(userId);
        review.setRating(overall);
        review.setBody(normalizedBody);
        review.setRatingManual(ratingManual);
        gameReviewsMapper.addGameReview(review);
        if (review.getId() == null) {
            return response(HttpStatus.INTERNAL_SERVER_ERROR, "리뷰 등록 결과를 확인하지 못했습니다.");
        }
        gameReviewAxesMapper.addReviewAxes(review.getId(), toAxisRows(review.getId(), axes));

        GameReviewData created = gameReviewsMapper.getGameReview(review.getId());
        if (created != null) {
            created.setAxes(loadAxes(created.getId()));
        }
        Map<String, Object> result = created != null ? reviewView(created) : reviewView(review);
        result.put("status", 200);
        result.put("message", "리뷰가 등록되었습니다.");
        return ResponseEntity.ok(result);
    }

    @PutMapping("/game/{id}/reviews/{reviewId}")
    @Transactional
    public ResponseEntity<Map<String, Object>> updateReview(
            @PathVariable("id") long id,
            @PathVariable("reviewId") long reviewId,
            @RequestParam(name = "rating", required = false) String rating,
            @RequestParam(name = "body", required = false) String body,
            @RequestParam(name = "immersion", required = false) String immersion,
            @RequestParam(name = "creativity", required = false) String creativity,
            @RequestParam(name = "controls", required = false) String controls,
            @RequestParam(name = "completeness", required = false) String completeness,
            @RequestParam(name = "sound", required = false) String sound,
            @RequestParam(name = "visual", required = false) String visual,
            HttpServletRequest request,
            HttpSession session
    ) {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        Long userId = sessionUserId(session);
        if (userId == null) {
            return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
        }

        GameReviewData review = gameReviewsMapper.getGameReview(reviewId);
        if (review == null || !Long.valueOf(id).equals(review.getGameId())) {
            return response(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다.");
        }
        if (!canModify(userId, review.getUserId(), session)) {
            return response(HttpStatus.FORBIDDEN, "작성자만 수정할 수 있습니다.");
        }

        Map<String, Integer> axes = parseAxes(immersion, creativity, controls, completeness, sound, visual);
        if (axes == null) {
            return response(HttpStatus.BAD_REQUEST, "세부 평가 항목은 모두 1~5 사이로 선택해 주세요.");
        }
        String normalizedBody = trimToNull(TextNormalizer.normalize(body));
        if (normalizedBody == null || normalizedBody.length() < BODY_MIN) {
            return response(HttpStatus.BAD_REQUEST, "평가는 최소 10자 이상 입력해 주세요.");
        }
        if (normalizedBody.length() > BODY_MAX) {
            return response(HttpStatus.BAD_REQUEST, "평가는 1,000자 이내로 입력해 주세요.");
        }

        Integer overall;
        boolean ratingManual;
        if (trimToNull(rating) == null) {
            overall = averageOf(axes);
            ratingManual = false;
        } else {
            overall = parseRating(rating);
            if (overall == null) {
                return response(HttpStatus.BAD_REQUEST, "별점은 1~5 사이로 선택해 주세요.");
            }
            ratingManual = true;
        }

        review.setRating(overall);
        review.setBody(normalizedBody);
        review.setRatingManual(ratingManual);
        gameReviewsMapper.editGameReview(review);
        gameReviewAxesMapper.deleteReviewAxes(reviewId);
        gameReviewAxesMapper.addReviewAxes(reviewId, toAxisRows(reviewId, axes));

        GameReviewData updated = gameReviewsMapper.getGameReview(reviewId);
        if (updated != null) {
            updated.setAxes(loadAxes(reviewId));
        }
        Map<String, Object> result = updated != null ? reviewView(updated) : reviewView(review);
        result.put("status", 200);
        result.put("message", "리뷰가 수정되었습니다.");
        return ResponseEntity.ok(result);
    }

    @DeleteMapping("/game/{id}/reviews/{reviewId}")
    @Transactional
    public ResponseEntity<Map<String, Object>> deleteReview(
            @PathVariable("id") long id,
            @PathVariable("reviewId") long reviewId,
            HttpServletRequest request,
            HttpSession session
    ) {
        if (!CsrfTokens.isValid(request)) {
            return ResponseEntity.status(HttpStatus.FORBIDDEN).body(CsrfTokens.errorBody());
        }
        Long userId = sessionUserId(session);
        if (userId == null) {
            return response(HttpStatus.UNAUTHORIZED, "로그인이 필요합니다.");
        }

        GameReviewData review = gameReviewsMapper.getGameReview(reviewId);
        if (review == null || !Long.valueOf(id).equals(review.getGameId())) {
            return response(HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다.");
        }
        if (!canModify(userId, review.getUserId(), session)) {
            return response(HttpStatus.FORBIDDEN, "작성자만 삭제할 수 있습니다.");
        }

        gameReviewsMapper.softDeleteGameReview(reviewId);

        Map<String, Object> result = new LinkedHashMap<>();
        result.put("status", 200);
        result.put("message", "리뷰가 삭제되었습니다.");
        return ResponseEntity.ok(result);
    }

    private Map<String, Object> reviewView(GameReviewData review) {
        Map<String, Object> view = new LinkedHashMap<>();
        view.put("reviewId", review.getId());
        view.put("gameId", review.getGameId());
        view.put("authorName", review.getAuthorName());
        view.put("userId", review.getUserId());
        view.put("rating", review.getRating());
        view.put("body", review.getBody());
        view.put("ratingManual", review.getRatingManual() != null && review.getRatingManual());
        view.put("edited", review.getEdited() != null && review.getEdited());
        view.put("axes", review.getAxes());
        view.put("createdAt", review.getCreatedAt());
        view.put("updatedAt", review.getUpdatedAt());
        return view;
    }

    // 6축 점수 파싱·검증. 전 축 1~5 필수. 위반 시 null 반환(컨트롤러가 400).
    private Map<String, Integer> parseAxes(String immersion, String creativity, String controls,
                                           String completeness, String sound, String visual) {
        Map<String, String> raw = new LinkedHashMap<>();
        raw.put("immersion", immersion);
        raw.put("creativity", creativity);
        raw.put("controls", controls);
        raw.put("completeness", completeness);
        raw.put("sound", sound);
        raw.put("visual", visual);

        Map<String, Integer> axes = new LinkedHashMap<>();
        for (String key : AXIS_KEYS) {
            String v = trimToNull(raw.get(key));
            if (v == null) {
                return null;
            }
            int s;
            try {
                s = Integer.parseInt(v);
            } catch (NumberFormatException e) {
                return null;
            }
            if (s < RATING_MIN || s > RATING_MAX) {
                return null;
            }
            axes.put(key, s);
        }
        return axes;
    }

    private int averageOf(Map<String, Integer> axes) {
        int sum = 0;
        for (String key : AXIS_KEYS) {
            sum += axes.get(key);
        }
        return (int) Math.round(sum / (double) AXIS_KEYS.length);
    }

    private List<ReviewAxisRow> toAxisRows(long reviewId, Map<String, Integer> axes) {
        List<ReviewAxisRow> rows = new ArrayList<>();
        for (String key : AXIS_KEYS) {
            rows.add(new ReviewAxisRow(reviewId, key, axes.get(key)));
        }
        return rows;
    }

    private Map<String, Integer> loadAxes(long reviewId) {
        Map<String, Integer> axes = new LinkedHashMap<>();
        for (ReviewAxisRow ax : gameReviewAxesMapper.listAxesByReviewIds(List.of(reviewId))) {
            axes.put(ax.getAxisKey(), ax.getScore());
        }
        return axes.isEmpty() ? null : axes;
    }

    private Map<String, Object> buildSummary(long gameId) {
        Map<String, Object> stats = gameReviewStatsMapper.getStats(gameId);
        if (stats == null) {
            return null;
        }
        Object countValue = stats.get("reviewCount");
        long reviewCount = countValue instanceof Number number ? number.longValue() : 0L;
        if (reviewCount == 0) {
            return null;
        }
        Map<String, Object> axes = new LinkedHashMap<>();
        for (String key : AXIS_KEYS) {
            axes.put(key, stats.get(key));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("avgRating", stats.get("avgRating"));
        summary.put("reviewCount", reviewCount);
        summary.put("axes", axes);
        return summary;
    }

    private String normalizeReviewSort(String sort) {
        return switch (sort == null ? "" : sort) {
            case "rating_desc" -> "rating_desc";
            case "rating_asc" -> "rating_asc";
            default -> "newest";
        };
    }

    private Integer parseRating(String rating) {
        String text = trimToNull(rating);
        if (text == null) {
            return null;
        }
        int value;
        try {
            value = Integer.parseInt(text);
        } catch (NumberFormatException e) {
            return null;
        }
        if (value < RATING_MIN || value > RATING_MAX) {
            return null;
        }
        return value;
    }

    private boolean canModify(Long currentUserId, Long authorUserId, HttpSession session) {
        return (authorUserId != null && authorUserId.equals(currentUserId)) || permissionGate.canModerate(session);
    }

    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 trimToNull(String value) {
        if (value == null) {
            return null;
        }
        String text = value.trim();
        return text.isBlank() ? null : text;
    }

    private ResponseEntity<Map<String, Object>> response(HttpStatus status, String message) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", status.value());
        body.put("message", message);
        return ResponseEntity.status(status).body(body);
    }
}
