package ru.micord.ervu.security.esia.service;

import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContext;
import ru.micord.ervu.kafka.model.Document;
import ru.micord.ervu.kafka.model.Person;
import ru.micord.ervu.kafka.model.Response;
import ru.micord.ervu.kafka.service.ReplyingKafkaService;
import ru.micord.ervu.security.esia.token.TokensStore;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import ru.micord.ervu.security.esia.model.FormUrlencoded;
import ru.micord.ervu.security.esia.model.EsiaAccessToken;
import ru.micord.ervu.security.esia.model.EsiaTokenResponse;
import ru.micord.ervu.security.esia.model.PersonModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import ru.micord.ervu.security.webbpm.jwt.JwtAuthentication;
import ru.micord.ervu.security.webbpm.jwt.helper.SecurityHelper;
import ru.micord.ervu.security.webbpm.jwt.service.JwtTokenService;
import ru.micord.ervu.security.webbpm.jwt.model.Token;

/**
 * @author Eduard Tihomirov
 */
@Service
public class EsiaAuthService {

  @Autowired
  private ObjectMapper objectMapper;

  @Autowired
  private EsiaConfig esiaConfig;

  @Autowired
  private JwtTokenService jwtTokenService;

  @Autowired
  @Qualifier("person")
  private ReplyingKafkaService<String, String> replyingKafkaService;

  @Autowired
  private PersonalDataService personalDataService;

  @Autowired
  private SecurityHelper securityHelper;
  @Autowired
  private AuthenticationManager authenticationManager;

  @Value("${ervu.kafka.reply.topic}")
  private String requestReplyTopic;

  @Value("${ervu.kafka.request.topic}")
  private String requestTopic;

