package io.aiven.klaw.service;

import static io.aiven.klaw.error.KlawErrorMessages.CLUSTER_API_ERR_117;

import io.aiven.klaw.error.KlawException;
import io.aiven.klaw.model.ApiResponse;
import io.aiven.klaw.service.interfaces.HAMessagingServiceI;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.net.InetAddress;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import javax.crypto.spec.SecretKeySpec;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;

@Service
@Slf4j
public class HARestMessagingService implements HAMessagingServiceI {

  public static final String URL_SEPERATOR = "/";
  public static final String TENANT = "tenant";
  public static final String ID = "id";
  public static final String BEARER = "Bearer ";
  public static final String AUTHORIZATION = "Authorization";
  public static final String ROLES = "Roles";
  public static final String NAME = "name";
  public static final String ENTITY_TYPE = "entityType";
  public static final String CACHE = "cache";
  public static final String CACHE_ADMIN = "CACHE_ADMIN";
  public static final String APP_2_APP = "App2App";
  @Autowired Environment environment;

  private List<String> clusterUrls;

  private RestTemplate rest;
  private static volatile HttpComponentsClientHttpRequestFactory requestFactory = null;

  private static Map<String, String> baseUrlsMap;

  @Value("${klaw.core.app2app.base64.secret:#{''}}")
  private String app2AppApiKey;

  @Value("${klaw.core.app2app.username:KlawApp2App}")
  private String apiUser;

  @Value("${klaw.uiapi.servers:server1,server2}")
  private String clusterUrlsAsString;

  public static final String BASE_URL_ADDRESS = "BASE_URL_ADDRESS";
  public static final String BASE_URL_NAME = "BASE_URL_NAME";

  private Map<String, String> getBaseIpUrlFromEnvironment() {
    if (baseUrlsMap != null && !baseUrlsMap.isEmpty()) {
      return baseUrlsMap;
    } else {
      baseUrlsMap = new HashMap<>();
      String hostAddress = InetAddress.getLoopbackAddress().getHostAddress();
      String hostName = InetAddress.getLoopbackAddress().getHostName();
      int port = Integer.parseInt(Objects.requireNonNull(environment.getProperty("server.port")));

      baseUrlsMap.put(BASE_URL_ADDRESS, hostAddress + ":" + port);
      baseUrlsMap.put(BASE_URL_NAME, hostName + ":" + port);
      return baseUrlsMap;
    }
  }

  public boolean isLocalServerUrl(String basePath) {
    Map<String, String> baseUrlsFromEnv = getBaseIpUrlFromEnvironment();
    if (baseUrlsFromEnv != null && !baseUrlsFromEnv.isEmpty()) {
      if (existsInMap(basePath, baseUrlsFromEnv, BASE_URL_ADDRESS)) {
        return true;
      }
      return existsInMap(basePath, baseUrlsFromEnv, BASE_URL_NAME);
    }
    return false;
  }

  private static boolean existsInMap(
      String basePath, Map<String, String> baseUrlsFromEnv, String key) {
    return baseUrlsFromEnv.containsKey(key) && basePath.contains(baseUrlsFromEnv.get(key));
  }

  public RestTemplate getRestTemplate() {
    if (clusterUrlsAsString.toLowerCase().startsWith("https")) {
      if (requestFactory == null) {
        synchronized (HARestMessagingService.class) {
          if (requestFactory == null) {
            requestFactory = ClusterApiService.requestFactory;
            if (requestFactory == null) {
              throw new RuntimeException(
                  "Connectivity between Klaw core and Klaw cluster should be established first");
            }
          }
        }
      }
      return new RestTemplate(requestFactory);
    } else {
      return new RestTemplate();
    }
  }

  public List<String> getHAClusterUrls() {
    if (clusterUrlsAsString != null && !clusterUrlsAsString.isEmpty()) {
      return Arrays.stream(clusterUrlsAsString.split(","))
          .filter(serverUrl -> !isLocalServerUrl(serverUrl))
          .toList();
    } else {
      return new ArrayList<>();
    }
  }

  public HttpHeaders createHeaders() throws KlawException {
    HttpHeaders httpHeaders = new HttpHeaders();
    String authHeader = BEARER + generateToken(apiUser);
    httpHeaders.set(AUTHORIZATION, authHeader);
    httpHeaders.setContentType(MediaType.APPLICATION_JSON);

    return httpHeaders;
  }

  private String generateToken(String username) throws KlawException {
    if (app2AppApiKey.isBlank()) {
      log.error(CLUSTER_API_ERR_117);
      throw new KlawException(CLUSTER_API_ERR_117);
    }

    Key hmacKey =
        new SecretKeySpec(
            Base64.decodeBase64(app2AppApiKey), SignatureAlgorithm.HS256.getJcaName());
    Instant now = Instant.now();

    return Jwts.builder()
        .claim(NAME, username)
        .claim(ROLES, List.of(CACHE_ADMIN, APP_2_APP))
        .subject(username)
        .id(UUID.randomUUID().toString())
        .issuedAt(Date.from(now))
        .expiration(Date.from(now.plus(3L, ChronoUnit.MINUTES))) // expiry in 3 minutes
        .signWith(hmacKey)
        .compact();
  }

  @Override
  public void sendUpdate(String entityType, int tenantId, Object entry) {
    if (isLazyLoaded()) {
      throw new RuntimeException("Unable to load High Availability Cache");
    }

    for (String url : clusterUrls) {
      try {
        HttpEntity<Object> request = new HttpEntity<>(entry, createHeaders());
        rest.postForObject(getUrl(url, entityType, tenantId, null), request, ApiResponse.class);

      } catch (KlawException | RestClientException clientException) {
        log.error(
            "Exception while sending HA updates to another instance {} in the cluster.",
            url,
            clientException);
      }
    }
  }

  @Override
  public void sendRemove(String entityType, int tenantId, int id) {
    if (isLazyLoaded()) {
      throw new RuntimeException("Unable to load High Availability Cache");
    }

    for (String url : clusterUrls) {
      try {
        rest.exchange(
            getUrl(url, entityType, tenantId, id),
            HttpMethod.DELETE,
            new HttpEntity<>(createHeaders()),
            Void.class);
      } catch (RestClientException | KlawException clientException) {
        log.error(
            "Exception while sending HA updates to another instance {} in the cluster.",
            url,
            clientException);
      }
    }
  }

  private boolean isLazyLoaded() {

    if (rest == null || clusterUrls == null) {
      this.rest = getRestTemplate();
      this.clusterUrls = getHAClusterUrls();
    }
    return (rest == null && clusterUrls == null);
  }

  private String getUrl(String url, String entityType, int tenantId, Integer id) {
    if (url.endsWith(URL_SEPERATOR)) {
      url = url.substring(0, url.length() - 1);
    }
    if (id != null) {
      return String.join(
          URL_SEPERATOR,
          url,
          CACHE,
          TENANT,
          Integer.toString(tenantId),
          ENTITY_TYPE,
          entityType,
          ID,
          Integer.toString(id));
    } else {
      return String.join(
          URL_SEPERATOR, url, CACHE, TENANT, Integer.toString(tenantId), ENTITY_TYPE, entityType);
    }
  }
}
