package com.icontrols.oauth.controller;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.List;
import java.util.Random;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import com.icontrols.oauth.constants.ERROR;
import com.icontrols.oauth.model.ClientInfo;
import com.icontrols.oauth.model.ComplexInfo;
import com.icontrols.oauth.model.JWTInfo;
import com.icontrols.oauth.model.Token;
import com.icontrols.oauth.model.WallpadCode;
import com.icontrols.oauth.repo.ClientInfoRepository;
import com.icontrols.oauth.repo.ComplexInfoRepository;
import com.icontrols.oauth.utils.AES256Util;
import com.icontrols.oauth.utils.DanziAuthUtils;
import com.icontrols.oauth.utils.JWTUtils;
import com.sun.net.httpserver.HttpServer;

@RestController
@RequestMapping(value = "/api/oauth2")
public class OAuthController {
	private static final Logger logger = LoggerFactory.getLogger(OAuthController.class);

	@Autowired
	ClientInfoRepository clientInfoRepo;

	@Autowired
	ComplexInfoRepository complexInfoRepo;

	@Autowired
	RedisTemplate<String, Object> redisTemplate;

//	http://127.0.0.1:8080/api/oauth2/authorize?client_id=clientid&redirect_uri=http://127.0.0.1:8080/api/oauth2/redirect&scope=scope&response_type=code&state=state
	@RequestMapping(value = "/authorize", method = RequestMethod.GET)
	public ModelAndView Authorize(@RequestHeader HttpHeaders headers,
			@RequestParam(value = "response_type", required = true) String responseType,
			@RequestParam(value = "client_id", required = true) String clientId,
			@RequestParam(value = "state", required = true) String state, @RequestParam(value = "scope") String scope,
			@RequestParam(value = "redirect_uri", required = true) String redirectUri, HttpSession httpSession,
			Model model) {
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("[STEP #1] /api/oauth2/authorize");
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("*[parameters]");
		logger.info("*1. response_type: " + responseType);
		logger.info("*2. client_id: " + clientId);
		logger.info("*3. state: " + state);
//		logger.info("*4. scope: " + scope);
		logger.info("*5. redirect_uri: " + redirectUri);
		// 월패드 인증코드 시간이 만료되어 history.go(-1)로 해당 페이지에 들어오는 경우에
		// 세션에 저장되었던 사용자 정보를 지움.
		if (httpSession.getAttribute("wallpadCode") != null)
			httpSession.removeAttribute("wallpadCode");
		if (httpSession.getAttribute("homeInfo") != null)
			httpSession.removeAttribute("homeInfo");

		ModelAndView mav = new ModelAndView();

		ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);

