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

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import com.softcorporation.sandi.base.model.Client;
import com.softcorporation.sandi.base.model.Collection;
import com.softcorporation.sandi.base.service.ClientService;
import com.softcorporation.sandi.base.service.CollectionService;
import com.softcorporation.sandi.base.util.CollectionException;
import com.softcorporation.sandi.base.util.Constants;
import com.softcorporation.sandi.index.model.TypeAheadEntry;
import com.softcorporation.util.StringUtil;

public abstract class TypeAheadIndexerService {

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

    public static final String TYPE_AHEAD_PREFIX = "_ta_";
    protected static final String LAST_RUN_DOC_ID = "_";
    protected static final String LAST_RUN_FIELD = "_last_run";
    protected static final String TA_TEXT_FIELD = "text";
    protected static final String TA_COUNT_FIELD = "count";
    protected static final int BATCH_SIZE = 100;
    protected static final int WEIGHT_HIGH = 3;
    protected static final int WEIGHT_LOW = 2;
    protected static final int WEIGHT_CONTENT = 1;
    protected static final SimpleDateFormat SOLR_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

    static {
        SOLR_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
    }

    protected static class DocumentPage {
        public final List<Map<String, Object>> docs;
        public final long numFound;

        public DocumentPage(List<Map<String, Object>> docs, long numFound) {
            this.docs = docs;
            this.numFound = numFound;
        }
    }

    @Value("${sandi.typeahead.indexing.ngram.max:1000000}")
    private int ngramMax;

    @Autowired
    private ClientService clientService;

    @Autowired
    private CollectionService collectionService;

    private final AtomicBoolean running = new AtomicBoolean(false);
    private final AtomicBoolean stopRequested = new AtomicBoolean(false);

    // --- abstract document operations ---

    protected abstract Date getLastRunDate(String taCollection);

    protected abstract DocumentPage fetchDocuments(String clientId, String collection, String[] fields, Date createdAfter,
            int start, int rows) throws IOException;

    protected abstract void flushToCollection(String taCollection, Map<String, TypeAheadEntry> map) throws IOException;

    protected abstract void commit(String taCollection) throws IOException;

    protected abstract void saveLastRunDate(String taCollection, Date date) throws IOException;

    // --- public API ---

    public boolean isRunning() {
        return running.get();
    }

    public void stop() {
        stopRequested.set(true);
        LOGGER.info("Type-ahead indexing stop requested");
    }

    public void indexAllTypeAhead() {
        if (!running.compareAndSet(false, true)) {
            LOGGER.debug("Type-ahead indexing already running, skipping");
            return;
        }
        stopRequested.set(false);

        try {
            LOGGER.info("Starting type-ahead indexing for all clients");
            long startTime = System.currentTimeMillis();
            int totalClients = 0;

            List<Client> clients = clientService.listClients();
            LOGGER.info("Found {} clients to process", clients.size());

            for (Client client : clients) {
                if (stopRequested.get()) {
                    LOGGER.info("Type-ahead indexing stopped by request");
                    break;
                }
                if (!client.isActive()) {
                    LOGGER.debug("Skipping inactive client: {}", client.getClientId());
                    continue;
                }
                try {
                    indexTypeAheadForClient(client);
                    totalClients++;
                } catch (Exception e) {
                    LOGGER.error("Failed to index type-ahead for client '{}': {}", client.getClientId(), e.getMessage(), e);
                }
            }

            long duration = System.currentTimeMillis() - startTime;
            LOGGER.info("Type-ahead indexing completed. Processed {} clients in {}ms", totalClients, duration);
        } catch (Exception e) {
            LOGGER.error("Failed to index type-ahead {}", e.getMessage(), e);
        } finally {
            running.set(false);
        }
    }

    // --- core indexing logic ---

    private void indexTypeAheadForClient(Client client) throws CollectionException, IOException {
        String clientId = client.getClientId();
        String clientCollection = client.getCollection();

        if (clientCollection == null || clientCollection.isEmpty()) {
            LOGGER.warn("Client '{}' has no collection configured", clientId);
            return;
        }

        if (!collectionService.hasCollection(clientCollection)) {
            LOGGER.warn("Client collection '{}' does not exist", clientCollection);
            return;
        }

        String taCollection = getTypeAheadCollectionName(clientCollection);
        if (!collectionService.hasCollection(taCollection)) {
            createTypeAheadCollection(clientCollection);
        }

        Date lastRunDate = getLastRunDate(taCollection);
        Date queryStartDate = new Date();
        LOGGER.info("Processing client '{}' collection '{}', last run: {}", clientId, clientCollection, lastRunDate);

        String[] queryFields = buildQueryFields(client);
        if (queryFields.length == 0) {
            LOGGER.info("Client '{}' has no fields configured for type-ahead", clientId);
            return;
        }

        Map<String, TypeAheadEntry> map = new HashMap<>();
        int start = 0;
        long totalDocs = 0;
        boolean stopped = false;

        while (true) {
            if (stopRequested.get()) {
                stopped = true;
                LOGGER.info("Type-ahead indexing stopped by request during client '{}'", clientId);
                break;
            }

            DocumentPage page = fetchDocuments(clientId, clientCollection, queryFields, lastRunDate, start, BATCH_SIZE);

            if (page == null || page.docs.isEmpty()) {
                break;
            }

            for (Map<String, Object> doc : page.docs) {
                collectNgrams(doc, client.getFieldsHigh(), WEIGHT_HIGH, map);
                collectNgrams(doc, client.getFieldsLow(), WEIGHT_LOW, map);
                collectNgrams(doc, client.getFieldsContent(), WEIGHT_CONTENT, map);
            }

            totalDocs += page.docs.size();
            start += BATCH_SIZE;

            if (map.size() > ngramMax) {
                LOGGER.info("Ngram map size {} exceeded limit {}, flushing to collection", map.size(), ngramMax);
                flushToCollection(taCollection, map);
                map.clear();
            }

            if (start >= page.numFound) {
                break;
            }
        }

        if (!stopped) {
            if (!map.isEmpty()) {
                flushToCollection(taCollection, map);
            }
            saveLastRunDate(taCollection, queryStartDate);
            commit(taCollection);
            LOGGER.info("Type-ahead indexing completed for client '{}', processed {} documents", clientId, totalDocs);
        }
    }