  public String generateAuthCodeUrl() {
    try {
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String state = UUID.randomUUID().toString();
      String redirectUrl = esiaConfig.getRedirectUrl();
      String redirectUrlEncoded = redirectUrl.replaceAll(":", "%3A")
          .replaceAll("/", "%2F");
      String scope = esiaConfig.getEsiaScopes();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", scope);
      parameters.put("timestamp", timestamp);
      parameters.put("state", state);
      parameters.put("redirect_uri", esiaConfig.getRedirectUrl());

      String clientSecret = signMap(parameters);

      String responseType = "code";

      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaCodeUrl();

      URL url = new URL(authUrl);
      Map<String, String> params = mapOf("scope", scope,
          "timestamp", urlEncode(timestamp),
          "state", state,
          "client_id", clientId,
          "client_secret", clientSecret,
          "response_type", responseType,
          "redirect_uri", redirectUrlEncoded,
          "client_certificate_hash", esiaConfig.getClientCertHash());

      return makeRequest(url, params);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private static Map<String, String> mapOf(String... params) {
    if (params.length % 2 != 0) {
      throw new IllegalArgumentException("Invalid params count");
    }
    Map<String, String> map = new LinkedHashMap<>(params.length / 2);
    for (int i = 0; i < params.length / 2; i++) {
      map.put(params[i * 2], params[i * 2 + 1]);
    }
    return map;
  }

  private static String urlEncode(String s) {
    return URLEncoder.encode(s, StandardCharsets.UTF_8)
        .replace("+", "%20");
  }

  private static String makeRequest(URL url, Map<String, String> params) {
    StringBuilder uriBuilder = new StringBuilder(url.toString());
    uriBuilder.append('?');
    for (Map.Entry<String, String> node : params.entrySet()) {
      uriBuilder.append(node.getKey()).append('=').append(node.getValue()).append("&");
    }
    return uriBuilder.toString();
  }

  public ResponseEntity<?> getEsiaTokensByCode(String esiaAuthCode, HttpServletRequest request, HttpServletResponse response) {
    try {
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String state = UUID.randomUUID().toString();
      String redirectUrl = esiaConfig.getRedirectUrl();
      String scope = esiaConfig.getEsiaScopes();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", scope);
      parameters.put("timestamp", timestamp);
      parameters.put("state", state);
      parameters.put("redirect_uri", redirectUrl);
      parameters.put("code", esiaAuthCode);

      String clientSecret = signMap(parameters);
      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
      String postBody = new FormUrlencoded()
          .setParameter("client_id", clientId)
          .setParameter("code", esiaAuthCode)
          .setParameter("grant_type", "authorization_code")
          .setParameter("client_secret", clientSecret)
          .setParameter("state", state)
          .setParameter("redirect_uri", redirectUrl)
          .setParameter("scope", scope)
          .setParameter("timestamp", timestamp)
          .setParameter("token_type", "Bearer")
          .setParameter("client_certificate_hash", esiaConfig.getClientCertHash())
          .toFormUrlencodedString();
      HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl))
          .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
          .POST(HttpRequest.BodyPublishers.ofString(postBody))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      HttpResponse<String> postResp = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(postReq, HttpResponse.BodyHandlers.ofString());
      String responseString = postResp.body();
      EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString,
          EsiaTokenResponse.class
      );

      if (tokenResponse == null || tokenResponse.getError() != null) {
        String errMsg =
            tokenResponse != null ? tokenResponse.getError_description() : "response is empty";
        throw new IllegalStateException("Esia response error. " + errMsg);
      }
      String esiaAccessTokenStr = tokenResponse.getAccess_token();
      String esiaRefreshTokenStr = tokenResponse.getRefresh_token();
      EsiaAccessToken esiaAccessToken = personalDataService.readToken(esiaAccessTokenStr);
      String prnOid = esiaAccessToken.getSbj_id();
      Long expiresIn = tokenResponse.getExpires_in();
      TokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
      TokensStore.addRefreshToken(prnOid, esiaRefreshTokenStr, expiresIn);
      Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr);
      Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId());
      int expiry = tokenResponse.getExpires_in().intValue();
      Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry);
      response.addCookie(accessCookie);
      UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
          new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      JwtAuthentication jwtAuthentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
          esiaAccessToken.getSbj_id(), token.getValue());
      authenticationManager.authenticate(jwtAuthentication);
      context.setAuthentication(jwtAuthentication);
      SecurityContextHolder.setContext(context);
      Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry);
      response.addCookie(authMarkerCookie);
      return ResponseEntity.ok("Authentication successful");
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void getEsiaTokensByRefreshToken(HttpServletRequest request, HttpServletResponse response) {
    try {
      String refreshToken = jwtTokenService.getRefreshToken(request);
      String clientId = esiaConfig.getClientId();
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss xx");
      ZonedDateTime dt = ZonedDateTime.now();
      String timestamp = dt.format(formatter);
      String state = UUID.randomUUID().toString();
      String redirectUrl = esiaConfig.getRedirectUrl();

      Map<String, String> parameters = new LinkedHashMap<String, String>();
      parameters.put("client_id", clientId);
      parameters.put("scope", esiaConfig.getEsiaScopes());
      parameters.put("timestamp", timestamp);
      parameters.put("state", state);
      parameters.put("redirect_uri", esiaConfig.getRedirectUrl());
      parameters.put("refresh_token", refreshToken);

      String clientSecret = signMap(parameters);
      String authUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaTokenUrl();
      String postBody = new FormUrlencoded()
          .setParameter("client_id", clientId)
          .setParameter("refresh_token", refreshToken)
          .setParameter("grant_type", "refresh_token")
          .setParameter("client_secret", clientSecret)
          .setParameter("state", state)
          .setParameter("redirect_uri", redirectUrl)
          .setParameter("scope", esiaConfig.getEsiaScopes())
          .setParameter("timestamp", timestamp)
          .setParameter("token_type", "Bearer")
          .setParameter("client_certificate_hash", esiaConfig.getClientCertHash())
          .toFormUrlencodedString();
      HttpRequest postReq = HttpRequest.newBuilder(URI.create(authUrl))
          .header(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
          .POST(HttpRequest.BodyPublishers.ofString(postBody))
          .timeout(Duration.ofSeconds(esiaConfig.getRequestTimeout()))
          .build();
      HttpResponse<String> postResp = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(postReq, HttpResponse.BodyHandlers.ofString());
      String responseString = postResp.body();
      EsiaTokenResponse tokenResponse = objectMapper.readValue(responseString, EsiaTokenResponse.class);
      if (tokenResponse == null || tokenResponse.getError() != null) {
        String errMsg =
            tokenResponse != null ? tokenResponse.getError_description() : "response is empty";
        throw new IllegalStateException("Esia response error. " + errMsg);
      }
      String esiaAccessTokenStr = tokenResponse.getAccess_token();
      String esiaNewRefreshTokenStr = tokenResponse.getRefresh_token();
      EsiaAccessToken esiaAccessToken = personalDataService.readToken(esiaAccessTokenStr);
      String prnOid = esiaAccessToken.getSbj_id();
      Long expiresIn = tokenResponse.getExpires_in();
      TokensStore.addAccessToken(prnOid, esiaAccessTokenStr, expiresIn);
      TokensStore.addRefreshToken(prnOid, esiaNewRefreshTokenStr, expiresIn);
      Response ervuIdResponse = getErvuIdResponse(esiaAccessTokenStr);
      Token token = jwtTokenService.createAccessToken(esiaAccessToken.getSbj_id(), expiresIn, ervuIdResponse.getErvuId());
      int expiry = tokenResponse.getExpires_in().intValue();
      Cookie accessCookie = securityHelper.createAccessCookie(token.getValue(), expiry);
      response.addCookie(accessCookie);
      UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
          new UsernamePasswordAuthenticationToken(token.getUserAccountId(), null);
      SecurityContext context = SecurityContextHolder.createEmptyContext();
      JwtAuthentication jwtAuthentication = new JwtAuthentication(usernamePasswordAuthenticationToken,
          esiaAccessToken.getSbj_id(), token.getValue());
      authenticationManager.authenticate(jwtAuthentication);
      context.setAuthentication(jwtAuthentication);
      SecurityContextHolder.setContext(context);
      Cookie authMarkerCookie = securityHelper.createAuthMarkerCookie("true", expiry);
      response.addCookie(authMarkerCookie);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private String signMap(Map<String, String> paramsToSign) {
    try {
      StringBuilder toSign = new StringBuilder();
      for (String s : paramsToSign.values()) {
        toSign.append(s);
      }
      String requestBody = toSign.toString();

      HttpRequest request = HttpRequest.newBuilder()
          .uri(URI.create(esiaConfig.getSignUrl()))
          .header("Content-Type", "text/plain")
          .POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
          .build();
      HttpResponse<String> response = HttpClient.newBuilder()
          .connectTimeout(Duration.ofSeconds(esiaConfig.getConnectionTimeout()))
          .build()
          .send(request, HttpResponse.BodyHandlers.ofString());
      errorHandler(response);
      return response.body();

    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private void errorHandler(HttpResponse<?> httpResponse) {
    if (httpResponse.statusCode() != 200) {
      throw new RuntimeException(httpResponse.statusCode() + " " + httpResponse.body());
    }
  }

  public String logout(HttpServletRequest request, HttpServletResponse response) {
    try {
      securityHelper.clearAccessCookies(response);
      String userId = jwtTokenService.getUserAccountId(request);
      TokensStore.removeAccessToken(userId);
      TokensStore.removeRefreshToken(userId);
      String logoutUrl = esiaConfig.getEsiaBaseUri() + esiaConfig.getEsiaLogoutUrl();
      String redirectUrl = esiaConfig.getRedirectUrl();
      URL url = new URL(logoutUrl);
      Map<String, String> params = mapOf(
          "client_id", esiaConfig.getClientId(),
          "redirect_url", redirectUrl);
      return makeRequest(url, params);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public Response getErvuIdResponse(String accessToken) {
    try {
      PersonModel personModel = personalDataService.getPersonModel(accessToken);
      Person person = copyToPerson(personModel);
      String kafkaResponse = replyingKafkaService.sendMessageAndGetReply(requestTopic,
          requestReplyTopic, objectMapper.writeValueAsString(person)
      );
      return objectMapper.readValue(kafkaResponse, Response.class);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private Person copyToPerson(PersonModel personModel) {
    Person person = new Person();
    person.setBirthDate(personModel.getBirthDate());
    person.setFirstName(personModel.getFirstName());
    person.setLastName(personModel.getLastName());
    person.setSnils(personModel.getSnils());
    person.setMiddleName(personModel.getMiddleName());
    Document document = new Document();
    document.setNumber(personModel.getPassportModel().getNumber());
    document.setSeries(personModel.getPassportModel().getSeries());
    document.setIssueDate(personModel.getPassportModel().getIssueDate());
    person.setDocument(document);
    return person;
  }
}
