/**********************************************************************
* This file is part of iDempiere ERP Open Source                      *
* http://www.idempiere.org                                            *
*                                                                     *
* Copyright (C) Contributors                                          *
*                                                                     *
* This program is free software; you can redistribute it and/or       *
* modify it under the terms of the GNU General Public License         *
* as published by the Free Software Foundation; either version 2      *
* of the License, or (at your option) any later version.              *
*                                                                     *
* This program is distributed in the hope that it will be useful,     *
* but WITHOUT ANY WARRANTY; without even the implied warranty of      *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the        *
* GNU General Public License for more details.                        *
*                                                                     *
* You should have received a copy of the GNU General Public License   *
* along with this program; if not, write to the Free Software         *
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,          *
* MA 02110-1301, USA.                                                 *
*                                                                     *
* Contributors:                                                       *
* - Trek Global Corporation                                           *
* - Heng Sin Low                                                      *
**********************************************************************/
package com.trekglobal.idempiere.rest.api.v1.resource.impl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Base64;
import java.util.logging.Level;

import javax.activation.MimetypesFileTypeMap;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.compiere.model.MUser;
import org.compiere.util.CLogger;
import org.compiere.util.Env;
import org.compiere.util.MimeType;
import org.compiere.util.Util;
import org.idempiere.distributed.IClusterMember;
import org.idempiere.distributed.IClusterService;

import com.google.gson.JsonObject;
import com.trekglobal.idempiere.rest.api.json.ResponseUtils;
import com.trekglobal.idempiere.rest.api.util.ClusterUtil;
import com.trekglobal.idempiere.rest.api.util.ErrorBuilder;
import com.trekglobal.idempiere.rest.api.v1.resource.FileResource;
import com.trekglobal.idempiere.rest.api.v1.resource.file.FileAccess;
import com.trekglobal.idempiere.rest.api.v1.resource.file.FileInfo;
import com.trekglobal.idempiere.rest.api.v1.resource.file.FileStreamingOutput;
import com.trekglobal.idempiere.rest.api.v1.resource.file.GetFileInfoCallable;
import com.trekglobal.idempiere.rest.api.v1.resource.file.RemoteFileStreamingOutput;

/**
 * 
 * @author hengsin
 *
 */
public class FileResourceImpl implements FileResource {
	
	protected static final int BLOCK_SIZE = 1024 * 1024 * 5;
	private static final CLogger log = CLogger.getCLogger(FileResourceImpl.class);
	
	public FileResourceImpl() {
	}

	@Override
	public Response getFile(String fileName, long length, String nodeId, String asJson) {
		MUser user = MUser.get(Env.getCtx());
		if (!user.isAdministrator())
			return forbidden("Access denied", "Access denied for get file request");
		/* TODO: allow access to files generated by processes in a more secure way
		 *   the length is not enough for security because a hacker can get access with trial/error
		 *   maybe adding an md5sum in the response of the process, and validating it here can make the implementation safer
		 */

		if (Util.isEmpty(nodeId)) {
			return getLocalFile(fileName, true, length, asJson);
		} else {
			IClusterService service = ClusterUtil.getClusterService();
			if (service == null) 
				return getLocalFile(fileName, true, length, asJson);
			
			IClusterMember local = service.getLocalMember();
			if (local != null && local.getId().equals(nodeId))
				return getLocalFile(fileName, true, length, asJson);
			
			return getRemoteFile(fileName, true, length, nodeId, asJson);
		}
	}

	/**
	 * 
	 * @param fileName
	 * @param nodeId
	 * @return response
	 */
	public Response getFile(String fileName, String nodeId, String asJson) {
		if (Util.isEmpty(nodeId)) {
			return getLocalFile(fileName, false, 0, asJson);
		} else {
			IClusterService service = ClusterUtil.getClusterService();
			if (service == null) 
				return getLocalFile(fileName, false, 0, asJson);
			
			IClusterMember local = service.getLocalMember();
			if (local != null && local.getId().equals(nodeId))
				return getLocalFile(fileName, false, 0, asJson);
			
			return getRemoteFile(fileName, false, 0, nodeId, asJson);
		}
	}
	
