package ctbrec.recorder.server;

import static javax.servlet.http.HttpServletResponse.*;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;

import ctbrec.Config;
import ctbrec.GlobalThreadPool;
import ctbrec.Model;
import ctbrec.ModelGroup;
import ctbrec.Recording;
import ctbrec.io.BandwidthMeter;
import ctbrec.io.FileJsonAdapter;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.io.UuidJSonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;

public class RecorderServlet extends AbstractCtbrecServlet {

    private static final transient Logger LOG = LoggerFactory.getLogger(RecorderServlet.class);

    private Recorder recorder;

    private List<Site> sites;

    public RecorderServlet(Recorder recorder, List<Site> sites) {
        this.recorder = recorder;
        this.sites = sites;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(SC_OK);
        resp.setContentType("application/json");

        String json = null;
        try {
            PrintWriter responseWriter = resp.getWriter();
            json = body(req);
            boolean isRequestAuthenticated = checkAuthentication(req, json);
            if (!isRequestAuthenticated) {
                sendError(resp, SC_UNAUTHORIZED, "{\"status\": \"error\", \"msg\": \"HMAC does not match\"}");
                return;
            }

            LOG.debug("Request: {}", json);
            Moshi moshi = new Moshi.Builder()
                    .add(Instant.class, new InstantJsonAdapter())
                    .add(Model.class, new ModelJsonAdapter(sites))
                    .add(File.class, new FileJsonAdapter())
                    .add(UUID.class, new UuidJSonAdapter())
                    .build();
            JsonAdapter<Request> requestAdapter = moshi.adapter(Request.class);
            Request request = requestAdapter.fromJson(json);
            if (request.action == null) {
                sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"action is missing\"}");
                return;
            }
            switch (request.action) {
            case "start":
                LOG.debug("Starting recording for model {} - {}", request.model.getName(), request.model.getUrl());
                LOG.trace("Model marked: {}", request.model.isMarkedForLaterRecording());
                LOG.trace("Model paused: {}", request.model.isSuspended());
                LOG.trace("Model until: {}", request.model.getRecordUntil().equals(Instant.ofEpochMilli(Model.RECORD_INDEFINITELY)) ? "no limit" : request.model.getRecordUntil());
                LOG.trace("Model after: {}", request.model.getRecordUntilSubsequentAction());
                recorder.addModel(request.model);
                String response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
                responseWriter.write(response);
                break;
            case "startByUrl":
                LOG.debug("Starting recording for model {}", request.model.getUrl());
                startByUrl(request);
                response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
                responseWriter.write(response);
                break;
            case "startByName":
                LOG.debug("Starting recording for model {}", request.model.getUrl());
                startByName(request);
                response = "{\"status\": \"success\", \"msg\": \"Recording started\"}";
                responseWriter.write(response);
                break;
            case "stop":
                GlobalThreadPool.submit(() -> {
                    try {
                        recorder.stopRecording(request.model);
                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
                        LOG.error("Couldn't stop recording for model {}", request.model, e);
                    }
                });
                response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
                responseWriter.write(response);
                break;
            case "stopAt":
                GlobalThreadPool.submit(() -> {
                    try {
                        recorder.stopRecordingAt(request.model);
                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
                        LOG.error("Couldn't stop recording for model {}", request.model, e);
                    }
                });
                response = "{\"status\": \"success\", \"msg\": \"Stopping recording\"}";
                responseWriter.write(response);
                break;
            case "list":
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of models\", \"models\": [");
                JsonAdapter<Model> modelAdapter = new ModelJsonAdapter();
                List<Model> models = recorder.getModels();
                for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
                    Model model = iterator.next();
                    responseWriter.write(modelAdapter.toJson(model));
                    if (iterator.hasNext()) {
                        responseWriter.write(',');
                    }
                }
                responseWriter.write("]}");
                break;
            case "listOnline":
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of online models\", \"models\": [");
                modelAdapter = new ModelJsonAdapter();
                models = recorder.getOnlineModels();
                for (Iterator<Model> iterator = models.iterator(); iterator.hasNext();) {
                    Model model = iterator.next();
                    responseWriter.write(modelAdapter.toJson(model));
                    if (iterator.hasNext()) {
                        responseWriter.write(',');
                    }
                }
                responseWriter.write("]}");
                break;
            case "recordings":
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
                JsonAdapter<Recording> recAdapter = moshi.adapter(Recording.class);
                List<Recording> recordings = recorder.getRecordings();
                for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
                    Recording recording = iterator.next();
                    String recJSON = recAdapter.toJson(recording);
                    LOG.debug("Rec: {}", recJSON);
                    responseWriter.write(recJSON);
                    if (iterator.hasNext()) {
                        responseWriter.write(',');
                    }
                }
                responseWriter.write("]}");
                break;
            case "delete":
                recorder.delete(request.recording);
                recAdapter = moshi.adapter(Recording.class);
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
                responseWriter.write(recAdapter.toJson(request.recording));
                responseWriter.write("]}");
                break;
            case "pin":
                recorder.pin(request.recording);
                recAdapter = moshi.adapter(Recording.class);
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
                responseWriter.write(recAdapter.toJson(request.recording));
                responseWriter.write("]}");
                break;
            case "unpin":
                recorder.unpin(request.recording);
                recAdapter = moshi.adapter(Recording.class);
                responseWriter.write("{\"status\": \"success\", \"msg\": \"Note saved\", \"recordings\": [");
                responseWriter.write(recAdapter.toJson(request.recording));
                responseWriter.write("]}");
                break;
            case "setNote":
                recorder.setNote(request.recording, request.recording.getNote());
                recAdapter = moshi.adapter(Recording.class);
                responseWriter.write("{\"status\": \"success\", \"msg\": \"List of recordings\", \"recordings\": [");
                responseWriter.write(recAdapter.toJson(request.recording));
                responseWriter.write("]}");
                break;
            case "rerunPostProcessing":
                recorder.rerunPostProcessing(request.recording);
                responseWriter.write("{\"status\": \"success\", \"msg\": \"Post-Processing triggered\"}");
                break;
            case "switch":
                recorder.switchStreamSource(request.model);
                response = "{\"status\": \"success\", \"msg\": \"Resolution switched\"}";
                responseWriter.write(response);
                break;
            case "suspend":
                LOG.debug("Suspend recording for model {} - {}", request.model.getName(), request.model.getUrl());
                GlobalThreadPool.submit(() -> {
                    try {
                        recorder.suspendRecording(request.model);
                    } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) {
                        LOG.error("Couldn't suspend recording for model {}", request.model, e);
                    }
                });
                response = "{\"status\": \"success\", \"msg\": \"Suspending recording\"}";
                responseWriter.write(response);
                break;
            case "resume":
                LOG.debug("Resume recording for model {} - {}", request.model.getName(), request.model.getUrl());
                recorder.resumeRecording(request.model);
                response = "{\"status\": \"success\", \"msg\": \"Recording resumed\"}";
                responseWriter.write(response);
                break;
            case "space":
                JSONObject jsonResponse = new JSONObject();
                jsonResponse.put("status", "success");
                jsonResponse.put("spaceTotal", recorder.getTotalSpaceBytes());
                jsonResponse.put("spaceFree", recorder.getFreeSpaceBytes());
                jsonResponse.put("throughput", BandwidthMeter.getThroughput());
                jsonResponse.put("throughputTimeframe", BandwidthMeter.getTimeframe().toMillis());
                jsonResponse.put("minimumSpaceLeftInBytes", Config.getInstance().getSettings().minimumSpaceLeftInBytes);
                responseWriter.write(jsonResponse.toString());
                break;
            case "changePriority":
                recorder.priorityChanged(request.model);
                response = "{\"status\": \"success\"}";
                responseWriter.write(response);
                break;
            case "pauseRecorder":
                recorder.pause();
                response = "{\"status\": \"success\"}";
                responseWriter.write(response);
                break;
            case "resumeRecorder":
                recorder.resume();
                response = "{\"status\": \"success\"}";
                responseWriter.write(response);
                break;
            case "saveModelGroup":
                recorder.saveModelGroup(request.modelGroup);
                sendModelGroups(resp, recorder.getModelGroups());
                break;
            case "deleteModelGroup":
                recorder.deleteModelGroup(request.modelGroup);
                sendModelGroups(resp, recorder.getModelGroups());
                break;
            case "listModelGroups":
                sendModelGroups(resp, recorder.getModelGroups());
                break;
            case "markForLater":
                LOG.debug("Mark model {} for later recording", request.model.getName());
                response = "{\"status\": \"success\", \"msg\": \"Model marked for later recording\"}";
                responseWriter.write(response);
                recorder.markForLaterRecording(request.model, true);
                break;
            case "unmarkForLater":
                LOG.debug("Unmark model {} for later recording", request.model.getName());
                response = "{\"status\": \"success\", \"msg\": \"Model has been unmarked\"}";
                responseWriter.write(response);
                recorder.markForLaterRecording(request.model, false);
                break;
            default:
                sendError(resp, SC_BAD_REQUEST, "{\"status\": \"error\", \"msg\": \"Unknown action [" + request.action + "]\"}");
                break;
            }
        } catch(Exception e) {
            resp.setStatus(SC_INTERNAL_SERVER_ERROR);
            JSONObject response = new JSONObject();
            response.put("status", "error");
            response.put("msg", e.getMessage());
            resp.getWriter().write(response.toString());
            LOG.error("Unexpected error", e);
            if (json != null) {
                LOG.debug("Request: {}", json);
            }
        }
    }

    private void sendError(HttpServletResponse resp, int code, String body) throws IOException {
        resp.setStatus(code);
        resp.getWriter().write(body);
    }

    private void sendModelGroups(HttpServletResponse resp, Set<ModelGroup> modelGroups) throws IOException {
        JSONObject jsonResponse = new JSONObject();
        jsonResponse.put("status", "success");
        jsonResponse.put("groups", modelGroups);
        resp.getWriter().write(jsonResponse.toString());
    }

    private void startByUrl(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        String url = request.model.getUrl();
        for (Site site : sites) {
            Model model = site.createModelFromUrl(url);
            if (model != null) {
                recorder.addModel(model);
                return;
            }
        }
        throw new IllegalArgumentException("No site found to record " + url);
    }

    private void startByName(Request request) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        String[] input = request.model.getUrl().split(":");
        if (input.length != 2) {
            throw new IllegalArgumentException("Invalid input. Should be site:model");
        }
        String siteName = input[0];
        String modelName = input[1];
        for (Site site : sites) {
            if (Objects.equals(siteName.toLowerCase(), site.getClass().getSimpleName().toLowerCase())) {
                Model m = site.createModel(modelName);
                recorder.addModel(m);
                return;
            }
        }
        throw new IllegalArgumentException("No site found to record " + request.model.getUrl());
    }

    private static class Request {
        public String action;
        public Model model;
        public Recording recording;
        public ModelGroup modelGroup;
    }
}
