package nxm.minio.lib;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.MinioClient.Builder;
import io.minio.PutObjectArgs;
import io.minio.StatObjectArgs;
import nxm.sys.inc.Debug;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MapKBT;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.IOResource;
import nxm.sys.lib.JsonUtilities;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.ReflectUtil;
import nxm.sys.lib.Shell;

/**
 *  URL are of the format [https://]host/bucket/key
 *  examples minus bucket and key are:
 *    https://s3.amazonaws.com
 *    https://play.min.io:9000
 *    https://play.min.io
 *    play.min.io
 *    localhost
 *
 */
@ProvisionalUseOnly("Prototype only - under development")
public class MinIOResource extends IOResource {
  private MinioClient minioClient = null;
  InputStream stream = null;
  private String endpoint = "";
  private String bucket = "";
  private String key = "";
  private String accessKey = "";
  private String secretKey = "";
  private URL    url;
  private final String PROTOCOL_ENDING = "://";
  private final int    PR_END_LTH = PROTOCOL_ENDING.length();
  private final String resourceType = "s3:";
  private final int    resTypeLen = resourceType.length();
  private byte[] writeBuffer = null;
  private int writeBuffStarterSize = 40000;
  /** The last read offset */
  private long lastReadOffset=-1;
  private int numWriteTotal = 0;
  private static Map<String,MinioClient> minioClients = new HashMap<>();;
  private static List<MapKBT.AbstractMapKBT> jsonConfig = null;
  private static MapKBT aliases = null;
  private static String pathJsonConfig = System.getProperty("user.home")+"/.mc/config.json";
  /** Reference minio MinioClient.Builder class */
  static final Class<?> IO_MINIO_MINIOCLIENT_BUILDER_CLASS =
      ReflectUtil.getClassForName("io.minio.MinioClient$Builder");
  /** MinioClient.Builder endpoint method */
  static final Method MINIOCLIENT_BUILDER_ENDPOINT =
      ReflectUtil.getMethodOfClass(IO_MINIO_MINIOCLIENT_BUILDER_CLASS, "endpoint", String.class);

  static {
    importJsonConfig();
  }

  /** Default Constructor */
  public MinIOResource() {}

  /** MinIOResource Constructor
   *  @param url URL
   */
  public MinIOResource(String url) {
    setURL(url);
  }

  public void setURL (String str) {
    if (str.startsWith(resourceType)) str = str.substring(resTypeLen);
    String prev_urlstr = getURL();
    if (!str.equals(prev_urlstr)) { // do below only if different than previous urlstr
      int idxPrEnd = str.indexOf(PROTOCOL_ENDING);
      int startHost = (idxPrEnd>0) ? idxPrEnd + PR_END_LTH : 0; // if no prototcol, host (or alias is at beginnning)
      String hostBucketKey = str.substring(startHost);
      int hostBucketSep = hostBucketKey.indexOf('/');
      boolean justHost = hostBucketSep <= 0;
      String endPointOrAlias  = (justHost) ? str : str.substring(0, hostBucketSep+startHost);
      if (aliases.containsKey(endPointOrAlias)) {
        MapKBT entry = aliases.getMapKBT(endPointOrAlias);
        this.endpoint = entry.getS("url");
        this.accessKey = entry.getS("accessKey");
        this.secretKey = entry.getS("secretKey");
        super.setURL(this.endpoint + str.substring(endPointOrAlias.length()));
      }
      else {
        this.endpoint = (justHost) ? str : str.substring(0, hostBucketSep+startHost);
        super.setURL(str);
      }
      if (justHost) {
        this.bucket = "";
        this.key = "";
      }
      else {
        String bucketKey = hostBucketKey.substring(hostBucketSep + 1);
        int bucketKeySep = bucketKey.indexOf('/');
        boolean noKey = bucketKeySep <= 0;
        this.bucket = (noKey) ? bucketKey : bucketKey.substring(0, bucketKeySep);
        this.key    = (noKey) ? "" : bucketKey.substring(bucketKeySep + 1);
      }
      try {
        url = (new URI(super.getURL())).toURL();
      }
      catch (Exception e) { throw new MidasException("MinIO URL: " + str, e); }
    }
  }

