/*
 * Copyright 2013-2020 Erudika. https://erudika.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * For issues and patches go to: https://github.com/erudika
 */
package com.erudika.scoold.controllers;

import com.erudika.para.client.ParaClient;
import com.erudika.para.core.Address;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Pager;
import com.erudika.para.utils.Utils;
import static com.erudika.scoold.ScooldServer.ANSWER_APPROVE_REWARD_AUTHOR;
import static com.erudika.scoold.ScooldServer.ANSWER_APPROVE_REWARD_VOTER;
import static com.erudika.scoold.ScooldServer.MAX_REPLIES_PER_POST;
import com.erudika.scoold.core.Comment;
import com.erudika.scoold.core.Post;
import com.erudika.scoold.core.Profile;
import com.erudika.scoold.core.Profile.Badge;
import com.erudika.scoold.core.Reply;
import com.erudika.scoold.utils.ScooldUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import static com.erudika.scoold.ScooldServer.QUESTIONSLINK;
import static com.erudika.scoold.ScooldServer.SIGNINLINK;
import com.erudika.scoold.core.Question;
import com.erudika.scoold.core.UnapprovedQuestion;
import com.erudika.scoold.core.UnapprovedReply;
import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Optional;

/**
 *
 * @author Alex Bogdanovski [[email protected]]
 */
@Controller
@RequestMapping("/question")
public class QuestionController {

	public static final Logger logger = LoggerFactory.getLogger(QuestionController.class);

	private final ScooldUtils utils;
	private final ParaClient pc;

	@Inject
	public QuestionController(ScooldUtils utils) {
		this.utils = utils;
		this.pc = utils.getParaClient();
	}

	@GetMapping({"/{id}", "/{id}/{title}", "/{id}/{title}/*"})
	public String get(@PathVariable String id, @PathVariable(required = false) String title,
			@RequestParam(required = false) String sortby, HttpServletRequest req, HttpServletResponse res, Model model) {

		Post showPost = pc.read(id);
		if (showPost == null || !ParaObjectUtils.typesMatch(showPost)) {
			return "redirect:" + QUESTIONSLINK;
		}
		Profile authUser = utils.getAuthUser(req);
		if (!utils.canAccessSpace(authUser, showPost.getSpace())) {
			return "redirect:" + (utils.isDefaultSpacePublic() || utils.isAuthenticated(req) ?
					QUESTIONSLINK : SIGNINLINK + "?returnto=" + req.getRequestURI());
		} else if (!utils.isDefaultSpace(showPost.getSpace()) && pc.read(utils.getSpaceId(showPost.getSpace())) == null) {
			showPost.setSpace(Post.DEFAULT_SPACE);
			pc.update(showPost);
		}

		if (showPost instanceof UnapprovedQuestion && !(utils.isMine(showPost, authUser) || utils.isMod(authUser))) {
			return "redirect:" + QUESTIONSLINK;
		}

		Pager itemcount = utils.getPager("page", req);
		itemcount.setSortby("newest".equals(sortby) ? "timestamp" : "votes");
		List<Reply> answerslist = getAllAnswers(authUser, showPost, itemcount);
		LinkedList<Post> allPosts = new LinkedList<Post>();
		allPosts.add(showPost);
		allPosts.addAll(answerslist);
		utils.fetchProfiles(allPosts);
		utils.getComments(allPosts);
		utils.updateViewCount(showPost, req, res);

		model.addAttribute("path", "question.vm");
		model.addAttribute("title", utils.getLang(req).get("questions.title") + " - " + showPost.getTitle());
		model.addAttribute("description", Utils.abbreviate(Utils.stripAndTrim(showPost.getBody(), " "), 195));
		model.addAttribute("itemcount", itemcount);
		model.addAttribute("showPost", allPosts.removeFirst());
		model.addAttribute("answerslist", allPosts);
		model.addAttribute("similarquestions", utils.getSimilarPosts(showPost, new Pager(10)));
		model.addAttribute("maxCommentLength", Comment.MAX_COMMENT_LENGTH);
		model.addAttribute("includeGMapsScripts", utils.isNearMeFeatureEnabled());
		model.addAttribute("maxCommentLengthError", Utils.formatMessage(utils.getLang(req).get("maxlength"),
				Comment.MAX_COMMENT_LENGTH));
		return "base";
	}

