package com.pandoli365.bibimbap.controller;

import com.pandoli365.bibimbap.data.PostCategoryData;
import com.pandoli365.bibimbap.data.PostData;
import com.pandoli365.bibimbap.mapper.PostCategoriesMapper;
import com.pandoli365.bibimbap.mapper.PostsMapper;
import com.pandoli365.bibimbap.security.CsrfTokens;
import com.pandoli365.bibimbap.security.PermissionGate;
import com.pandoli365.bibimbap.security.PermissionKeys;
import com.pandoli365.bibimbap.service.OgPreviewService;
import com.pandoli365.bibimbap.service.OgPreviewService.OgPreview;
import com.pandoli365.bibimbap.service.PostMarkdownService;
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.ui.Model;
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.RequestParam;
import org.springframework.web.server.ResponseStatusException;

import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

@Controller
public class PostController {

    private static final int PAGE_SIZE = 20;
    private static final int MAX_TITLE = 200;
    private static final int MAX_BODY = 20000;
    private static final int MAX_LINK_URL = 2048;
    private static final Set<String> STATUSES = Set.of("DRAFT", "PUBLISHED");
    private static final String DEFAULT_STATUS = "PUBLISHED";

    private final PostsMapper postsMapper;
    private final PostCategoriesMapper postCategoriesMapper;
    private final PermissionGate gate;
    private final PostMarkdownService postMarkdownService;
    private final OgPreviewService ogPreviewService;

    public PostController(PostsMapper postsMapper,
                          PostCategoriesMapper postCategoriesMapper,
                          PermissionGate gate,
                          PostMarkdownService postMarkdownService,
                          OgPreviewService ogPreviewService) {
        this.postsMapper = postsMapper;
        this.postCategoriesMapper = postCategoriesMapper;
        this.gate = gate;
        this.postMarkdownService = postMarkdownService;
        this.ogPreviewService = ogPreviewService;
    }

    @GetMapping("/posts")
    public String list(
            @RequestParam(name = "categoryId", required = false) Long categoryId,
            @RequestParam(name = "cursorCreatedAt", required = false) String cursorCreatedAt,
            @RequestParam(name = "cursorId", required = false) Long cursorId,
            Model model
    ) {
        OffsetDateTime cursor = parseOffsetDateTime(cursorCreatedAt);
        List<PostData> rows = postsMapper.listPublishedKeyset(cursor, cursorId, categoryId, PAGE_SIZE + 1);

        boolean hasNext = rows.size() > PAGE_SIZE;
        if (hasNext) {
            rows = rows.subList(0, PAGE_SIZE);
        }

        model.addAttribute("posts", rows);
        model.addAttribute("categories", postCategoriesMapper.listActive());
        model.addAttribute("categoryId", categoryId);
        model.addAttribute("hasNext", hasNext);
        if (hasNext && !rows.isEmpty()) {
            PostData last = rows.get(rows.size() - 1);
            model.addAttribute("nextCursorCreatedAt", last.getCreatedAt());
            model.addAttribute("nextCursorId", last.getId());
        }
        return "posts-list";
    }

    @GetMapping("/posts/{id}")
    public String detail(@PathVariable("id") long id, Model model) {
        PostData post = postsMapper.getPublished(id);
        if (post == null) {
            return "redirect:/posts";
        }
        model.addAttribute("post", post);
        return "posts-detail";
    }

    @GetMapping("/posts/new")
    public String createForm(HttpSession session, Model model) {
        if (sessionUserId(session) == null) {
            return "redirect:/login";
        }
        requireWritePermission(session);
        model.addAttribute("categories", postCategoriesMapper.listActive());
        model.addAttribute("csrfToken", CsrfTokens.getOrCreate(session));
        model.addAttribute("mode", "new");
        return "posts-form";
    }

