/**
 * 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.service.solr;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
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.stereotype.Service;

import com.softcorporation.sandi.base.model.Collection;
import com.softcorporation.sandi.base.service.LanguageModelService;
import com.softcorporation.sandi.base.service.SynonymService;
import com.softcorporation.sandi.base.util.Constants;
import com.softcorporation.sandi.base.util.SynonymException;
import com.softcorporation.sandi.base.util.Utils;
import com.softcorporation.util.StringUtil;
import com.softcorporation.util.VecUtil;

import jakarta.annotation.PostConstruct;

@Service
public class SolrSynonymService implements SynonymService {

    private static final Logger LOGGER = LoggerFactory.getLogger(SolrSynonymService.class);
    
    private List<String> languages = new ArrayList<>();
    
    /**
     * Create collections
     */
    @Value("${sandi.collections.create}")
    private boolean createColls;
    
    @Autowired(required = false)
    private LanguageModelService languageModelService;
    
    @Value("${sandi.embedding.process.max:50}")
    private int embeddingProcessMax;
    
    @Value("${sandi.languages:en}")
    private String sandiLanguages;
    
    @Value("${sandi.synonyms.directory:/sandi/conf/synonyms}")
    private String synonymsDirectory;
    
    @Autowired
    private SolrService solrService;
    
    @Autowired
    private SolrCollectionService collectionService;
    
    /**
	 * Embedding byte scale
	 */
    @Value("${sandi.embedding.byte.scale:500}")
    private int embByteScale;
    
    @PostConstruct
    @Override
    public void init() {
        LOGGER.debug("Initializing SynonymService");
        if (createColls) {
            try {
            	if (!collectionService.hasCollection(Constants.SYNONYMS_COLLECTION)) {
            		Collection synonymsCollection = new Collection(Constants.SYNONYMS_COLLECTION, Constants.SYNONYMS_COLLECTION);
            		collectionService.createCollection(synonymsCollection);
            	}
            } catch (Exception e) {
                LOGGER.error("Failed to initialize SynonymService: {}", e.getMessage(), e);
            }
        }
        String[] languagesArray = sandiLanguages.split(",");
        for (int i = 0; i < languagesArray.length; i++) {
        	String language = languagesArray[i];
        	language = language.trim();
        	if (language.length() == 2) {
        		languages.add(language);
        	}
        }
    }

    @Override
    public List<String> listSynonymSets() throws SynonymException {
        LOGGER.debug("Listing all synonym sets");
        
        try {
            SolrQuery query = new SolrQuery("*:*");
            query.setRows(0); // We only want facets, not results
            query.setFacet(true);
            query.addFacetField("name");
            query.setFacetLimit(-1); // Get all unique names
            query.setFacetMinCount(1);

            QueryResponse response = solrService.getSolrClient().query(Constants.SYNONYMS_COLLECTION, query);

            List<String> names = new ArrayList<>();
            if (response.getFacetField("name") != null) {
                response.getFacetField("name").getValues().forEach(count -> {
                    names.add(count.getName());
                });
            }
            LOGGER.info("Found synonym sets: {}", names);
            return names;
            
        } catch (SolrServerException | IOException e) {
            LOGGER.error("Failed to list synonym sets: {}", e.getMessage(), e);
            throw new SynonymException("Failed to list synonym sets: " + e.getMessage());
        }
    }
    
    @Override
    public void uploadSynonyms(final String name, final String fileName, final String language) throws SynonymException {
        if (name == null || name.trim().isEmpty()) {
            throw new SynonymException("Synonyms set name cannot be null or empty");
        }
        if (StringUtil.containsNonWordChars(name)) {
            throw new SynonymException("Invalid synonyms set name " + name);
        }
        if (fileName == null || fileName.trim().isEmpty()) {
            throw new SynonymException("Synonym set file name must be provided");
        }
        
        if (!languages.contains(language)) {
            throw new SynonymException("Language '" + language + "' not supported");
        }
        
    	Path path = Paths.get(synonymsDirectory + "/" + fileName);
        if (path == null || !Files.exists(path)) {
            throw new SynonymException("Synonyms set file must exist: " + path);
        }

        if (Files.isDirectory(path)) {
            throw new SynonymException("Synonyms set must be a file: " + path);
        }
        
        LOGGER.info("Uploading synonym set '{}' from path: {}", name, path);
        
        try (BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) {
            String line;
            List<String> lines = new ArrayList<>();
            
            while ((line = reader.readLine()) != null) {
                line = line.trim();                
                if (line.isEmpty()) {
                    continue; // Skip empty lines
                }
            	lines.add(line);
            	
            	if (lines.size() >= embeddingProcessMax) {
    			    processSynonyms(name, lines, language);
    			    lines = new ArrayList<>();
            	}
            }
        	if (lines.size() > 0) {
			    processSynonyms(name, lines, language);
        	}
        	solrService.getSolrClient().commit(Constants.SYNONYMS_COLLECTION);
        	init();
            LOGGER.info("Successfully uploaded synonym set '{}'", name);
        } catch (SolrServerException | IOException e) {
            LOGGER.error("Failed to upload synonym set '{}': {}", name, e.getMessage(), e);
            throw new SynonymException("Failed to upload synonym set: " + e.getMessage());
        }
    }
    
    String processLine(final String line) {
    	StringBuilder sb = new StringBuilder();
    	String[] synonyms = line.split(",");
    	for (String synonym : synonyms) {
    		if (sb.length() > 0) {
    			sb.append(" ");
    		}
    		if (synonym.contains(" ")) {
    			sb.append("\"");
    			sb.append(Utils.escapeQuotes(synonym));
    			sb.append("\"");
    		} else {
    			sb.append(synonym);
    		}
    	}
    	return sb.toString();
    }
    
    /***
     * Process and upload synonyms
     * 
     */
    void processSynonyms(final String name, final List<String> lines, final String language) throws IOException, SolrServerException {
    	List<String> wordList = new ArrayList<>(lines.size());
    	List<List<String>> synonymList = new ArrayList<>(lines.size());
    	List<String> textList = new ArrayList<>(lines.size());
    	
    	for (String line : lines) {
    		String comment;
    		if (line.contains("#")) {
        		String[] parts = line.split("#");
        		line = parts[0].trim();
        		comment = " # " + parts[1].trim();
    		} else {
    			comment = "";
    		}
    		if (line.length() == 0) {
    			continue;
    		}
    		
            String[] parts = line.split("=>");
            if (parts.length != 2) {
                LOGGER.warn("Invalid synonyms file format. Skipping line: {}", line);
            	continue;
            }
    	    String word = parts[0].trim();
    	    if (word.length() == 0) {
                LOGGER.warn("Invalid synonyms file format. Skipping line: {}", line);
            	continue;
    	    }
    	    String words = parts[1].trim();
            if (words.length() == 0) {
                LOGGER.warn("Invalid synonyms file format. Skipping line: {}", line);
            	continue;
            }
            List<String> synonyms = new ArrayList<>();
            String[] array = words.split(",");
            for (String synonym : array) {
            	synonym = synonym.trim();
            	if (synonym.length() == 0) {
                	continue;
            	}
            	synonyms.add(synonym);
            }
            if (synonyms.size() == 0) {
                LOGGER.warn("Invalid synonyms file format. Skipping line: {}", line);
            	continue;
            }
            wordList.add(word);
            synonymList.add(synonyms);
            StringBuilder sb = new StringBuilder();
            sb.append(word);
            for (String synonym: synonyms) {
                sb.append(", ");
            	sb.append(synonym);
            }
            textList.add(sb.toString() + comment);
    	}
    	
    	List<SolrInputDocument> solrDocuments = new ArrayList<>();
    	for (int i = 0; i < textList.size(); i++) {
            SolrInputDocument solrDoc = new SolrInputDocument();
//            solrDoc.addField(Constants.INDEXED, new Date());
            String word = wordList.get(i);
            solrDoc.addField(Constants.WORDS_PREFIX + language, word);
            solrDoc.addField(Constants.SYNONYMS, synonymList.get(i));
            solrDoc.addField(Constants.SYNONYMS_NAME, name);
            solrDocuments.add(solrDoc);
    	}
    	solrService.getSolrClient().add(Constants.SYNONYMS_COLLECTION, solrDocuments);
        LOGGER.debug("Successfully added {} synonym lines for set: {}", solrDocuments.size(), name);
    }
    
    /**
     * Deletes all synonyms with the specified name from the _synonyms collection.
     *
     * @param name The synonym set name to delete
     * @return true if any records were deleted, false otherwise
     * @throws SynonymException If deletion fails
     */    
    @Override
    public boolean deleteSynonyms(final String name) throws SynonymException {
        if (name == null || name.trim().isEmpty()) {
            throw new SynonymException("Synonym set name cannot be null or empty");
        }

        LOGGER.info("Deleting synonym set '{}'", name);
        
        try {
            String deleteQuery = "name:\"" + Utils.escapeQueryChars(name) + "\"";
            
            SolrQuery countQuery = new SolrQuery(deleteQuery);
            countQuery.setRows(0);
            QueryResponse countResponse = solrService.getSolrClient().query(Constants.SYNONYMS_COLLECTION, countQuery);
            long count = countResponse.getResults().getNumFound();

            if (count == 0) {
                LOGGER.debug("No synonyms found with name: {}", name);
                return false;
            }
            
            solrService.getSolrClient().deleteByQuery(Constants.SYNONYMS_COLLECTION, deleteQuery);
            solrService.getSolrClient().commit(Constants.SYNONYMS_COLLECTION);
            init();
            LOGGER.info("Deleted {} synonym records from set: {}", count, name);
            return true;
        } catch (Exception e) {
            LOGGER.error("Failed to delete synonym set '{}': {}", name, e.getMessage(), e);
            throw new SynonymException("Failed to delete synonym set: " + e.getMessage());
        }
    }

    @Override
    public List<String> getAllSynonyms(final String word, final String name) throws SynonymException {
        if (name == null || name.isEmpty()) {
            throw new SynonymException("Synonym set name cannot be null or empty");
        }
        List<String> allSynonyms = new ArrayList<String>();
        if (word == null || word.trim().isEmpty() || word.equals("*")) {
            return new ArrayList<String>();
        }
        LOGGER.info("Searching synonym set '{}'", name);
        
        try {
        	String text = word.replace(" ", "\\ ");
        	text = Utils.escapeQueryChars(text);
        	
            SolrQuery solrQuery = new SolrQuery();
            solrQuery.set("q", "*:*");
            solrQuery.set("fl", Constants.SYNONYMS);

           	StringBuilder sbFilter = new StringBuilder();
            for (String language : languages) {
            	if (sbFilter.length() > 0) {
            		sbFilter.append(" OR ");
            	}
            	sbFilter.append(Constants.WORDS_PREFIX);
            	sbFilter.append(language);
            	sbFilter.append(":");
            	sbFilter.append(text);
            }
            String filter = "name:" + Utils.escapeQueryChars(name) + " AND (" + sbFilter.toString() + ")";
            solrQuery.set("fq", filter);           	
            
            QueryResponse response = solrService.getSolrClient().query(Constants.SYNONYMS_COLLECTION, solrQuery);
            SolrDocumentList documents = response.getResults();
            for (SolrDocument document : documents) {
            	List<String> synonyms = (List<String>) document.getFieldValue(Constants.SYNONYMS);
            	for (String synonym : synonyms) {
            		if (!allSynonyms.contains(synonym)) {
            		    allSynonyms.add(synonym);
            		}
            	}
            }
            LOGGER.debug("Found {} synonym records for word '{}' in synonym set {} ", allSynonyms.size(), word, name);
        	return allSynonyms;
        } catch (SolrServerException | IOException e) {
            LOGGER.error("Failed to search synonym set {} Error: {}", name, e.getMessage(), e);
            throw new SynonymException("Failed to search synonym set: " + e.getMessage());
        }
    }

    @Override
    public Map<String, List<List<String>>> getSynonyms(final List<String> words, final List<String> names) throws SynonymException {
        Map<String, List<List<String>>> synonymMap = new TreeMap<>();
    	
        // names of synonyms sets
    	if (names == null || names.isEmpty()) {
            return synonymMap;
        }
        
    	// words to search synonyms
        if (words == null || words.isEmpty()) {
            return synonymMap;
        }
   
        SolrQuery solrQuery = new SolrQuery();
//      solrQuery.set("q", "*:*");

        StringBuilder sbQuery = new StringBuilder();
        sbQuery.append("{!bool");
        StringBuilder sbFields = new StringBuilder();
    	sbFields.append(Constants.SYNONYMS);
      	sbFields.append(",");
      	sbFields.append(Constants.SCORE);
      	sbFields.append(",");
      	sbFields.append(Constants.WORD);
      	sbFields.append(":");
      	for (int i = 0; i < words.size(); i++) {
      		String word = words.get(i);
      		String queryWord = word.trim(); // TODO add escape special chars
          	if (queryWord.length() == 0) {
          		continue;
          	}

           	StringBuilder sbSubQuery = new StringBuilder();
           	for (String language : languages) {
            	if (sbSubQuery.length() > 0) {
            		sbSubQuery.append(" OR ");
            	}
            	sbSubQuery.append(Constants.WORDS_PREFIX);
            	sbSubQuery.append(language);
            	sbSubQuery.append(":(");
            	sbSubQuery.append(queryWord);
            	sbSubQuery.append(")");
            }
           	String qi = "q" + (i + 1);
           	solrQuery.set(qi, sbSubQuery.toString());
          	sbQuery.append(" should=$");
          	sbQuery.append(qi);

          	//if(gt(query($q1),0),"text",
          	sbFields.append("if(gt(query($");
          	sbFields.append(qi);
          	sbFields.append("),0),\"");
          	sbFields.append(queryWord);
          	sbFields.append("\",");
      	}
      	sbFields.append("\"\"");
      	for (int i = 0; i < words.size(); i++) {
      		sbFields.append(")");
      	}
      	solrQuery.set("fl", sbFields.toString());
        sbQuery.append("}");
        solrQuery.set("fq", sbQuery.toString());
          
        solrQuery.set("q", "*:*");
        solrQuery.set("rows", 1000);
        solrQuery.set("omitHeader", true);
        		
        LOGGER.debug("Final Synonyms query: {}", solrQuery);
        
        try {
            QueryResponse response = solrService.getSolrClient().query(Constants.SYNONYMS_COLLECTION, solrQuery);
            
            LOGGER.debug("Searching synonyms took {} ms", response.getQTime());

            SolrDocumentList documents = response.getResults();
            for (SolrDocument document : documents) {
            	List<String> synonyms = (List<String>) document.getFieldValue(Constants.SYNONYMS);
            	String word = (String) document.getFieldValue(Constants.WORD);
            	List<List<String>> list = synonymMap.get(word);
            	if (list == null) {
            		list = new ArrayList<>();
                	synonymMap.put(word, list);
                }
            	list.add(synonyms);
            }
            LOGGER.debug("Found synonyms: {} in synonym set(s) {}", synonymMap, names.toString());
            
        } catch (SolrServerException | IOException e) {
            LOGGER.error("Failed to search synonym set {} Error: {}", names, e.getMessage(), e);
            throw new SynonymException("Failed to search synonym set: " + e.getMessage());
        }
    	return synonymMap;
    }

}