		if (clientInfo == null) {
			throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);
		} else {
			logger.info("*[clientinfo]");
			logger.info("*" + clientInfo.toString());

			if (!responseType.equals("code")) {
				throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.UNSUPPORTED_GRANT_TYPE);
			} else {
				// 클라이언트에 따라 endpoint를 변경해야해서 세션에 클라이언트 정보를 저장.
				httpSession.setAttribute("clientId", clientId);
				httpSession.setAttribute("redirectUri", redirectUri);
				httpSession.setAttribute("state", state);

//				if (scope != null)
//					httpSession.setAttribute("scope", scope);

				List<ComplexInfo> complexInfos = complexInfoRepo.findAll();
				model.addAttribute("complexInfos", complexInfos);

				// \src\main\webapp\WEB-INF\jsp\InsertUserInfo.jsp
				mav.setViewName("InsertUserInfo");
			}

		}

		return mav;
	}

	@RequestMapping(value = "/user/info/submit", method = RequestMethod.GET)
	public ModelAndView sendCode(HttpSession httpSession,
			@RequestParam(value = "complex", required = true) String complex,
			@RequestParam(value = "dong", required = true) String dong,
			@RequestParam(value = "ho", required = true) String ho)
			throws IOException, NoSuchAlgorithmException, GeneralSecurityException {

		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("[STEP #2] /api/oauth2/user/submit");
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("*[parameters]");
		logger.info("*1. complex: " + complex);
		logger.info("*2. dong: " + dong);
		logger.info("*3. ho: " + ho);

		String clientId = httpSession.getAttribute("clientId").toString();

		// wallpad 코드 생성
		Random generator = new Random();
		String wallpadCode = "";
		for (int i = 0; i < 6; i++) {
			wallpadCode += Integer.toString(generator.nextInt(10));
		}

		logger.info("*[wallpadCode]");
		logger.info("*code: " + wallpadCode);

		// 세대정보 인코딩
		String homeInfo = complex + "/" + dong + "/" + ho + "/" + clientId;

		// 2019.05.02 DB저장에서 세션 저장 방식으로 변경
		httpSession.setAttribute("wallpadCode", wallpadCode);
		httpSession.setAttribute("homeInfo", homeInfo);

		logger.info("*[redis]");
		logger.info("*(key,value): (" + wallpadCode + "," + homeInfo + ")");

		// 단지서버로 월패드인증번호 전송
		WallpadCode body = new WallpadCode(dong, ho, wallpadCode);
		logger.info(body.toString());

		ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
		String endPoint = clientInfo.getEndPoint();

//		String url = "http://" + complex + ":8002/kakao/auth";
		String url = "http://" + complex + endPoint + "/auth";

		logger.info(url);

		// TODO 예외처리 단지통신
		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
		factory.setReadTimeout(3000);
		factory.setConnectTimeout(3000);
		RestTemplate template = new RestTemplate(factory);
		HttpEntity<Object> entity = new HttpEntity<Object>(body);
		try {
			String answer = template.postForObject(url, entity, String.class);
			logger.info(answer);
		} catch (Exception e) {
			new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_HOMEINFO);
		}

		ModelAndView mav = new ModelAndView();
		mav.setViewName("InsertCode");
		return mav;
	}

	// InsertCode.jsp에서 월패드 인증번호를 입력받아 ajax로 실행하는 부분,
	// 사용자가 입력한 인증번호의 유효성을 검증함.
	@RequestMapping(value = "/validate", method = RequestMethod.GET)
	public @ResponseBody Boolean validate(HttpSession httpSession,
			@RequestParam(value = "code", required = true) String wallpadCode) {
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("[STEP #3] /api/oauth2/auth");
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("*[parameters]");
		logger.info("*1. Input code: " + wallpadCode);

//		ValueOperations<String, Object> vop = redisTemplate.opsForValue();
//		if (vop.get(wallpadCode) == null) { // 없으면 null
//
//			return false;
//		}
//		httpSession.setAttribute("homeId", vop.get(wallpadCode));

		// 2019.05.02 DB저장에서 세션 저장 방식으로 변경
		logger.info(httpSession.getAttributeNames().toString());
		if (httpSession.getAttribute("wallpadCode") == null) {
			logger.info("null");
			return false;
		} else {
			if (!httpSession.getAttribute("wallpadCode").toString().equals(wallpadCode)) {
				logger.info(httpSession.getAttribute("wallpadCode").toString());
				return false;
			}
		}
		logger.info(httpSession.getAttribute("wallpadCode").toString());
		return true;
	}

	// 월패드 인증이 실행된 경우, 인증 코드를 발급해 리다이렉트하는 부분
	@RequestMapping(value = "/code/generate", method = RequestMethod.GET)
	public RedirectView validateCode(HttpSession httpSession) {
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("[STEP #4] /code/generate");
		logger.info("**************************************************************");
		logger.info("**************************************************************");
		logger.info("*[parameters]");
		logger.info("*1. home id: " + httpSession.getAttribute("homeInfo"));

		String code = "";
		String homeInfo = "";
		ValueOperations<String, Object> vop = redisTemplate.opsForValue();

		// 코드 발급
		homeInfo = httpSession.getAttribute("homeInfo").toString();
		logger.info("*[home info]");
		logger.info("*1. homeInfo: " + homeInfo);
		code = "";
		Random rnd = new Random();
		for (int i = 0; i < 10; i++) {
			code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65));
		}
		logger.info("*[code]");
		logger.info("*1. code: " + code);

		Duration d = Duration.ofSeconds(180);

		while (!vop.setIfAbsent(code, homeInfo, d)) {
			code = "";
			for (int i = 0; i < 10; i++) {
				code += String.valueOf((char) ((int) (rnd.nextInt(26)) + 65));
			}
		}

		String redirectUri = (String) httpSession.getAttribute("redirectUri");
		redirectUri += "?state=" + httpSession.getAttribute("state");
		redirectUri += "&code=" + code;

		RedirectView redirectView = new RedirectView();
		redirectView.setUrl(redirectUri);
		return redirectView;

	}

	// 토큰 발급하는 부분
	@RequestMapping(value = "/token", method = RequestMethod.POST, produces = "application/json")
	public @ResponseBody String Token(@RequestParam(value = "grant_type", required = true) String grantType,
			@RequestParam(value = "code", required = false) String code,
			@RequestParam(value = "refresh_token", required = false) String refreshToken, HttpSession httpSession,
			@RequestHeader HttpHeaders headers, HttpServletResponse response)
			throws NoSuchAlgorithmException, UnsupportedEncodingException, GeneralSecurityException {

		response.setStatus(HttpServletResponse.SC_OK);
		logger.info("[STEP #5] /api/oauth2/token");
		// TODO Code유효성 검사
		// TODO token 발급
		logger.info("grant_type: " + grantType);
		ValueOperations<String, Object> vop = redisTemplate.opsForValue();

		String newAccessToken = ""; // 생성해야 함.
		String newRefreshToken = ""; // 생성해야 함.
		String answer = null; // 단지서버에 토큰 관련요청 후 수신하는 응답
		Token token = new Token();
		if (grantType.equals("authorization_code")) {
			// TODO 코드 검사
			if (vop.get(code) == null) { // 인증코드가 없는 경우
				// 에러페이지 리턴
				throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_GRANT);

			} else {
				AES256Util aes = new AES256Util();
				String homeInfo = vop.get(code).toString();
				String jwt_homeInfo = homeInfo.split("/")[1] + "/" + homeInfo.split("/")[2];
				String encodedHomeInfo = aes.encrypt(jwt_homeInfo);
				logger.info("encodedHomeInfo =" + encodedHomeInfo);
				logger.info("encoded = " + encodedHomeInfo);

				String complexIp = homeInfo.split("/")[0];
				Encoder encoder = Base64.getEncoder();
				complexIp = new String(encoder.encode(complexIp.getBytes()));
				JWTInfo jwtinfo = new JWTInfo(complexIp, encodedHomeInfo, homeInfo.split("/")[3]);
				newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
				newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");

				token.setAccess_token(newAccessToken);
				token.setRefresh_token(newRefreshToken);

				String clientId = JWTUtils.getClientInfoFromToken(newAccessToken);
				ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
				String url = clientInfo.getEndPoint();

				answer = DanziAuthUtils.create(token, url);

			}
		} else if (grantType.equals("refresh_token")) {
			if (refreshToken != null) {
				// 1. 정상적으로 생성되었는지 확인
				if (!JWTUtils.validateToken(refreshToken, "refreshToken"))
					throw new ResponseStatusException(HttpStatus.BAD_REQUEST, ERROR.INVALID_REFRESHTOKEN);

				// client정보검사
				if (headers.get("Authorization").get(0) == null)
					throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);

//				if (!validateHeaderAuth(headers.get("Authorization").get(0)))
//					throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, ERROR.INVALID_CLIENT);

				// 단지에 갱신토큰 유효성 확인
				String clientId = JWTUtils.getClientInfoFromToken(refreshToken);
				ClientInfo clientInfo = clientInfoRepo.findByClientId(clientId);
				String url = clientInfo.getEndPoint();
				String isValidToken = DanziAuthUtils.get(url, refreshToken);
				logger.info(isValidToken);
				JSONObject isValidTokenJson = new JSONObject(isValidToken);
				logger.info(isValidTokenJson.toString());

				if (!isValidTokenJson.get("result").toString().equalsIgnoreCase("success")) {

					if (isValidTokenJson.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
						throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);

					response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
					logger.info(isValidTokenJson.get("result").toString());
					JSONObject error = new JSONObject();
					error.put("error", "invalid_grant");
					error.put("error_description", "invalid token");
					return error.toString();
				}

				// 유효한 토큰인 경우 토큰 생성
				JWTInfo jwtinfo = JWTUtils.getJWTInfoFromToken(refreshToken);
				newAccessToken = JWTUtils.generateToken(jwtinfo, "accessToken");
				newRefreshToken = JWTUtils.generateToken(jwtinfo, "refreshToken");
				token.setAccess_token(newAccessToken);
				token.setRefresh_token(newRefreshToken);

				// 단지서버에 갱신된 토큰 전달
				answer = DanziAuthUtils.refresh(url, token, refreshToken);
				logger.info(answer);

			}
		} else {
			throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
		}

		// 갱신된 토큰 전달에 대한 응답처리
		logger.info(answer);
		if (answer == null) {
			throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
		} else {
			JSONObject obj = new JSONObject(answer);
			logger.info(obj.get("result").toString());
			if (!obj.get("result").toString().equalsIgnoreCase("success")) {

				if (obj.getJSONObject("error").getInt("code") == -200) // 서버에러인경우
					throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);

				response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
				JSONObject error = new JSONObject();
				error.put("error", "invalid_grant");
				error.put("error_description", "invalid token");
				return error.toString();
			}
		}

		// class 만들어서 사용하는 것으로 변경해야함.
		logger.info(token.toString());

		// TODO 단지서버로 전송하고 정상 응답 받은 경우에만 정상리턴 -> 아닌경우에는 500
		// 토큰 생성인 경우와 갱신인 경우 구분해서 처리 create, refresh

		JSONObject tokenResult = new JSONObject();
		tokenResult.put("access_token", token.getAccess_token());
		tokenResult.put("refresh_token", token.getRefresh_token());
		tokenResult.put("token_type", token.getToken_type());
		tokenResult.put("expires_in", token.getExpires_in());

		return tokenResult.toString();

	}

	public boolean validateHeaderAuth(String authorization) {
		String splitStr[] = authorization.split(" ");

		// splitStr length 가 2가 아니면 유효하지 않은 authorization 값
		if (splitStr.length != 2)
			return false;

		// splitStr이 Basic이 아니어도 에러
		if (!splitStr[0].equals("Basic"))
			return false;

		// Base64 디코드
		String decodeStr = new String(Base64.getDecoder().decode(splitStr[1].trim()));

		splitStr = decodeStr.split(":");
		// splitStr length 가 2가 아니면 유효하지 않은 authorization 값
		if (splitStr.length != 2)
			return false;

		ClientInfo clientInfo = clientInfoRepo.findByClientId(splitStr[0]);
		if (clientInfo == null)
			return false;
		if (!clientInfo.getClientSecret().equals(splitStr[1]))
			return false;

		return true;
	}

	// ((Test)) Redirect Destination
	// 여기는 리다이렉트 받는 URl임 테스트용. Client를 대신함.
	@RequestMapping(value = "/redirect", method = RequestMethod.GET)
	public @ResponseBody String RedirectTester(HttpSession httpSession,
			@RequestParam(value = "code", required = true) String code) {
		logger.info("/api/oauth2/redirect");
		String redirectUri = "http://61.33.215.56:8003/api/oauth2/token?grant_type=authorization_code&code=" + code;
		RedirectView redirectView = new RedirectView();
		redirectView.setUrl(redirectUri);
		return code;

	}

	@RequestMapping(value = "/aws/healthcheck", method = RequestMethod.GET)
	public void healthCheck(HttpEntity<String> httpEntity) {

		return;
	}

}