	@PostMapping("/{id}/edit")
	public String edit(@PathVariable String id, @RequestParam(required = false) String title,
			@RequestParam(required = false) String body, @RequestParam(required = false) String tags,
			@RequestParam(required = false) String location, @RequestParam(required = false) String latlng,
			@RequestParam(required = false) String space, HttpServletRequest req, HttpServletResponse res, Model model) {

		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (!utils.canEdit(showPost, authUser) || showPost == null) {
			model.addAttribute("post", showPost);
			if (utils.isAjaxRequest(req)) {
				res.setStatus(400);
				return "blank";
			} else {
				return "redirect:" + req.getRequestURI(); // + "/edit-post-12345" ?
			}
		}
		boolean isQuestion = !showPost.isReply();
		HashSet<String> addedTags = new HashSet<>();
		Post beforeUpdate = null;
		try {
			beforeUpdate = (Post) BeanUtils.cloneBean(showPost);
		} catch (Exception ex) {
			logger.error(null, ex);
		}

		if (StringUtils.length(title) > 10) {
			showPost.setTitle(title);
		}
		// body can be blank
		showPost.setBody(body);
		showPost.setLocation(location);
		showPost.setAuthor(authUser);
		if (!StringUtils.isBlank(tags) && isQuestion) {
			showPost.updateTags(showPost.getTags(), Arrays.asList(StringUtils.split(tags, ",")));
			addedTags.addAll(showPost.getTags());
			addedTags.removeAll(new HashSet<>(Optional.ofNullable(showPost.getTags()).orElse(Collections.emptyList())));
		}
		if (isQuestion) {
			String validSpace = utils.getValidSpaceIdExcludingAll(authUser,
					Optional.ofNullable(space).orElse(showPost.getSpace()), req);
			if (utils.canAccessSpace(authUser, validSpace) && validSpace != null &&
					!utils.getSpaceId(validSpace).equals(utils.getSpaceId(showPost.getSpace()))) {
				showPost.setSpace(validSpace);
				changeSpaceForAllAnswers(showPost, validSpace);
			}
		}
		//note: update only happens if something has changed
		if (!showPost.equals(beforeUpdate)) {
			updatePost(showPost, authUser);
			updateLocation(showPost, authUser, location, latlng);
			utils.addBadgeOnceAndUpdate(authUser, Badge.EDITOR, true);
			utils.sendUpdatedFavTagsNotifications(showPost, new ArrayList<>(addedTags));
		}
		model.addAttribute("post", showPost);
		if (utils.isAjaxRequest(req)) {
			res.setStatus(200);
			res.setContentType("application/json");
			try {
				res.getWriter().println("{\"url\":\"" + getPostLink(showPost) + "\"}");
			} catch (IOException ex) { }
			return "blank";
		} else {
			return "redirect:" + showPost.getPostLink(false, false);
		}
	}