	private Response getLocalFile(String fileName, boolean verifyLength, long length, String asJson) {
		File file = new File(fileName);
		if (file.exists() && file.isFile()) {
			if (!file.canRead() || !FileAccess.isAccessible(file)) {
				return Response.status(Status.FORBIDDEN)
						.entity(new ErrorBuilder().status(Status.FORBIDDEN).title("File not readable").append("File not readable: ").append(fileName).build().toString())
						.build();
			} else if (!verifyLength || file.length()==length) {
				try {
					if (asJson == null) {
						String contentType = null;
						String lfn = fileName.toLowerCase();
						if (lfn.endsWith(".html") || lfn.endsWith(".htm")) {
							contentType = MediaType.TEXT_HTML;
						} else if (lfn.endsWith(".csv") || lfn.endsWith(".ssv") || lfn.endsWith(".log")) {
							contentType = MediaType.TEXT_PLAIN;
						} else {
							contentType = MimeType.getMimeType(file.getName());
						}
						if (Util.isEmpty(contentType, true))
							contentType = MediaType.APPLICATION_OCTET_STREAM;
						FileStreamingOutput fso = new FileStreamingOutput(file);
						return Response.ok(fso, contentType).build();
					} else {
						JsonObject json = new JsonObject();
						byte[] binaryData = Files.readAllBytes(file.toPath());
						String data = Base64.getEncoder().encodeToString(binaryData);
						json.addProperty("data", data);
						return Response.ok(json.toString()).build();
					}
				} catch (IOException ex) {
					return ResponseUtils.getResponseErrorFromException(ex, "IO error");
				}
			} else {
				return Response.status(Status.FORBIDDEN)
						.entity(new ErrorBuilder().status(Status.FORBIDDEN).title("Access denied").append("Access denied for file: ").append(fileName).build().toString())
						.build();
			}
		} else {
			return Response.status(Status.NOT_FOUND)
					.entity(new ErrorBuilder().status(Status.NOT_FOUND).title("File not found").append("File not found: ").append(fileName).build().toString())
					.build();
		}
	}

	private Response getRemoteFile(String fileName, boolean verifyLength, long length, String nodeId, String asJson) {
		IClusterService service = ClusterUtil.getClusterService();
		IClusterMember member = ClusterUtil.getClusterMember(nodeId);
		if (member == null) {
			return Response.status(Status.NOT_FOUND)
					.entity(new ErrorBuilder().status(Status.NOT_FOUND).title("Invalid Node Id").append("No match found for node id: ").append(nodeId).build().toString())
					.build(); 
		}
		
		try {
			GetFileInfoCallable infoCallable = new GetFileInfoCallable(null, fileName, BLOCK_SIZE);
			FileInfo fileInfo = service.execute(infoCallable, member).get();
			if (fileInfo == null) {
				return Response.status(Status.NOT_FOUND)
						.entity(new ErrorBuilder().status(Status.NOT_FOUND).title("Invalid File Name").append("File does not exists or not readable: ").append(fileName).build().toString())
						.build(); 
			}
			if (verifyLength && length != fileInfo.getLength()) {
				return Response.status(Status.FORBIDDEN)
						.entity(new ErrorBuilder().status(Status.FORBIDDEN).title("Access denied").append("Access denied for file: ").append(fileName).build().toString())
						.build();
			}
			
			RemoteFileStreamingOutput rfso = new RemoteFileStreamingOutput(fileInfo, member);
			if (asJson == null) {
				String contentType = null;
				String lfn = fileName.toLowerCase();
				if (lfn.endsWith(".html") || lfn.endsWith(".htm")) {
					contentType = MediaType.TEXT_HTML;
				} else if (lfn.endsWith(".csv") || lfn.endsWith(".ssv") || lfn.endsWith(".log")) {
					contentType = MediaType.TEXT_PLAIN;
				} else {
					MimetypesFileTypeMap map = new MimetypesFileTypeMap();
					contentType = map.getContentType(fileInfo.getFileName());
				}
				if (Util.isEmpty(contentType, true))
					contentType = MediaType.APPLICATION_OCTET_STREAM;
				return Response.ok(rfso, contentType).build();
			} else {
				try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
					rfso.write(byteArrayOutputStream);
					JsonObject json = new JsonObject();
					byte[] binaryData = byteArrayOutputStream.toByteArray();
					String data = Base64.getEncoder().encodeToString(binaryData);
					json.addProperty("data", data);
					return Response.ok(json.toString()).build();
				}
			}
		} catch (Exception ex) {
			log.log(Level.SEVERE, ex.getMessage(), ex);
			return Response.status(Status.INTERNAL_SERVER_ERROR)
					.entity(new ErrorBuilder().status(Status.INTERNAL_SERVER_ERROR).title("Server error").append("Server error with exception: ").append(ex.getMessage()).build().toString())
					.build();
		}
	}

	private Response forbidden(String title, String detail) {
		return Response.status(Status.FORBIDDEN)
				.entity(new ErrorBuilder().status(Status.FORBIDDEN).title(title).append(detail).build().toString())
				.build();
	}

}