    @PostMapping("/posts")
    @Transactional
    public ResponseEntity<Map<String, Object>> create(
            @RequestParam(name = "categoryId", required = false) Long categoryId,
            @RequestParam(name = "title", required = false) String title,
            @RequestParam(name = "bodyMarkdown", required = false) String bodyMarkdown,
            @RequestParam(name = "linkUrl", required = false) String linkUrl,
            @RequestParam(name = "status", required = false) String status,
            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 (!gate.has(session, PermissionKeys.POST_WRITE.name())) {
            return response(HttpStatus.FORBIDDEN, "권한이 없습니다.");
        }

        Validated validated = validate(title, bodyMarkdown, linkUrl, status, categoryId);
        if (validated.error != null) {
            return validated.error;
        }

        PostData post = new PostData();
        post.setCategoryId(categoryId);
        post.setAuthorUserId(userId);
        post.setTitle(validated.title);
        post.setBodyMarkdown(validated.bodyMarkdown);
        post.setBodySanitizedHtml(postMarkdownService.render(validated.bodyMarkdown));
        post.setLinkUrl(validated.linkUrl);
        post.setStatus(validated.status);
        applyOgPreview(post, validated.linkUrl);

        postsMapper.insert(post);
        if (post.getId() == null) {
            return response(HttpStatus.INTERNAL_SERVER_ERROR, "포스트 등록 결과를 확인하지 못했습니다.");
        }

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", 200);
        body.put("message", "포스트가 등록되었습니다.");
        body.put("postId", post.getId());
        body.put("location", "/posts/" + post.getId());
        return ResponseEntity.ok(body);
    }

    @GetMapping("/posts/{id}/edit")
    public String editForm(@PathVariable("id") long id, HttpSession session, Model model) {
        if (sessionUserId(session) == null) {
            return "redirect:/login";
        }
        requireWritePermission(session);
        PostData post = postsMapper.getPublished(id);
        if (post == null) {
            return "redirect:/posts";
        }
        model.addAttribute("post", post);
        model.addAttribute("categories", postCategoriesMapper.listActive());
        model.addAttribute("csrfToken", CsrfTokens.getOrCreate(session));
        model.addAttribute("mode", "edit");
        return "posts-form";
    }

    @PostMapping("/posts/{id}")
    @Transactional
    public ResponseEntity<Map<String, Object>> update(
            @PathVariable("id") long id,
            @RequestParam(name = "categoryId", required = false) Long categoryId,
            @RequestParam(name = "title", required = false) String title,
            @RequestParam(name = "bodyMarkdown", required = false) String bodyMarkdown,
            @RequestParam(name = "linkUrl", required = false) String linkUrl,
            @RequestParam(name = "status", required = false) String status,
            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 (!gate.has(session, PermissionKeys.POST_WRITE.name())) {
            return response(HttpStatus.FORBIDDEN, "권한이 없습니다.");
        }

        Validated validated = validate(title, bodyMarkdown, linkUrl, status, categoryId);
        if (validated.error != null) {
            return validated.error;
        }

        PostData existing = postsMapper.getPublished(id);
        if (existing == null) {
            return response(HttpStatus.NOT_FOUND, "포스트를 찾을 수 없습니다.");
        }

        existing.setCategoryId(categoryId);
        existing.setTitle(validated.title);
        existing.setBodyMarkdown(validated.bodyMarkdown);
        existing.setBodySanitizedHtml(postMarkdownService.render(validated.bodyMarkdown));
        existing.setStatus(validated.status);

        boolean linkChanged = !equalsLink(existing.getLinkUrl(), validated.linkUrl);
        existing.setLinkUrl(validated.linkUrl);
        if (linkChanged) {
            clearOgPreview(existing);
            applyOgPreview(existing, validated.linkUrl);
        }

        postsMapper.update(existing);

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", 200);
        body.put("message", "포스트가 수정되었습니다.");
        body.put("postId", existing.getId());
        body.put("location", "/posts/" + existing.getId());
        return ResponseEntity.ok(body);
    }

    @PostMapping("/posts/{id}/delete")
    @Transactional
    public ResponseEntity<Map<String, Object>> delete(
            @PathVariable("id") long id,
            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 (!gate.has(session, PermissionKeys.POST_WRITE.name())) {
            return response(HttpStatus.FORBIDDEN, "권한이 없습니다.");
        }

        PostData existing = postsMapper.getPublished(id);
        if (existing == null) {
            return response(HttpStatus.NOT_FOUND, "포스트를 찾을 수 없습니다.");
        }

        postsMapper.softDelete(id);

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("status", 200);
        body.put("message", "포스트가 삭제되었습니다.");
        body.put("postId", id);
        return ResponseEntity.ok(body);
    }

