package ervu.client.fileupload;

import java.io.IOException;
import java.io.InputStream;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.PasswordAuthentication;
import java.net.URI;
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.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

import javax.annotation.PostConstruct;

import com.github.sardine.DavResource;
import com.github.sardine.Sardine;
import com.github.sardine.SardineFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import ervu.exception.WebDavException;
import ervu.model.webdav.Server;
import org.apache.http.client.ClientProtocolException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import ru.micord.ervu.security.esia.config.EsiaConfig;
import ru.micord.ervu.util.UrlUtils;

/**
 * @author Alexandr Shalaginov
 */
@Component
public class WebDavClient {
  private static final Logger LOGGER = LoggerFactory.getLogger(WebDavClient.class);

  @Value("${webdav.urls}")
  private String[] urls;
  @Value("${webdav.username}")
  private String username;
  @Value("${webdav.password}")
  private String password;
  @Value("${webdav.bad_servers.cache.expire.seconds:120}")
  private long cacheExpireSec;
  @Value("${request.timeout:20}")
  private long requestTimeout;
  @Value("${connection.timeout:10}")
  private long connectionTimeout;

  private List<Server> servers;
  private LoadingCache<String, String> badServersCache;
  private final AtomicInteger counter = new AtomicInteger(0);

  @PostConstruct
  public void init() {
    CacheLoader<String, String> loader = new CacheLoader<>() {
      @Override
      public String load(String key) {
        return key;
      }
    };
    badServersCache = CacheBuilder.newBuilder()
        .expireAfterAccess(cacheExpireSec, TimeUnit.SECONDS)
        .build(loader);
    LOGGER.info("WebDAV urls: {}", Arrays.asList(urls));
    servers = Arrays.stream(urls)
        .map(url -> new Server(url, ZonedDateTime.now().toInstant().toEpochMilli()))
        .toList();
  }

  @Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
  public String uploadFile(MultipartFile multipartFile) {
    String fileName = getNewFilename(multipartFile.getOriginalFilename());
    Sardine sardine = initClient(username, password);

    try {
      return putAndGetUrl(multipartFile.getBytes(), fileName, sardine);
    }
    catch (IOException e) {
      throw new WebDavException("Failed to put file into WebDAV", e);
    }
    finally {
      try {
        sardine.shutdown();
      }
      catch (IOException e) {
        LOGGER.error("Failed to shutdown WebDAV client", e);
      }
    }
  }

  private String getNewFilename(String filename) {
    int lastIndexOf = filename.lastIndexOf(".");
    String fileExtension = lastIndexOf == -1 ? "" : filename.substring(lastIndexOf);
    return UUID.randomUUID() + fileExtension;
  }

  public String putAndGetUrl(byte[] fileBytes, String fileName, Sardine client) throws IOException {
    if (badServersCache.size() == urls.length) {
      return null;
    }
    Server server;

    if (urls.length == 1) {
      server = servers.get(0);
    }
    else {
      Predicate<Server> isNotBad = s -> badServersCache.getIfPresent(s.getUrl()) == null;
      server = servers.stream()
          .filter(isNotBad.and(s -> servers.indexOf(s) == index()))
          .findFirst()
          .orElseGet(() -> servers.stream()
              .filter(isNotBad)
              .min(Comparator.comparing(Server::getLastCallTime))
              .orElse(null));
    }

    if (server == null) {
      return null;
    }
    boolean isBad = false;
    String serverUrl = server.getUrl();
    String fileUploadUrl = serverUrl + "/" + fileName;

    try {
      client.put(fileUploadUrl, fileBytes);
      server.setLastCallTime(System.currentTimeMillis());
    }
    catch (ConnectException | ClientProtocolException ignore) {
      isBad = true;
    }

    if (isBad) {
      badServersCache.getUnchecked(serverUrl);
      return putAndGetUrl(fileBytes, fileName, client);
    }

    return fileUploadUrl;
  }

  private int index() {
    counter.compareAndSet(Integer.MAX_VALUE, 0);
    return counter.getAndIncrement() % urls.length;
  }

  @Retryable(value = {IOException.class, InterruptedException.class},
      backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
  public ResponseEntity<Resource> webDavDownloadFile(String url) {
    try {
      HttpClient httpClient = HttpClient.newBuilder()
          .authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
              return new PasswordAuthentication(username, password.toCharArray());
            }
          })
          .connectTimeout(Duration.ofSeconds(connectionTimeout))
          .build();

      HttpRequest httpRequest = HttpRequest.newBuilder().uri(URI.create(url))
          .GET()
          .timeout(Duration.ofSeconds(requestTimeout))
          .build();

      HttpResponse<InputStream> response = httpClient.send(httpRequest,
          HttpResponse.BodyHandlers.ofInputStream()
      );
      if (response.statusCode() == 200) {
        long contentLength = response.headers()
            .firstValueAsLong(HttpHeaders.CONTENT_LENGTH)
            .orElse(0);
        InputStreamResource resource = new InputStreamResource(response.body());
        String encodedFilename = URLEncoder.encode(UrlUtils.extractFileNameFromUrl(url), StandardCharsets.UTF_8);
        return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename*=UTF-8''" + encodedFilename
            )
            .contentLength(contentLength)
            .contentType(MediaType.APPLICATION_OCTET_STREAM)
            .body(resource);
      }
      else {
        LOGGER.error("Failed to download file: HTTP status code {}", response.statusCode());
        return ResponseEntity.status(response.statusCode()).build();
      }
    }
    catch (IOException | InterruptedException e) {
      throw new WebDavException(e);
    }
  }


  @Retryable(value = {IOException.class}, backoff = @Backoff(delayExpression = "${webdav.retry.delay:500}"))
  public void deleteFilesOlderThan(long seconds, String url, String... extensions) throws IOException {
    Sardine sardine = initClient(username, password);

    try {
      List<DavResource> resources = sardine.list(url);
      resources.stream().filter(getPredicate(extensions))
          .forEach(davResource -> {
            long age = System.currentTimeMillis() - davResource.getModified().getTime();

            if (age > seconds * 1000) {
              try {
                sardine.delete(url + davResource.getPath());
              }
              catch (IOException e) {
                LOGGER.error("Failed to delete file {}", davResource.getName(), e);
              }
            }
          });
    }
    finally {
      try {
        sardine.shutdown();
      }
      catch (IOException e) {
        LOGGER.error("Failed to shutdown WebDAV client", e);
      }
    }
  }

  private Sardine initClient(String username, String password) {
    return SardineFactory.begin(username, password);
  }

  private Predicate<DavResource> getPredicate(String... extensions) {
    if (extensions.length == 0) {
      return davResource -> true;
    }

    return davResource -> {
      AtomicBoolean condition = new AtomicBoolean(false);
      Arrays.stream(extensions).forEach(extension -> condition.set(
          condition.get() || davResource.getName().endsWith(extension)));
      return condition.get();
    };
  }
}
