/*
 * Copyright (c) 2021 OBiBa. All rights reserved.
 *
 * This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.obiba.opal.rest.client.magma;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.obiba.magma.*;
import org.obiba.magma.support.AbstractDatasource;
import org.obiba.magma.support.Initialisables;
import org.obiba.magma.type.DateTimeType;
import org.obiba.opal.web.model.Magma.DatasourceDto;
import org.obiba.opal.web.model.Magma.TableDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;

public class RestDatasource extends AbstractDatasource {

  private static final Logger log = LoggerFactory.getLogger(RestDatasource.class);

  private final OpalJavaClient opalClient;

  private final URI datasourceURI;

  private Timestamps timestamps;

  public RestDatasource(String name, String opalUri, String remoteDatasource, String username, String password)
      throws URISyntaxException {
    this(name, new OpalJavaClient(opalUri, username, password), remoteDatasource);
  }

  public RestDatasource(String name, OpalJavaClient opalClient, String remoteDatasource) {
    super(name, "rest");
    this.opalClient = opalClient;
    datasourceURI = opalClient.newUri().segment("datasource", remoteDatasource).build();
  }

  @NotNull
  @Override
  public Timestamps getTimestamps() {
    if (timestamps == null) {
      refresh();
    }

    return  timestamps;
  }

  @Override
  public Set<ValueTable> getValueTables() {
    refresh();

    return super.getValueTables();
  }

  @Override
  public void initialise() {
    try {
      super.initialise();
    } catch(RuntimeException e) {
      if(e.getCause() instanceof ConnectException) {
        log.error("Failed connecting to Opal server: {}", e.getMessage(), e);
      } else {
        log.error("Unexpected error while communicating with Opal server: {}", e.getMessage(), e);
      }
      throw new MagmaRuntimeException(e.getMessage(), e);
    }
  }

  @Override
  protected void onDispose() {
    super.onDispose();
    try {
      opalClient.close();
    } catch(Exception ignore) {
      // ignore
    }
  }

  @Override
  protected Set<String> getValueTableNames() {
    DatasourceDto d = opalClient.getResource(DatasourceDto.class, datasourceURI, DatasourceDto.newBuilder());
    return ImmutableSet.copyOf(d.getTableList());
  }

  @NotNull
  @Override
  public ValueTableWriter createWriter(@NotNull String tableName, @NotNull String entityType) {
    if(!hasValueTable(tableName)) {
      URI tableUri = newReference("tables");
      try (CloseableHttpResponse response = getOpalClient()
          .post(tableUri, TableDto.newBuilder().setName(tableName).setEntityType(entityType).build())) {
        if(response.getCode() != HttpStatus.SC_CREATED) {
          throw new RuntimeException("cannot create table " + response.getReasonPhrase());
        }
        addValueTable(tableName);
        EntityUtils.consume(response.getEntity());
      } catch(IOException e) {
        throw new RuntimeException(e);
      }
    }
    return new RestValueTableWriter((RestValueTable) getValueTable(tableName));
  }

  @Override
  protected ValueTable initialiseValueTable(String tableName) {
    return new RestValueTable(this, opalClient
        .getResource(TableDto.class, newUri("table", tableName).query("counts", "false").build(),
            TableDto.newBuilder()));
  }

  private synchronized void refresh() {
    try {
      DatasourceDto d = opalClient.getResource(DatasourceDto.class, datasourceURI, DatasourceDto.newBuilder());
      Value currentTimestamp = DateTimeType.get().valueOf(d.getTimestamps().getLastUpdate());

      if(timestamps != null && timestamps.getLastUpdate().equals(currentTimestamp)) {
        log.debug("RestDatasource is up to date. Skipping refresh.");
        return; //Cache is up to date
      }

      log.debug("Refreshing data source value tables.");

      Set<String> cachedTableNames = ImmutableSet
          .copyOf(Iterables.transform(super.getValueTables(), new Function<ValueTable, String>() {
            @Override
            public String apply(ValueTable input) {
              return input.getName();
            }
          }));

      timestamps = new TimestampsBean(DateTimeType.get().valueOf(d.getTimestamps().getCreated()),
          DateTimeType.get().valueOf(d.getTimestamps().getLastUpdate()));
      Set<String> currentTables = ImmutableSet.copyOf(d.getTableList());

      for(String tableName : cachedTableNames) {
        ValueTable table = getCachedValueTable(tableName);
        removeValueTable(table);
      }

      for(String tableName : currentTables) {
        addValueTable(tableName);
      }
    } catch (Exception e) {
      log.error("Failed refreshing value tables from Opal server: {}", e.getMessage(), e);

      if(e.getCause() instanceof ConnectException) {
        log.error("Failed connecting to Opal server: {}", e.getMessage(), e);
      } else {
        log.error("Unexpected error while communicating with Opal server: {}", e.getMessage(), e);
      }

      throw new MagmaRuntimeException(e.getMessage(), e);
    }
  }

  private ValueTable getCachedValueTable(final String tableName) {
    return Iterables.find(super.getValueTables(), new Predicate<ValueTable>() {
      @Override
      public boolean apply(ValueTable input) {
        return tableName.equals(input.getName());
      }
    });
  }

  private void addValueTable(String table) {
    ValueTable vt = initialiseValueTable(table);
    Initialisables.initialise(vt);
    addValueTable(vt);
  }

  OpalJavaClient getOpalClient() {
    return opalClient;
  }

  URI newReference(String... segments) {
    return uriBuilder().segment(segments).build();
  }

  UriBuilder newUri(String... segments) {
    return uriBuilder().segment(segments);
  }

  URI buildURI(URI root, String... segments) {
    return uriBuilder(root).segment(segments).build();
  }

  UriBuilder uriBuilder() {
    return uriBuilder(datasourceURI);
  }

  UriBuilder uriBuilder(URI root) {
    return opalClient.newUri(root);
  }

}