    private void requireWritePermission(HttpSession session) {
        if (!gate.has(session, PermissionKeys.POST_WRITE.name())) {
            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "권한이 없습니다.");
        }
    }

    private Validated validate(String title, String bodyMarkdown, String linkUrl, String status, Long categoryId) {
        Validated v = new Validated();

        String normalizedTitle = trimToNull(title);
        if (normalizedTitle == null || normalizedTitle.length() > MAX_TITLE) {
            v.error = response(HttpStatus.BAD_REQUEST, "제목을 " + MAX_TITLE + "자 이내로 입력해 주세요.");
            return v;
        }
        v.title = normalizedTitle;

        String normalizedBody = bodyMarkdown == null ? "" : bodyMarkdown;
        if (normalizedBody.length() > MAX_BODY) {
            v.error = response(HttpStatus.BAD_REQUEST, "본문은 " + MAX_BODY + "자 이내로 입력해 주세요.");
            return v;
        }
        v.bodyMarkdown = normalizedBody;

        v.linkUrl = normalizeLinkUrl(linkUrl);
        if (v.linkUrl != null && v.linkUrl.length() > MAX_LINK_URL) {
            v.error = response(HttpStatus.BAD_REQUEST, "링크 URL은 " + MAX_LINK_URL + "자 이내로 입력해 주세요.");
            return v;
        }

        String normalizedStatus = trimToNull(status);
        if (normalizedStatus == null) {
            normalizedStatus = DEFAULT_STATUS;
        }
        if (!STATUSES.contains(normalizedStatus)) {
            v.error = response(HttpStatus.BAD_REQUEST, "공개 상태를 확인해 주세요.");
            return v;
        }
        v.status = normalizedStatus;

        if (categoryId == null) {
            v.error = response(HttpStatus.BAD_REQUEST, "카테고리를 선택해 주세요.");
            return v;
        }
        PostCategoryData category = postCategoriesMapper.getActive(categoryId);
        if (category == null) {
            v.error = response(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다.");
            return v;
        }

        return v;
    }

    // OG fetch 는 graceful: 실패해도 작성/수정은 성공시킨다.
    private void applyOgPreview(PostData post, String linkUrl) {
        if (linkUrl == null) {
            return;
        }
        Optional<OgPreview> preview = ogPreviewService.fetch(linkUrl);
        if (preview.isEmpty()) {
            return;
        }
        OgPreview og = preview.get();
        post.setOgTitle(og.title());
        post.setOgDescription(og.description());
        post.setOgImageUrl(og.imageUrl());
        post.setOgSiteName(og.siteName());
        post.setOgFetchedAt(OffsetDateTime.now());
    }

    private void clearOgPreview(PostData post) {
        post.setOgTitle(null);
        post.setOgDescription(null);
        post.setOgImageUrl(null);
        post.setOgSiteName(null);
        post.setOgFetchedAt(null);
    }

    private boolean equalsLink(String a, String b) {
        if (a == null) {
            return b == null;
        }
        return a.equals(b);
    }

    private String normalizeLinkUrl(String linkUrl) {
        String text = trimToNull(linkUrl);
        if (text == null) {
            return null;
        }
        String lower = text.toLowerCase();
        if (!lower.startsWith("http://") && !lower.startsWith("https://")) {
            return null;
        }
        return text;
    }

    private OffsetDateTime parseOffsetDateTime(String value) {
        String text = trimToNull(value);
        if (text == null) {
            return null;
        }
        try {
            return OffsetDateTime.parse(text);
        } catch (DateTimeParseException e) {
            return null;
        }
    }

    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);
    }

    private static final class Validated {
        private String title;
        private String bodyMarkdown;
        private String linkUrl;
        private String status;
        private ResponseEntity<Map<String, Object>> error;
    }
}
