/*
 * Copyright (C) Photon Vision.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package org.photonvision.server;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.http.Context;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.io.FileUtils;
import org.photonvision.common.configuration.ConfigManager;
import org.photonvision.common.configuration.NetworkConfig;
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
import org.photonvision.common.hardware.HardwareManager;
import org.photonvision.common.hardware.Platform;
import org.photonvision.common.hardware.metrics.MetricsPublisher;
import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger;
import org.photonvision.common.networking.NetworkManager;
import org.photonvision.common.util.ShellExec;
import org.photonvision.common.util.TimedTaskManager;
import org.photonvision.common.util.file.ProgramDirectoryUtilities;
import org.photonvision.vision.processes.VisionModuleManager;
import org.photonvision.vision.target.TargetModel;

public class RequestHandler {
    private static final Logger logger = new Logger(RequestHandler.class, LogGroup.WebServer);

    private static final ObjectMapper kObjectMapper = new ObjectMapper();

    public static void onSettingUpload(Context ctx) {
        var file = ctx.uploadedFile("zipData");
        if (file != null) {
            // Copy the file from the client to a temporary location
            var tempFilePath =
                    new File(Path.of(System.getProperty("java.io.tmpdir"), file.getFilename()).toString());
            tempFilePath.getParentFile().mkdirs();
            try {
                FileUtils.copyInputStreamToFile(file.getContent(), tempFilePath);
            } catch (IOException e) {
                logger.error("Exception while uploading settings file to temp folder!");
                e.printStackTrace();
                return;
            }

            // Process the file by its extension
            if (file.getExtension().contains("zip")) {
                // .zip files are assumed to be full packages of configuration files
                logger.debug("Processing uploaded settings zip " + file.getFilename());
                ConfigManager.saveUploadedSettingsZip(tempFilePath);

            } else if (file.getFilename().equals(ConfigManager.HW_CFG_FNAME)) {
                // Filenames matching the hardware config .json file are assumed to be
                // hardware config .json's
                logger.debug("Processing uploaded hardware config " + file.getFilename());
                ConfigManager.getInstance().saveUploadedHardwareConfig(tempFilePath.toPath());

            } else if (file.getFilename().equals(ConfigManager.HW_SET_FNAME)) {
                // Filenames matching the hardware settings .json file are assumed to be
                // hardware settings.json's
                logger.debug("Processing uploaded hardware settings" + file.getFilename());
                ConfigManager.getInstance().saveUploadedHardwareSettings(tempFilePath.toPath());

            } else if (file.getFilename().equals(ConfigManager.NET_SET_FNAME)) {
                // Filenames matching the network config .json file are assumed to be
                // network config .json's
                logger.debug("Processing uploaded network config " + file.getFilename());
                ConfigManager.getInstance().saveUploadedNetworkConfig(tempFilePath.toPath());

            } else {
                logger.error(
                        "Couldn't apply provided settings file - did not recognize "
                                + file.getFilename()
                                + " as a supported file.");
                ctx.status(500);
                return;
            }

            ctx.status(200);
            logger.info("Settings uploaded, going down for restart.");
            restartProgram();
        } else {
            logger.error("Couldn't read uploaded file! Ignoring.");
            ctx.status(500);
        }
    }

    public static void onOfflineUpdate(Context ctx) {
        logger.info("Handling offline update .jar upload...");
        var file = ctx.uploadedFile("jarData");
        logger.info("New .jar uploaded successfully.");

        if (file != null) {
            if (Platform.isRaspberryPi()) {
                try {
                    Path filePath =
                            Paths.get(ProgramDirectoryUtilities.getProgramDirectory(), "photonvision.jar");
                    File targetFile = new File(filePath.toString());
                    var stream = new FileOutputStream(targetFile);

                    logger.info(
                            "Streaming user-provided " + file.getFilename() + " into " + targetFile.toString());

                    file.getContent().transferTo(stream);
                    stream.close();

                    ctx.status(200);
                    logger.info("New .jar in place, going down for restart...");
                    restartProgram();

                } catch (FileNotFoundException e) {
                    logger.error(
                            ".jar of this program could not be found. How the heck this program started in the first place is a mystery.");
                    ctx.status(500);
                } catch (IOException e) {
                    logger.error("Could not overwrite the .jar for this instance of photonvision.");
                    ctx.status(500);
                }

            } else {
                logger.error("Hot .jar replace currently only supported on Raspberry pi. Ignoring.");
                ctx.status(500);
            }

        } else {
            logger.error("Couldn't read provided file for new .jar! Ignoring.");
            ctx.status(500);
        }
    }

    @SuppressWarnings("unchecked")
    public static void onGeneralSettings(Context context) throws JsonProcessingException {
        Map<String, Object> map =
                (Map<String, Object>) kObjectMapper.readValue(context.body(), Map.class);

        var networkConfig = NetworkConfig.fromHashMap(map);
        ConfigManager.getInstance().setNetworkSettings(networkConfig);
        ConfigManager.getInstance().requestSave();
        NetworkManager.getInstance().reinitialize();
        NetworkTablesManager.getInstance().setConfig(networkConfig);

        context.status(200);
    }

    @SuppressWarnings("unchecked")
    public static void onCameraSettingsSave(Context context) {
        try {
            var settingsAndIndex = kObjectMapper.readValue(context.body(), Map.class);
            logger.info("Got cam setting json from frontend!\n" + settingsAndIndex.toString());
            var settings = (HashMap<String, Object>) settingsAndIndex.get("settings");
            int index = (Integer) settingsAndIndex.get("index");

            // The only settings we actually care about are FOV
            var fov = Double.parseDouble(settings.get("fov").toString());

            logger.info(String.format("Setting camera %s's fov to %s", index, fov));
            var module = VisionModuleManager.getInstance().getModule(index);
            module.setFov(fov);
            module.saveModule();
        } catch (JsonProcessingException e) {
            logger.error("Got invalid camera setting JSON from frontend!");
            e.printStackTrace();
        }
    }

    public static void onSettingsDownload(Context ctx) {
        logger.info("exporting settings to download...");
        try {
            var zip = ConfigManager.getInstance().getSettingsFolderAsZip();
            var stream = new FileInputStream(zip);
            logger.info("Uploading settings with size " + stream.available());
            ctx.result(stream);
            ctx.contentType("application/zip");
            ctx.header("Content-Disposition: attachment; filename=\"photonvision-settings-export.zip\"");
            ctx.status(200);
        } catch (IOException e) {
            e.printStackTrace();
            ctx.status(501);
            logger.error("Got bad recode from zip to byte");
        }
    }

    public static void onCalibrationEnd(Context ctx) {
        logger.info("Calibrating camera! This will take a long time...");
        var index = Integer.parseInt(ctx.body());
        var calData = VisionModuleManager.getInstance().getModule(index).endCalibration();
        if (calData == null) {
            ctx.status(500);
            return;
        }

        ctx.result(String.valueOf(calData.standardDeviation));
        ctx.status(200);
        logger.info("Camera calibrated!");
    }

    public static void restartDevice(Context ctx) {
        ctx.status(HardwareManager.getInstance().restartDevice() ? 200 : 500);
    }

    public static void restartProgram(Context ctx) {
        restartProgram();
    }

    public static void restartProgram() {
        TimedTaskManager.getInstance().addOneShotTask(RequestHandler::restartProgramInternal, 0);
    }

    /**
     * Note that this doesn't actually restart the program itself -- instead, it relies on systemd or
     * an equivalent.
     */
    public static void restartProgramInternal() {
        if (Platform.isRaspberryPi()) {
            try {
                new ShellExec().executeBashCommand("systemctl restart photonvision.service");
            } catch (IOException e) {
                logger.error("Could not restart device!", e);
                System.exit(0);
            }
        } else {
            System.exit(0);
        }
    }

    public static void setCameraNickname(Context ctx) {
        try {
            var data = kObjectMapper.readValue(ctx.body(), HashMap.class);
            String name = String.valueOf(data.get("name"));
            int idx = Integer.parseInt(String.valueOf(data.get("cameraIndex")));
            VisionModuleManager.getInstance().getModule(idx).setCameraNickname(name);
            ctx.status(200);
            return;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        ctx.status(500);
    }

    public static void uploadPnpModel(Context ctx) {
        UITargetData data;
        try {
            data = kObjectMapper.readValue(ctx.body(), UITargetData.class);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            ctx.status(500);
            return;
        }

        VisionModuleManager.getInstance().getModule(data.index).setTargetModel(data.targetModel);
        ctx.status(200);
    }

    public static void sendMetrics(Context ctx) {
        MetricsPublisher.getInstance().publish();
        // TimedTaskManager.getInstance().addOneShotTask(() -> RoborioFinder.getInstance().findRios(),
        // 0);
        ctx.status(200);
    }

    public static class UITargetData {
        public int index;
        public TargetModel targetModel;
    }
}