  public boolean open () {
    isOpen = false;
    minioClient = getMinioClient();
    try {
      minioClient.listBuckets();

      // add other stuff here

      isOpen = true;
    } catch (Exception e) {
      // TODO Auto-generated catch block
      if (M.debug(Debug.TRACE)) M.warning("MinIOResource - exception during open()" + e);
    }

    return isOpen;
  }

  /** Creates or gets the current MinioClient for the endpoint and credentials
   *
   *  @return the current MinioClient
   */
  @InternalUseOnly
  public MinioClient getMinioClient() {
    MinioClient client = null;
    if (minioClients.containsKey(endpoint)) {
      client = minioClients.get(endpoint);
    }
    else {
      try {
        client = ((Builder)MINIOCLIENT_BUILDER_ENDPOINT.invoke(MinioClient.builder(), endpoint)).
            credentials(accessKey, secretKey).build();
        minioClients.put(endpoint,client);
      }
      catch (Exception e) {
        throw new MidasException("MinioResource reflection code failed, notify NeXtMidas team",e);
      }
    }
    return client;
  }

  public boolean close () {
    // handle if was read
    if (stream!=null) try {
      stream.close();
    }
    catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    finally {
      stream = null;
    }
    if ((writeBuffer!=null) && (numWriteTotal!=0)) {
      ByteArrayInputStream bais = null;
      try {
        boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        if (!bucketExists) minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
        // now do the real thing
        bais = new ByteArrayInputStream(writeBuffer,0,numWriteTotal);

        minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(key).stream(
            bais,bais.available(),-1).build());
        M.info("MinIO successful write");
      }
      catch (Exception e) {
        M.warnStackTrace("MinIO write failed", e);
      }
      finally {
        if (bais!=null) try {
          bais.close();
        } catch (IOException e) {
          // TODO Auto-generated catch block
          e.printStackTrace();
        }
        offset = 0;
        writeBuffer = null;
      }
    }