	@PostMapping({"/{id}", "/{id}/{title}"})
	public String reply(@PathVariable String id, @PathVariable(required = false) String title,
			@RequestParam(required = false) Boolean emailme, HttpServletRequest req,
			HttpServletResponse res, Model model) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (authUser == null || showPost == null) {
			if (utils.isAjaxRequest(req)) {
				res.setStatus(400);
				return "base";
			} else {
				return "redirect:" + QUESTIONSLINK + "/" + id;
			}
		}
		if (emailme != null) {
			followPost(showPost, authUser, emailme);
		} else if (!showPost.isClosed() && !showPost.isReply()) {
			//create new answer
			boolean needsApproval = utils.postNeedsApproval(authUser);
			Reply answer = utils.populate(req, needsApproval ? new UnapprovedReply() : new Reply(), "body");
			Map<String, String> error = utils.validate(answer);
			if (!error.containsKey("body") && !StringUtils.isBlank(answer.getBody())) {
				answer.setTitle(showPost.getTitle());
				answer.setCreatorid(authUser.getId());
				answer.setParentid(showPost.getId());
				answer.setSpace(showPost.getSpace());
				answer.create();

				showPost.setAnswercount(showPost.getAnswercount() + 1);
				showPost.setLastactivity(System.currentTimeMillis());
				if (showPost.getAnswercount() >= MAX_REPLIES_PER_POST) {
					showPost.setCloserid("0");
				}
				// update without adding revisions
				pc.update(showPost);
				utils.addBadgeAndUpdate(authUser, Badge.EUREKA, answer.getCreatorid().equals(showPost.getCreatorid()));
				answer.setAuthor(authUser);
				model.addAttribute("showPost", showPost);
				model.addAttribute("answerslist", Collections.singletonList(answer));
				// send email to the question author
				utils.sendReplyNotifications(showPost, answer);
				model.addAttribute("newpost", getNewAnswerPayload(answer));
			} else {
				model.addAttribute("error", error);
				model.addAttribute("path", "question.vm");
				res.setStatus(400);
			}
			return "reply";
		} else {
			model.addAttribute("error", "Parent post doesn't exist or cannot have children.");
		}
		if (utils.isAjaxRequest(req)) {
			res.setStatus(200);
			return "reply";
		} else {
			return "redirect:" + QUESTIONSLINK + "/" + id;
		}
	}

	@PostMapping("/{id}/approve")
	public String modApprove(@PathVariable String id, HttpServletRequest req) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (utils.isMod(authUser)) {
			if (showPost instanceof UnapprovedQuestion) {
				showPost.setType(Utils.type(Question.class));
				pc.create(showPost);
				utils.sendNewPostNotifications(showPost);
			} else if (showPost instanceof UnapprovedReply) {
				showPost.setType(Utils.type(Reply.class));
				pc.create(showPost);
			}
		}
		return "redirect:" + showPost.getPostLink(false, false);
	}

	@PostMapping("/{id}/approve/{answerid}")
	public String approve(@PathVariable String id, @PathVariable String answerid, HttpServletRequest req) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (!utils.canEdit(showPost, authUser) || showPost == null) {
			return "redirect:" + req.getRequestURI();
		}
		if (utils.canEdit(showPost, authUser) && answerid != null &&
				(utils.isMine(showPost, authUser) || utils.isAdmin(authUser))) {
			Reply answer = (Reply) pc.read(answerid);

			if (answer != null && answer.isReply()) {
				Profile author = pc.read(answer.getCreatorid());
				if (author != null && utils.isAuthenticated(req)) {
					boolean samePerson = author.equals(authUser);

					if (answerid.equals(showPost.getAnswerid())) {
						// Answer approved award - UNDO
						showPost.setAnswerid("");
						if (!samePerson) {
							author.removeRep(ANSWER_APPROVE_REWARD_AUTHOR);
							authUser.removeRep(ANSWER_APPROVE_REWARD_VOTER);
							pc.updateAll(Arrays.asList(author, authUser));
						}
					} else {
						// Answer approved award - GIVE
						showPost.setAnswerid(answerid);
						if (!samePerson) {
							author.addRep(ANSWER_APPROVE_REWARD_AUTHOR);
							authUser.addRep(ANSWER_APPROVE_REWARD_VOTER);
							utils.addBadgeOnce(authUser, Badge.NOOB, true);
							pc.updateAll(Arrays.asList(author, authUser));
						}
						utils.triggerHookEvent("answer.accept",
								getAcceptedAnswerPayload(showPost, answer, authUser, author));
					}
					showPost.update();
				}
			}
		}
		return "redirect:" + showPost.getPostLink(false, false);
	}

	@PostMapping("/{id}/close")
	public String close(@PathVariable String id, HttpServletRequest req) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (showPost == null) {
			return "redirect:" + req.getRequestURI();
		}
		if (utils.isMod(authUser) && !showPost.isReply()) {
			if (showPost.isClosed()) {
				showPost.setCloserid("");
			} else {
				showPost.setCloserid(authUser.getId());
				utils.triggerHookEvent("question.close", showPost);
			}
			showPost.update();
		}
		return "redirect:" + showPost.getPostLink(false, false);
	}

	@PostMapping("/{id}/restore/{revisionid}")
	public String restore(@PathVariable String id, @PathVariable String revisionid, HttpServletRequest req) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (!utils.canEdit(showPost, authUser) || showPost == null) {
			return "redirect:" + req.getRequestURI();
		}
		if (utils.canEdit(showPost, authUser)) {
			utils.addBadgeAndUpdate(authUser, Badge.BACKINTIME, true);
			showPost.restoreRevisionAndUpdate(revisionid);
		}
		return "redirect:" + showPost.getPostLink(false, false);
	}

	@PostMapping("/{id}/delete")
	public String delete(@PathVariable String id, HttpServletRequest req, Model model) {
		Post showPost = pc.read(id);
		Profile authUser = utils.getAuthUser(req);
		if (!utils.canEdit(showPost, authUser) || showPost == null) {
			model.addAttribute("post", showPost);
			return "redirect:" + req.getRequestURI();
		}
		if (!showPost.isReply()) {
			if ((utils.isMine(showPost, authUser) || utils.isMod(authUser))) {
				showPost.delete();
				model.addAttribute("deleted", true);
				return "redirect:" + QUESTIONSLINK + "?success=true&code=16";
			}
		} else if (showPost.isReply()) {
			if (utils.isMine(showPost, authUser) || utils.isMod(authUser)) {
				Post parent = pc.read(showPost.getParentid());
				parent.setAnswercount(parent.getAnswercount() - 1);
				parent.update();
				showPost.delete();
				model.addAttribute("deleted", true);
			}
		}
		return "redirect:" + showPost.getPostLink(false, false);
	}

	private void changeSpaceForAllAnswers(Post showPost, String space) {
		if (showPost == null || showPost.isReply()) {
			return;
		}
		Pager pager = new Pager(1, "_docid", false, Config.MAX_ITEMS_PER_PAGE);
		List<Reply> answerslist;
		try {
			do {
				answerslist = pc.getChildren(showPost, Utils.type(Reply.class), pager);
				for (Reply reply : answerslist) {
					reply.setSpace(space);
				}
				if (!answerslist.isEmpty()) {
					pc.updateAll(answerslist);
					Thread.sleep(500);
				}
			} while (!answerslist.isEmpty());
		} catch (InterruptedException ex) {
			logger.error(null, ex);
			Thread.currentThread().interrupt();
		}
	}

	private List<Reply> getAllAnswers(Profile authUser, Post showPost, Pager itemcount) {
		if (showPost == null || showPost.isReply()) {
			return Collections.emptyList();
		}
		List<Reply> answers = new ArrayList<>();
		Pager p = new Pager(itemcount.getPage(), itemcount.getLimit());
		if (utils.postsNeedApproval() && (utils.isMine(showPost, authUser) || utils.isMod(authUser))) {
			answers.addAll(showPost.getUnapprovedAnswers(p));
		}
		answers.addAll(showPost.getAnswers(itemcount));
		itemcount.setCount(itemcount.getCount() + p.getCount());
		return answers;
	}

	private void updatePost(Post showPost, Profile authUser) {
		showPost.setLasteditby(authUser.getId());
		showPost.setLastedited(System.currentTimeMillis());
		if (showPost.isQuestion()) {
			showPost.setLastactivity(System.currentTimeMillis());
			showPost.update();
		} else if (showPost.isReply()) {
			Post questionPost = pc.read(showPost.getParentid());
			if (questionPost != null) {
				showPost.setSpace(questionPost.getSpace());
				questionPost.setLastactivity(System.currentTimeMillis());
				pc.updateAll(Arrays.asList(showPost, questionPost));
			} else {
				showPost.update();
			}
		}
	}

	private void updateLocation(Post showPost, Profile authUser, String location, String latlng) {
		if (!showPost.isReply() && !StringUtils.isBlank(latlng)) {
			Address addr = new Address(showPost.getId() + Config.SEPARATOR + Utils.type(Address.class));
			addr.setAddress(location);
			addr.setCountry(location);
			addr.setLatlng(latlng);
			addr.setParentid(showPost.getId());
			addr.setCreatorid(authUser.getId());
			pc.create(addr);
		}
	}

	private Map<String, Object> getNewAnswerPayload(Reply answer) {
		Map<String, Object> payload = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(answer, false));
		payload.put("author", answer == null ? null : answer.getAuthor());
		utils.triggerHookEvent("answer.create", payload);
		return payload;
	}

	private Object getAcceptedAnswerPayload(Post showPost, Reply answer, Profile authUser, Profile author) {
		Map<String, Object> payload = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(showPost, false));
		Map<String, Object> answerPayload = new LinkedHashMap<>(ParaObjectUtils.getAnnotatedFields(answer, false));
		answerPayload.put("author", author);
		payload.put("children", answerPayload);
		payload.put("authUser", authUser);
		return payload;
	}

	private void followPost(Post showPost, Profile authUser, Boolean emailme) {
		if (emailme) {
			showPost.addFollower(authUser.getUser());
		} else {
			showPost.removeFollower(authUser.getUser());
		}
		pc.update(showPost); // update without adding revisions
	}

	private String getPostLink(Post showPost) {
		return showPost.getPostLink(false, false) + (showPost.isQuestion() ? "" :  "#post-" + showPost.getId());
	}
}