    private String[] buildQueryFields(Client client) {
        Set<String> fields = new LinkedHashSet<>();
        if (client.getFieldsHigh() != null) {
            for (String f : client.getFieldsHigh()) fields.add(f);
        }
        if (client.getFieldsLow() != null) {
            for (String f : client.getFieldsLow()) fields.add(f);
        }
        if (client.getFieldsContent() != null) {
            for (String f : client.getFieldsContent()) fields.add(f);
        }
        return fields.toArray(new String[0]);
    }

    @SuppressWarnings("unchecked")
    private void collectNgrams(Map<String, Object> doc, String[] fields, int weight, Map<String, TypeAheadEntry> map) {
        if (fields == null) return;
        Set<String> uniqueNgrams = new HashSet<>();
        for (String field : fields) {
            Object value = doc.get(field);
            if (value == null) continue;
            java.util.Collection<Object> values;
            if (value instanceof java.util.Collection) {
                values = (java.util.Collection<Object>) value;
            } else {
                values = List.of(value);
            }
            for (Object val : values) {
                if (val == null) continue;
                List<String> ngrams = createShingles(val.toString(), 1, 4);
                for (String ngram : ngrams) {
                    if (ngram.length() >= 2 && ngram.length() <= 100) {
                        uniqueNgrams.add(ngram);
                    }
                }
            }
        }
        for (String ngram : uniqueNgrams) {
            TypeAheadEntry entry = map.get(ngram);
            if (entry == null) {
                map.put(ngram, new TypeAheadEntry(ngram, weight));
            } else {
                entry.addCount(weight);
            }
        }
    }

    // --- utilities ---

    public void createTypeAheadCollection(String clientCollection) throws CollectionException {
        String taCollection = getTypeAheadCollectionName(clientCollection);
        if (collectionService.hasCollection(taCollection)) {
            LOGGER.info("Type-ahead collection '{}' already exists", taCollection);
            return;
        }
        LOGGER.info("Creating type-ahead collection '{}' for '{}'", taCollection, clientCollection);
        Collection collection = new Collection(taCollection, Constants.TYPEAHEAD_CONFIGURATION);
        collection.setShards(1); //TODO
        collection.setReplicas(1);
        collectionService.createCollection(collection);
        LOGGER.info("Successfully created type-ahead collection '{}'", taCollection);
    }

    public void deleteTypeAheadCollection(String clientCollection) throws CollectionException {
        String taCollection = getTypeAheadCollectionName(clientCollection);
        if (!collectionService.hasCollection(taCollection)) {
            LOGGER.info("Type-ahead collection '{}' does not exist", taCollection);
            return;
        }
        LOGGER.info("Deleting type-ahead collection '{}'", taCollection);
        collectionService.deleteCollection(taCollection);
        LOGGER.info("Successfully deleted type-ahead collection '{}'", taCollection);
    }

    public String getTypeAheadCollectionName(String clientCollection) {
        return TYPE_AHEAD_PREFIX + clientCollection;
    }

    public boolean isTypeAheadCollection(String collectionName) {
        return collectionName != null && collectionName.startsWith(TYPE_AHEAD_PREFIX);
    }

    public String getSourceCollectionName(String typeAheadCollection) {
        if (isTypeAheadCollection(typeAheadCollection)) {
            return typeAheadCollection.substring(TYPE_AHEAD_PREFIX.length());
        }
        return null;
    }

    public static List<String> createShingles(String text, int shingleSize) {
        List<String> shingles = new ArrayList<>();
        if (text == null || text.trim().isEmpty() || shingleSize < 1) {
            return shingles;
        }
        String[] tokens = text.trim().split("\\s+");
        if (tokens.length < shingleSize) {
            return shingles;
        }
        for (int i = 0; i <= tokens.length - shingleSize; i++) {
            StringBuilder shingle = new StringBuilder();
            for (int j = 0; j < shingleSize; j++) {
                if (j > 0) shingle.append(" ");
                shingle.append(tokens[i + j]);
            }
            shingles.add(shingle.toString());
        }
        return shingles;
    }

    public static List<String> createShingles(String text, int minShingleSize, int maxShingleSize) {
    	text = StringUtil.cleanText(text).toLowerCase();
        List<String> allShingles = new ArrayList<>();
        for (int size = minShingleSize; size <= maxShingleSize; size++) {
            allShingles.addAll(createShingles(text, size));
        }
        return allShingles;
    }
}
