/**
 * 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.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.softcorporation.sandi.base.util.Constants;
import com.softcorporation.sandi.index.model.IndexingJob;
import com.softcorporation.sandi.index.model.IndexingJob.JobStatus;
import com.softcorporation.sandi.index.service.processors.JsonlProcessingService;
import com.softcorporation.sandi.index.service.processors.SiteProcessingService;
import com.softcorporation.sandi.index.service.processors.SitemapProcessingService;
import com.softcorporation.sandi.index.service.processors.TxtlProcessingService;
import com.softcorporation.sandi.index.util.IndexerException;

@Service
public class IndexingJobScheduler {

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

    /**
     * Process files/urls containing json documents
     */
    @Autowired
    private JsonlProcessingService jsonlProcessingService;

    /**
     * Process files/urls containing text documents
     */
    @Autowired
    private TxtlProcessingService txtlProcessingService;

    /**
     * Process files/urls containing web site documents
     */
    @Autowired
    private SiteProcessingService siteProcessingService;
    
    /**
     * Process xml sitemap containing web site documents
     */
    @Autowired
    private SitemapProcessingService sitemapProcessingService;

    private final ConcurrentMap<String, IndexingJob> scheduledJobs = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, IndexingJob> runningJobs = new ConcurrentHashMap<>();
    private final ConcurrentMap<String, IndexingJob> completedJobs = new ConcurrentHashMap<>();

//    @Scheduled(fixedDelay = 10000)
    @Scheduled(fixedDelay = 1000)
    public void checkAndExecuteJobs() {
        LocalDateTime now = LocalDateTime.now();
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Checking for scheduled jobs to execute at {}", now);
        }
        List<IndexingJob> jobsToExecute = new ArrayList<>();

        // Find jobs that are ready to run
        for (IndexingJob job : scheduledJobs.values()) {
            if (job.getStatus() == JobStatus.SCHEDULED && 
            		job.getScheduledTime() != null && 
                    LocalDateTime.now().isAfter(job.getScheduledTime())) {
                jobsToExecute.add(job);
            }
        }

        if (!jobsToExecute.isEmpty()) {
            LOGGER.info("Found {} jobs ready to execute", jobsToExecute.size());
            
            for (IndexingJob job : jobsToExecute) {
                try {
                    // Move job from scheduled to running
                    scheduledJobs.remove(job.getJobId());
                    job.setStatus(JobStatus.RUNNING);
                    job.setStartedTime(LocalDateTime.now());
                    runningJobs.put(job.getJobId(), job);

                    LOGGER.info("Starting execution of job: {}", job.getJobId());
                    
                    // Execute the job asynchronously
                    executeJobAsync(job);

                } catch (Exception e) {
                    LOGGER.error("Error starting job execution for {}: {}", job.getJobId(), e.getMessage(), e);
                    job.setStatus(JobStatus.FAILED);
                    job.setMessage("Failed to start job: " + e.getMessage());
                    job.setCompletedTime(LocalDateTime.now());
                    runningJobs.remove(job.getJobId());
                    completedJobs.put(job.getJobId(), job);
                }
            }
        }

        // Log current job statistics
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Job statistics - Scheduled: {}, Running: {}, Completed: {}", 
                scheduledJobs.size(), runningJobs.size(), completedJobs.size());
        }
    }

    @Async
    public void executeJobAsync(IndexingJob job) {
        LOGGER.info("Executing indexing job: {}", job);
        
        try {
        	String jobType = job.getJobType();
        	if (Constants.JOB_TYPE_JSONL.equals(jobType)) {
            	jsonlProcessingService.executeIndexingJob(job);
        	} else if (Constants.JOB_TYPE_TXTL.equals(jobType)) {
               	txtlProcessingService.executeIndexingJob(job);
           	} else if (Constants.JOB_TYPE_SITE.equals(jobType)) {
        		siteProcessingService.executeIndexingJob(job);
        	} else if (Constants.JOB_TYPE_SITEMAP.equals(jobType)) {
        		sitemapProcessingService.executeIndexingJob(job);
            } else {
                throw new IndexerException("Unknown job type: " + jobType);
            }
        	
            // Job completed successfully
            job.setStatus(JobStatus.COMPLETED);
            job.setCompletedTime(LocalDateTime.now());
            LOGGER.info("Job {} completed successfully", job.getJobId());

        } catch (Exception e) {
            // Job failed
            LOGGER.error("Job {} failed: {}", job.getJobId(), e.getMessage(), e);
            job.setStatus(JobStatus.FAILED);
            job.setMessage("Job execution failed: " + e.getMessage());
            job.setCompletedTime(LocalDateTime.now());
        } finally {
            // Move job from running to completed
            runningJobs.remove(job.getJobId());
            completedJobs.put(job.getJobId(), job);
        }
    }

    public synchronized boolean addJob(IndexingJob job) {
        if (job == null || job.getJobId() == null) {
            LOGGER.warn("Cannot add null job or job with null ID");
            return false;
        }

        String jobId = job.getJobId();
        if (runningJobs.containsKey(jobId) || 
            	// scheduledJobs.containsKey(jobId) || 
                completedJobs.containsKey(jobId)) {
            LOGGER.warn("Job with ID {} already exists", jobId);
            return false;
        }

        scheduledJobs.put(jobId, job);
        job.setStatus(JobStatus.SCHEDULED);
        LOGGER.info("Added job to scheduler: {}", job);
        return true;
    }

    public synchronized IndexingJob getJob(String jobId) {
        IndexingJob job = scheduledJobs.get(jobId);
        if (job == null) {
            job = runningJobs.get(jobId);
        }
        if (job == null) {
            job = completedJobs.get(jobId);
        }
        return job;
    }

    public synchronized boolean cancelJob(String jobId) {
        IndexingJob job = scheduledJobs.remove(jobId);
        if (job != null) {
            job.setStatus(JobStatus.CANCELLED);
            job.setMessage("Job cancelled by user");
            job.setCompletedTime(LocalDateTime.now());
            completedJobs.put(jobId, job);
            LOGGER.info("Cancelled scheduled job: {}", jobId);
            return true;
        }
        
        // Cannot cancel running jobs for now
        job = runningJobs.get(jobId);
        if (job != null) {
        	
            LOGGER.warn("Cannot cancel running job: {}", jobId);
            return false;
        }
        
        LOGGER.warn("Job not found for cancellation: {}", jobId);
        return false;
    }

    public List<IndexingJob> getAllJobs() {
        List<IndexingJob> allJobs = new ArrayList<>();
        allJobs.addAll(scheduledJobs.values());
        allJobs.addAll(runningJobs.values());
        allJobs.addAll(completedJobs.values());
        return allJobs;
    }

    public List<IndexingJob> getJobsByStatus(JobStatus status) {
        List<IndexingJob> jobs = new ArrayList<>();
        
        switch (status) {
            case SCHEDULED:
                jobs.addAll(scheduledJobs.values());
                break;
            case RUNNING:
                jobs.addAll(runningJobs.values());
                break;
            case COMPLETED:
            case FAILED:
            case CANCELLED:
                jobs.addAll(completedJobs.values());
                jobs.removeIf(job -> job.getStatus() != status);
                break;
        }
        
        return jobs;
    }

    public int getScheduledJobCount() {
        return scheduledJobs.size();
    }

    public int getRunningJobCount() {
        return runningJobs.size();
    }

    public int getCompletedJobCount() {
        return completedJobs.size();
    }
}