    minioClient = null;
    isOpen = false;
    return !isOpen;
  }

  /** read data from resource */
  public int read (byte[] buf, int boff, int bytes) {
    if (!isOpen) open();
    int off=boff;
    try {
      boolean newOffset = lastReadOffset!=offset;
      if (newOffset) {
        if (stream!=null) stream.close();
        stream = null;
      }

      if (stream==null) {
        lastReadOffset = offset;
        stream = minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(key).offset(offset).build());
      }
      while (bytes > 0) {
        int thisbytesread = stream.read(buf, off, bytes);
        if (thisbytesread > 0) {
          bytes -= thisbytesread;
          off  += thisbytesread;
        }
      }
    }
    catch (Exception e) {
      M.warnStackTrace("FAILED S3GET URL="+url, e);
    }
    bytes = off-boff;
    offset += bytes;
    return bytes;
  }

  public int write (byte[] buf, int boff, int bytes) {
    // okay, this does not really write ... it buffers in prep for a write
    if (!isOpen) open();
    if (writeBuffer== null) {
      // first pass
      writeBuffer = new byte[Math.max(writeBuffStarterSize, bytes)];
    }
    else if (writeBuffer.length < (bytes + offset)) {
      byte[] newWriteBuffer = new byte[Math.max(writeBuffer.length*2,bytes + (int)offset)];
      System.arraycopy(writeBuffer, 0, newWriteBuffer, 0, (int)offset);
      writeBuffer = newWriteBuffer;
    }
    System.arraycopy(buf, boff, writeBuffer, (int)offset, bytes);
    offset += bytes;
    if (offset > numWriteTotal) numWriteTotal = (int)offset;
    return bytes;
  }

  @Override
  public boolean seek (long offset) {
    this.offset = offset;
    return true;
  }

  public boolean exists () {
    if (url==null) return false;
    if ("".equals(bucket)) return false;
    boolean result = false;
    MinioClient minioClient = getMinioClient();
    try {
      if ("".equals(key)) {
        // check if bucket exists
        result = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
      }
      else {
        // check if key exists
        result = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
        try {
          minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(key).build());
          return true;
        }
        catch (Exception e) {
          return false;
        }
      }
    }
    catch (Exception e) {}

    return result;
  }

  /** Set the host/endpoint
   *  @param endpoint endpoint
   */
  public void setEndpoint (String endpoint) {
    this.endpoint = endpoint;
  }

  /** Get the host/endpoint
   *  @return the host/endpoint
   */
  public String getEndpoint() {
    return this.endpoint;
  }

  /** Set the bucket
   *  @param bucket the S3 bucket
   */
  public void setBucket (String bucket) {
    this.bucket = bucket;
  }

  /** Get the S3 Bucket
   *  @return the name of the S3 bucket
   */
  public String getBucket() {
    return this.bucket;
  }

  /** Set the key (object/file name)
   *  @param key the key/name of the object
   */
  public void setKey (String key) {
    this.key = key;
  }

  /** Get the key (object/file name)
   *  @return the key/name of the object
   */
  public String getKey() {
    return this.key;
  }

  /** Add an alias
   *
   *  @param alias      the alias (will default to the url if empty)
   *  @param url        url
   *  @param accessKey  access key
   *  @param secretKey  secret key
   */
  public static void addAlias (String alias, String url, String accessKey, String secretKey) {
    MapKBT entry = MapKBT.newMap();
    entry.put("url", url);
    entry.put("accessKey", accessKey);
    entry.put("secretKey", secretKey);
    aliases.put(alias, entry);
  }

  /** Remove named alias
   *
   *  @param alias alias to remove
   */
  public static void removeAlias (String alias) {
    aliases.remove(alias);
  }

  /** Remove all aliases */
  @InternalUseOnly
  public static void removeAllAliases () {
    aliases = MapKBT.newMap();
  }

  // TODO better handling of case with no file
  private static void importJsonConfig () {
    try {
      jsonConfig = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class,
          pathJsonConfig);
      int i = 0;
      while( (i < jsonConfig.size()) && (aliases==null)) {
        MapKBT.AbstractMapKBT map =  jsonConfig.get(i);
        if (map.containsKey("aliases")) {
          aliases = map.getMapKBT("aliases");
        }
        i++;
      }
      Shell.info("MINIO loading of aliases from config.json successful");
    }
    catch (Exception e) {
      aliases = MapKBT.newMap();
    }
  }

  /** Load aliases from a JSON config file that matches spec for
   *  the config.json that is genereally placed under user.home/.mc
   *
   *  @param jsonConfigFile the full path to the config file
   */
  public static void loadAliases (String jsonConfigFile) {
    try {
      List<MapKBT.AbstractMapKBT> config = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class,
          jsonConfigFile);
      MapKBT fileAliases = null;
      int i = 0;
      while( (i < config.size()) && (fileAliases==null)) {
        MapKBT.AbstractMapKBT map =  config.get(i);
        if (map.containsKey("aliases")) {
          fileAliases = map.getMapKBT("aliases");
        }
        i++;
      }
      if (fileAliases!=null) aliases.putAll(fileAliases);
    }
    catch (Exception e) {
      Shell.warnStackTrace("MinIOResource: Loading of aliases failed - file:"+jsonConfigFile, e);
    }
  }

  /** Import a single alias from a JSON file
   *  must match MC ALIAS IMPORT format
   *
   *  @param alias     alias name
   *  @param jsonFile  full path to JSON file defining alias
   */
  public static void importAlias (String alias, String jsonFile) {
    try {
      List<MapKBT.AbstractMapKBT> config = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class,
          jsonFile);
      MapKBT aliasImport = config.get(0);
      addAlias(alias,aliasImport.getS("url"),aliasImport.getS("accessKey"),aliasImport.getS("secretKey"));
    }
    catch (Exception e) {
      Shell.warnStackTrace("MinIOResource: Loading of alias failed - file:"+jsonFile, e);
    }
  }

  /** List all the current aliases
   *  aliases have cooresponding url, accessKey, adn secret key */
  public static void listAliases () {
    Shell.info("MINIO ALIASES: "+aliases);
  }

  /** List the named alias
   *
   *  @param alias alias to list
   */
  public static void listAlias (String alias) {
    Shell.info("MINIO ALIAS "+alias+": "+aliases.getKey(alias));
  }

  /** Get the current aliases
   *  @return aliases
   */
  @InternalUseOnly()
  public static MapKBT getAliases () {
    return aliases;
  }

}

