package leigh.astro.scheduler.ekos;

import leigh.astro.astrometrics.Location;
import leigh.astro.scheduler.Tasking;
import leigh.mecha.cred.Credential;
import leigh.mecha.exec.ProcessExecution;
import leigh.mecha.exec.SshExecutor;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import leigh.mecha.util.Snavig;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.ByteArrayInputStream;
import java.io.StringWriter;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * This class implements support for controlling a remote Ekos installation.
 * <p>
 * This class works over SSH and in companion with a python script on the remote controller, <code>ekos.py</code>, which
 * must be installed in advance. The timezone of the controller must be UTC, or the scheduler will not generate times \
 * properly. This is due to the EKOS schedule xml lacking timezone.
 *
 * @author Alex Leigh
 */
public class EkosController {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(EkosController.class);
    public static final String KEY_TASKINGS = "taskings";
    public static final String KEY_TASKING = "tasking";
    public static final String KEY_CONTROLLER = "controller";
    public static final String KEY_LOCATION = "location";

    private final String hostname;
    private final Credential credential;

    public EkosController(String hostname, Credential credential) {
        this.hostname = hostname;
        this.credential = credential;
    }

    /**
     * Stop the remote scheduler in EKOS. If jobs were running, they will be marked as Aborted. This is the same
     * behavior as hitting the stop button in the EKOS job window on the remote controller.
     */
    public void scheduleStop() throws Exception {
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            ssh.execute("/home/aleigh/dbus/ekos.py", new String[]{"--stop_scheduler"});
        }
    }

    /**
     * Start the remote scheduler in EKOS. This is the same behavior as hitting the start button in the EKOS job
     * window on the remote controller.
     */
    public void scheduleStart() throws Exception {
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            ssh.execute("/home/aleigh/dbus/ekos.py", new String[]{"--start_scheduler"});
        }
    }

    /**
     * Reset the status of all the jobs in EKOS. This is the same behavior as hitting the refresh button on the EKOS
     * kob window in the remote controller.
     */
    public void schedulerReset() throws Exception {
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            ssh.execute("/home/aleigh/dbus/ekos.py", new String[]{"--reset_scheduler"});
        }
    }

    /**
     * Returns <code>true</code> if EKOS is currently running on the remote controller. If a schedule is running,
     * this does not necessarily mean that the telescope is actively operating, rather, that a job is at least
     * scheduled and the EKOS scheduler is running. It could be waiting for the job to complete, or there could be
     * a mechanical or other error with the telescope (for example, it is caught in a plate solving loop).
     */
    public boolean isRunning() throws Exception {
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            ProcessExecution pr = ssh.execute("/home/aleigh/dbus/status.sh");
            String result = pr.drainInput();
            int x = Snavig.convertToInteger(result);
            if (x == 2) return true;
            return false;
        }
    }

    /**
     * Return the temporary ID that will be used on the controller for a file of the given UUID. This is currently
     * setup to generate files in the <code>/tmp</code> directory.
     */
    public static String buildFilename(UUID id) {
        return "/tmp/ekos-" + id + ".xml";
    }

    /**
     * Publish a schedule file to the remote controller.
     */
    public void publishScheduleFile(EkosPlan plan) throws Exception {
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            // First we publish the sequences.
            for (Map.Entry<UUID, String> sequence : plan.getSequences().entrySet()) {
                String seqFn = buildFilename(sequence.getKey());
                byte[] seqData = sequence.getValue().getBytes();
                ssh.upload(seqFn, new ByteArrayInputStream(seqData), seqData.length);
            }
            String schedFn = buildFilename(plan.getId());
            byte[] schedData = plan.getSchedule().getBytes();
            ssh.upload(schedFn, new ByteArrayInputStream(schedData), schedData.length);
        }
    }

    public void loadSchedlule(UUID id) throws Exception {
        String schedFile = buildFilename(id);
        try (SshExecutor ssh = new SshExecutor(hostname, 22, credential.getUsername(), credential.getPassword())) {
            ssh.execute("/home/aleigh/dbus/ekos.py", new String[]{"--load_schedule", schedFile});
        }
    }

    /**
     * Generate a new {@link EkosPlan} for the given taskings. Note that the task configuration is subject to the
     * limitations of the EKOS scheduler, so should only contain taskings which actually should be (and can be)
     * completed in this run.
     * <p>
     * The plan will contain a single schedule entry for each tasking, and a separate sequence file for that
     * tasking. The caller should take care not to duplicate taskings for the same object in the request. The plan
     * will build properly, but EKOS may have problem processing such a plan.
     */
    public EkosPlan generate(List<Tasking> taskings, Location location) {
        Velocity.init();
        VelocityEngine ve = new VelocityEngine();
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());

        Template sequenceTemplate = ve.getTemplate("leigh/astro/scheduler/ekos/sequence.vm");
        // Before we can create the schedule, we have to build sequences.

        HashMap<UUID, String> sequenceFiles = new HashMap<>();

        for (Tasking t : taskings) {
            VelocityContext ctx = new VelocityContext();
            ctx.put(KEY_TASKING, t);
            StringWriter sw = new StringWriter();
            sequenceTemplate.merge(ctx, sw);
            sequenceFiles.put(t.getId(), sw.getBuffer().toString());
        }

        Template schedTemplate = ve.getTemplate("leigh/astro/scheduler/ekos/schedule.vm");

        VelocityContext context = new VelocityContext();
        context.put(KEY_TASKINGS, taskings);
        context.put(KEY_CONTROLLER, this);
        context.put(KEY_LOCATION, location);
        StringWriter sw = new StringWriter();
        schedTemplate.merge(context, sw);

        // We need to know the earliest tasking.
        Instant startTime = null;
        for (Tasking t : taskings) {
            if (startTime == null) {
                startTime = t.getMinTime();
            } else {
                if (t.getMinTime().isBefore(startTime)) startTime = t.getMinTime();
            }
        }

        return new EkosPlan(sw.getBuffer().toString(), sequenceFiles, startTime);
    }

}
