/**
 * Copyright (c) 2025 SoftCorporation LLC. All rights reserved.
 *
 * The Software License, Version 1.0
 *
 * SoftCorporation LLC. grants you ("Licensee") a non-exclusive, royalty free,
 * license to use, modify and redistribute this software in source and binary
 * code form, provided that the following conditions are met:
 *
 * 1. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        SoftCorporation LLC. (http://www.softcorporation.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 2. This software and SoftCorporation name must not be used to
 *    promote products derived from the software without prior
 *    written permission. For written permission, please contact
 *    info@softcorporation.com.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
 * IN NO EVENT SHALL THE SOFTCORPORATION BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION).
 *
 */
package com.softcorporation.sandi.base.util;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.softcorporation.util.StringUtil;
import com.softcorporation.util.net.ClientRequest;
import com.softcorporation.util.net.ClientResponse;
import com.softcorporation.util.net.HttpClient;

/**
 * Utils
 *
 */
public final class Utils {

    private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);

	/**
	 * Constructor
	 */
	private Utils() {
	}

	/**
	 * Set default time zone
	 */
	public static void setDefaultTimeZone() {
		TimeZone.setDefault(TimeZone.getTimeZone(Constants.TIME_ZONE_DEFAULT));
	}

	/**
	 * This method is to merge two json objects to new json
	 * 
	 * @param json1 json1
	 * @param json2 json2
	 * @return mergedJSON JSONObject
	 */
	public static JSONObject mergeJSONObjects(final JSONObject json1, final JSONObject json2) {
		JSONObject mergedJSON = new JSONObject(json1, JSONObject.getNames(json1));
		for (String key : JSONObject.getNames(json2)) {
			mergedJSON.put(key, json2.get(key));
		}
		return mergedJSON;
	}

	/**
	 * This method is to get current date and time.
	 * 
	 * @return SimpleDateFormat
	 */
	public static String getCurrentTime() {
		return new SimpleDateFormat(Constants.DATE_FORMAT_STANDARD, Locale.US)
				.format(Calendar.getInstance().getTimeInMillis());
	}

	/**
	 * This method is to get current date and time in TZ format.
	 * 
	 * @return SimpleDateFormat
	 */
	public static String getSolrTime() {
		return new SimpleDateFormat(Constants.SOLR_DATE_FORMAT, Locale.US)
				.format(Calendar.getInstance().getTimeInMillis());
	}

	/**
	 * Parse date.
	 * 
	 * @return Date
	 * @param sDate the Date String
	 * @throws ParseException
	 */
	public static Date parseDate(String sDate) throws ParseException {
		return new SimpleDateFormat(Constants.DATE_FORMAT, Locale.US).parse(sDate);
	}

	/**
	 * Format date.
	 * 
	 * @return Date as string
	 */
	public static String formatDate(Date date) {
		return new SimpleDateFormat(Constants.DATE_FORMAT, Locale.US).format(date);
	}

	/**
	 * Format standard date.
	 * 
	 * @param date the Date
	 * @return Date as string
	 */
	public static String formatStandardDate(final Date date) {
		return new SimpleDateFormat(Constants.DATE_FORMAT_STANDARD, Locale.US).format(date);
	}

	/**
	 * Parse standard date.
	 * 
	 * @param sDate the Date String
	 * @return Date
	 * @throws ParseException exception
	 */
	public static Date parseStandardDate(final String sDate) throws ParseException {
		return new SimpleDateFormat(Constants.DATE_FORMAT_STANDARD, Locale.US).parse(sDate);
	}

	/**
	 * Check if dir is writable
	 * 
	 * @param dir the directory
	 * @return true if writable
	 */
	public static boolean isWritableDir(final String dir) {
		boolean output;
		if (dir == null) {
			output = false;
		} else {
			File file = new File(dir);
			if (file.exists() && file.isDirectory() && file.canWrite()) {
				output = true;
			} else {
				output = false;
			}
		}
		return output;
	}

	/**
	 * Cuts text.
	 * 
	 * @param source    the source string
	 * @param maxLength max length of the source
	 * 
	 * @return Short string
	 */
	public static String cutText(final String source, final int maxLength) {
		String output;
		if (maxLength >= source.length()) {
			output = source;
		} else {
			output = source.substring(0, maxLength).trim() + "...";
		}
		return output;
	}

	/**
	 * Get StackTrace.
	 * 
	 * @param throwable Throwable
	 * @return StackTrace as String
	 */
	public static String getStackTrace(final Throwable throwable) {
		final Writer writer = new StringWriter();
		throwable.printStackTrace(new PrintWriter(writer));
		return writer.toString();
	}

	/**
	 * Create Zip File
	 * 
	 * @param input Path of file or directory
	 * @param output Path of zip file
	 * @throws IOException if an I/O error occurs
	 */
	public static void createZipFile(final Path input, final Path output) throws IOException {
		if (input == null) {
			throw new IllegalArgumentException("Input path cannot be null");
		}
		
		if (output == null) {
			throw new IllegalArgumentException("Output path cannot be null");
		}
		
		if (!Files.exists(input)) {
			throw new IllegalArgumentException("Input path does not exist: " + input);
		}
		
		// Create parent directories for output if they don't exist
		Path outputParent = output.getParent();
		if (outputParent != null && !Files.exists(outputParent)) {
			Files.createDirectories(outputParent);
		}
		
		try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(output))) {
			if (Files.isDirectory(input)) {
				zipDirectory(input, input, zipOut);
			} else {
				zipFile(input, input.getFileName().toString(), zipOut);
			}
		}
	}
	
	/**
	 * Recursively zip a directory
	 * 
	 * @param sourceDir Source directory path
	 * @param basePath Base path for relative entries
	 * @param zipOut ZipOutputStream
	 * @throws IOException if an I/O error occurs
	 */
	private static void zipDirectory(final Path sourceDir, final Path basePath, final ZipOutputStream zipOut) throws IOException {
		Files.walk(sourceDir)
			.filter(path -> !Files.isDirectory(path))
			.forEach(path -> {
				try {
					String entryName = basePath.relativize(path).toString().replace('\\', '/');
					zipFile(path, entryName, zipOut);
				} catch (IOException e) {
					throw new RuntimeException("Failed to zip file: " + path, e);
				}
			});
	}
	
	/**
	 * Add a single file to the zip
	 * 
	 * @param filePath Path of the file to zip
	 * @param entryName Name of the entry in the zip
	 * @param zipOut ZipOutputStream
	 * @throws IOException if an I/O error occurs
	 */
	private static void zipFile(final Path filePath, final String entryName, final ZipOutputStream zipOut) throws IOException {
		ZipEntry zipEntry = new ZipEntry(entryName);
		zipOut.putNextEntry(zipEntry);
		Files.copy(filePath, zipOut);
		zipOut.closeEntry();
	}
    	
	/**
	 * Make list of strings
	 * 
	 * @param commaSeparatedString String comma separated
	 * @return list
	 */
	public static List<String> stringToList(final String commaSeparatedString, final boolean allowDuplicates) {
		if (commaSeparatedString == null) {
			return null;
		}
		List<String> list = new ArrayList<>();
		String[] strings = commaSeparatedString.split(",");
		for (String string : strings) {
			string = string.trim();
			if (string.length() > 0) {
				if (!allowDuplicates && list.contains(string)) {
					continue;
				}
				list.add(string);
			}
		}
		return list;
	}
	
	/**
	 * Make a string from list
	 * 
	 * @param List of strings
	 * @return String
	 */
	public static String listToString(final List<String> strings) {
		if (strings == null || strings.size() == 0) {
			return null;
		}
		StringBuilder sb = new StringBuilder();
		for (String string : strings) {
			if (sb.length() > 0) {
				sb.append(",");
			}
			sb.append(string);
		}
		return sb.toString();
	}
	
	/**
	 * Make random UUID
	 * 
	 * @return UUID
	 */
	public static String getUUID() {
		return java.util.UUID.randomUUID().toString();
	}
	
	/**
	 * Make UUID from string
	 * 
	 * @param string String
	 * @return String
	 */
	public static String getUUID(final String string) {
		return java.util.UUID.nameUUIDFromBytes(string.getBytes(StandardCharsets.UTF_8)).toString();
	}

	/**
	 * GET request with default 1 min timeout
	 */
	public static String get(final String url) throws IOException {
		return get(url, 60);
	}
	
	/**
	 * GET request with timeout in sec
	 */
	public static String get(final String url, int timeout) throws IOException {
		ClientRequest clientRequest = new ClientRequest(url);
//		LOGGER.debug(clientRequest.toString());

		ClientResponse clientResponse = null;

		int retry = 0;
		boolean success = false;
		while (!success && retry < 2) {
			if (retry > 0) {
				try {
					LOGGER.debug("Retry " + retry + " pause (seconds): " + 1);
					Thread.sleep(1 * 1000);
				} catch (Exception exc) {
				}
			}
			try {
				HttpClient client = new HttpClient();
				client.setTimeout(timeout * 1000);
				client.addHeader("Content-Type", "application/json");
//                client.addHeader("Accept-Encoding", "gzip, deflate, br");
				clientResponse = client.get(clientRequest);
//				LOGGER.debug("ClientResponse status: " + clientResponse.getStatusCode());
				success = true;
			} catch (Exception exception) {
				LOGGER.error("Exception: " + exception.getMessage());
				LOGGER.error("Error processing request: " + url);
				retry++;
			}
		}

		if (clientResponse == null) {
			throw new IOException("Cannot download feed from url: " + url);
		}

		String content = clientResponse.getContent();
		return content;
	}

	/**
	 * GET request with file to download and default 1 min timeout
	 */
	public static long get(final String url, File file) throws IOException {
		return get(url, file, 60);
	}
	
	/**
	 * GET request with file to download and timeout in sec
	 */
	public static long get(final String url, final File file, final int timeout) throws IOException {
		ClientRequest clientRequest = new ClientRequest(url);
		clientRequest.setFile(file);
//		LOGGER.debug(clientRequest.toString());

		ClientResponse clientResponse = null;

		int retry = 0;
		boolean success = false;
		while (!success && retry < 2) {
			if (retry > 0) {
				try {
					LOGGER.debug("Retry " + retry + " pause (seconds): " + 1);
					Thread.sleep(1 * 1000);
				} catch (Exception exc) {
				}
			}
			try {
				HttpClient client = new HttpClient();
				client.setTimeout(timeout * 1000);
				client.addHeader("Content-Type", "application/json");
//                client.addHeader("Accept-Encoding", "gzip, deflate, br");
				clientResponse = client.get(clientRequest);
//				LOGGER.debug("ClientResponse status: " + clientResponse.getStatusCode());
				success = true;
			} catch (Exception exception) {
				LOGGER.error("Exception: " + exception.getMessage());
				LOGGER.error("Error processing request: " + url);
				retry++;
			}
		}

		if (clientResponse == null) {
			throw new IOException("Cannot download feed from url: " + url);
		}
		return clientResponse.getContentLength();
	}

	/**
	 * POST request with default 1 min timeout
	 */
	public static String post(final String url, final String body) throws IOException {
		return post(url, body, 60);
	}
	
	/**
	 * POST request with timeout in sec
	 */
	public static String post(final String url, final String body, int timeout) throws IOException {
		ClientRequest clientRequest = new ClientRequest(url);
		clientRequest.setContent(body);
//		LOGGER.debug(clientRequest.toString());

		ClientResponse clientResponse = null;

		int retry = 0;
		boolean success = false;
		while (!success && retry < 2) {
			if (retry > 0) {
				try {
					LOGGER.debug("Retry " + retry + " pause (seconds): " + 1);
					Thread.sleep(1 * 1000);
				} catch (Exception exc) {
				}
			}
			try {
				HttpClient client = new HttpClient();
				client.setTimeout(timeout * 1000);
				client.addHeader("Content-Type", "application/json");
//                client.addHeader("Accept-Encoding", "gzip, deflate, br");
				clientResponse = client.post(clientRequest);
//				LOGGER.debug("ClientResponse status: " + clientResponse.getStatusCode());
				success = true;
			} catch (Exception exception) {
				LOGGER.error("Exception: " + exception.getMessage());
				LOGGER.error("Error processing request: " + body);
				retry++;
			}
		}

		if (clientResponse == null) {
			throw new IOException("Cannot download feed from url: " + url);
		}

		String content = clientResponse.getContent();
		return content;
	}
	
    /**
     * Escape quotes
     * 
     * @param text String
     * @return Escaped text
     */
    public static String escapeQuotes(final String text) {
        String output = text.replaceAll("[\\\\]", "\\\\\\\\");
        output = output.replaceAll("[\"]", "\\\\\"");
        return output;
    }

    /**
     * Removes double quotes
     * 
     * @param text String
     * @return text without double quotes
     */
    public static String removeQuotes(final String text) {
        return StringUtil.cleanSpaces(text.replace('"', ' '));
    }

    /**
     * Replace double quotes to single
     * 
     * @param text String
     * @return text without double quotes
     */
    public static String replaceQuotes(final String text) {
        return text.replace('"', '\'');
    }

    public static boolean containsDigits(String text) {
        if (text.isEmpty()) {
            return false;
        }
        Pattern pattern = Pattern.compile(".*\\d.*");
        Matcher matcher = pattern.matcher(text);
        return matcher.matches();
    }
    
    /**
     * Checks if a JSONArray contains a specific value.
     *
     * @param jsonArray The JSONArray to search within.
     * @param value The value to search for.
     * @return true if the value is found, false otherwise.
     */
    public static boolean containsValue(JSONArray jsonArray, Object value) {
    	for (Object object : jsonArray) {
            if (value.equals(object)) {
                return true;
            }
    	}
        return false;
    }
    

	/**
	 * Get temp directory
	 * 
	 * @return the Path
	 * @throws IOException 
	 */
	public static Path getTempDir() throws IOException {
		return StringUtil.getTempDir("sandi");
	}    
    
    public static String getTextOfList(List<String> texts) {
    	if (texts == null) {
    		return null;
    	}
    	StringBuilder sb = new StringBuilder();
    	for (String text : texts) {
    		if (sb.length() > 0) {
        		sb.append("\n");
    		}
    		sb.append(text);
    	}
    	return sb.toString();
    }	
	
    /**
     * Get text of object
     * 
     * @param value
     * @return text
     */
    public static String getTextOfObject(Object value) {
    	String text;
		if (value == null) {
		    text = null;
		} else if (value instanceof String) {
   			text = (String) value;
		} else if (value instanceof JSONArray) {
			JSONArray array = (JSONArray) value;
			StringBuilder sb = new StringBuilder();
			for (Object object : array) {
    			if (sb.length() > 0) {
    				sb.append(", ");
    			}
				sb.append(String.valueOf(object));
			}
    		text = sb.toString();
		} else {
			text = String.valueOf(value);
		}
    	return text;
    }
	
    public static List<String> mergeLists(List<String> texts1, List<String> texts2) {
    	List<String> texts = new ArrayList<>();
    	for (String text : texts1) {
    		texts.add(text);
    	}
    	for (String text : texts2) {
    		texts.add(text);
    	}
    	return texts;
    }	
	
    
    // TODO Use md5 for file name?
    public static String extractFilenameFromUrl(String url) {
        try {
            String path = new URL(url).getPath();
            String filename = path.substring(path.lastIndexOf('/') + 1);
            if (filename.isEmpty() || !filename.contains(".")) {
                filename = "document_" + System.currentTimeMillis() + ".html";
            }
            return filename;
        } catch (Exception e) {
            return "document_" + System.currentTimeMillis() + ".html";
        }
    }
     
    /**
     * Text breaking characters
     */
	private static final String TEXT_BREAK_CHARS = " \n.,;\t\r";	

    /**
     * Check the text breaking character
     */
    public static boolean isTextBreakChar(char c) {
    	if (TEXT_BREAK_CHARS.indexOf(c) >= 0) {
    		return true;
    	}
    	return false;
    }
	
	/**
	 * Get sub text and trim to space
	 * 
	 * @param text String
	 * @return String
	 */
	public static String getSubText(final String text, int begin, int end) {
		if (begin <= 0) {
			begin = 0;
		} else {
			for (int i = begin - 1; i < text.length(); i++) {
				char c = text.charAt(i);
				if (Utils.isTextBreakChar(c)) {
					begin = i + 1;
					break;
				}
			}
		}
		if (end >= text.length()) {
			end = text.length();
		} else {
			for (int i = end; i > 0; i--) {
				char c = text.charAt(i);
				if (Utils.isTextBreakChar(c)) {
					end = i;
					break;
				}
			}
		}
		return text.substring(begin, end);
	}

	/**
	 * Exclude duplicates
	 * @param words List
	 * @return List of unique words
	 */
	public static List<String> excludeDuplicates(final List<String> words) {
		List<String> uniqueWords = new ArrayList<>();
		Set<String> set = new HashSet<>();
        for (String word : words) {
        	String wordL = word.toLowerCase();
            if (!set.contains(wordL)) {
            	uniqueWords.add(word);
            	set.add(wordL);
            }
        }
        return uniqueWords;
	}

	/**
	 * Escape query chars
	 * 
	 * @param query
	 * @return escaped query
	 */
    public static String escapeQueryChars(String query) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < query.length(); i++) {
            char c = query.charAt(i);
            if (c == '\\'
                || c == '+'
                || c == '-'
                || c == '!'
                || c == '('
                || c == ')'
                || c == ':'
                || c == '^'
                || c == '['
                || c == ']'
                || c == '\"'
                || c == '{'
                || c == '}'
                || c == '~'
//                || c == '*'
                || c == '?'
                || c == '|'
                || c == '&'
                || c == ';'
                || c == '/'
//                || Character.isWhitespace(c)
            ) {
                sb.append('\\');
            }
            sb.append(c);
        }
        return sb.toString();
    }
	
}
