package ctbrec.recorder;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

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.Hmac;
import ctbrec.Model;
import ctbrec.Recording;
import ctbrec.event.EventBusHolder;
import ctbrec.event.RecordingStateChangedEvent;
import ctbrec.io.HttpClient;
import ctbrec.io.HttpException;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.RequestBody;
import okhttp3.Response;

public class RemoteRecorder implements Recorder {

    private static final String SUCCESS = "success";

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

    public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    private Moshi moshi = new Moshi.Builder().add(Instant.class, new InstantJsonAdapter()).add(Model.class, new ModelJsonAdapter()).build();
    private JsonAdapter<ModelListResponse> modelListResponseAdapter = moshi.adapter(ModelListResponse.class);
    private JsonAdapter<RecordingListResponse> recordingListResponseAdapter = moshi.adapter(RecordingListResponse.class);
    private JsonAdapter<ModelRequest> modelRequestAdapter = moshi.adapter(ModelRequest.class);
    private JsonAdapter<RecordingRequest> recordingRequestAdapter = moshi.adapter(RecordingRequest.class);
    private JsonAdapter<SimpleResponse> simpleResponseAdapter = moshi.adapter(SimpleResponse.class);

    private List<Model> models = Collections.emptyList();
    private List<Model> onlineModels = Collections.emptyList();
    private List<Recording> recordings = Collections.emptyList();
    private List<Site> sites;
    private long spaceTotal = -1;
    private long spaceFree = -1;

    private Config config;
    private HttpClient client;
    private Instant lastSync = Instant.EPOCH;
    private SyncThread syncThread;

    public RemoteRecorder(Config config, HttpClient client, List<Site> sites) {
        this.config = config;
        this.client = client;
        this.sites = sites;

        syncThread = new SyncThread();
        syncThread.start();
    }

    private String getRecordingEndpoint() {
        return config.getServerUrl() + "/rec";
    }

    @Override
    public void startRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("start", model);
    }

    @Override
    public void stopRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("stop", model);
    }

    private void sendRequest(String action, Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        String payload = modelRequestAdapter.toJson(new ModelRequest(action, model));
        LOG.debug("Sending request to recording server: {}", payload);
        RequestBody body = RequestBody.create(JSON, payload);
        Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
        addHmacIfNeeded(payload, builder);
        Request request = builder.build();
        try (Response response = client.execute(request)) {
            String json = response.body().string();
            if (response.isSuccessful()) {
                ModelListResponse resp = modelListResponseAdapter.fromJson(json);
                if (!resp.status.equals(SUCCESS)) {
                    throw new IOException("Server returned error " + resp.status + " " + resp.msg);
                }

                if ("start".equals(action)) {
                    models.add(model);
                } else if ("stop".equals(action)) {
                    models.remove(model);
                    onlineModels.remove(model);
                }
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }

    private void sendRequest(String action, Recording recording, Runnable... onSuccess) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        RecordingRequest recReq = new RecordingRequest(action, recording);
        String msg = recordingRequestAdapter.toJson(recReq);
        RequestBody body = RequestBody.create(JSON, msg);
        Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
        addHmacIfNeeded(msg, builder);
        Request request = builder.build();
        try (Response response = client.execute(request)) {
            String json = response.body().string();
            RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
            if (response.isSuccessful()) {
                if (!resp.status.equals(SUCCESS)) {
                    throw new IOException("Request failed: " + resp.msg);
                } else {
                    for (Runnable callback : onSuccess) {
                        callback.run();
                    }
                }
            } else {
                throw new IOException("Request failed: " + resp.msg);
            }
        }
    }

    private void addHmacIfNeeded(String msg, Builder builder) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
        if (Config.getInstance().getSettings().requireAuthentication) {
            byte[] key = Config.getInstance().getSettings().key;
            String hmac = Hmac.calculate(msg, key);
            builder.addHeader("CTBREC-HMAC", hmac);
        }
    }

    @Override
    public boolean isTracked(Model model) {
        return models != null && models.contains(model);
    }

    @Override
    public boolean isSuspended(Model model) {
        int index = models.indexOf(model);
        if (index >= 0) {
            Model m = models.get(index);
            return m.isSuspended();
        } else {
            return false;
        }
    }

    @Override
    public List<Model> getModels() {
        if (!lastSync.equals(Instant.EPOCH) && lastSync.isBefore(Instant.now().minusSeconds(60))) {
            throw new IllegalStateException("Last sync was over a minute ago");
        }
        return new ArrayList<>(models);
    }

    @Override
    public void shutdown() {
        syncThread.stopThread();
    }

    private class SyncThread extends Thread {
        private static final String SERVER_RETURNED_ERROR = "Server returned error: {} - {}";
        private static final String COULDNT_SYNCHRONIZE_WITH_SERVER = "Couldn't synchronize with server";
        private static final String COULDNT_SYNCHRONIZE_WITH_SERVER_HTTP_STATUS = "Couldn't synchronize with server. HTTP status: {} - {}";
        private volatile boolean running = false;

        public SyncThread() {
            setName("RemoteRecorder SyncThread");
            setDaemon(true);
        }

        @Override
        public void run() {
            running = true;
            while (running) {
                syncModels();
                syncOnlineModels();
                syncSpace();
                syncRecordings();
                sleep();
            }
        }

        private void syncSpace() {
            try {
                String msg = "{\"action\": \"space\"}";
                RequestBody body = RequestBody.create(JSON, msg);
                Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
                addHmacIfNeeded(msg, builder);
                Request request = builder.build();
                try (Response response = client.execute(request)) {
                    String json = response.body().string();
                    if (response.isSuccessful()) {
                        JSONObject resp = new JSONObject(json);
                        spaceTotal = resp.getLong("spaceTotal");
                        spaceFree = resp.getLong("spaceFree");
                    } else {
                        LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER_HTTP_STATUS, response.code(), json);
                    }
                }
            } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
                LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER, e);
            }
        }

        private void syncModels() {
            try {
                String msg = "{\"action\": \"list\"}";
                RequestBody body = RequestBody.create(JSON, msg);
                Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
                addHmacIfNeeded(msg, builder);
                Request request = builder.build();
                try (Response response = client.execute(request)) {
                    String json = response.body().string();
                    if (response.isSuccessful()) {
                        ModelListResponse resp = modelListResponseAdapter.fromJson(json);
                        if (resp.status.equals(SUCCESS)) {
                            models = resp.models;
                            for (Model model : models) {
                                for (Site site : sites) {
                                    if (site.isSiteForModel(model)) {
                                        model.setSite(site);
                                    }
                                }
                            }
                            lastSync = Instant.now();
                        } else {
                            LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg);
                        }
                    } else {
                        LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER_HTTP_STATUS, response.code(), json);
                    }
                }
            } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
                LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER, e);
            }
        }

        private void syncOnlineModels() {
            try {
                String msg = "{\"action\": \"listOnline\"}";
                RequestBody body = RequestBody.create(JSON, msg);
                Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
                addHmacIfNeeded(msg, builder);
                Request request = builder.build();
                try (Response response = client.execute(request)) {
                    String json = response.body().string();
                    if (response.isSuccessful()) {
                        ModelListResponse resp = modelListResponseAdapter.fromJson(json);
                        if (resp.status.equals(SUCCESS)) {
                            onlineModels = resp.models;
                            for (Model model : models) {
                                for (Site site : sites) {
                                    if (site.isSiteForModel(model)) {
                                        model.setSite(site);
                                    }
                                }
                            }
                        } else {
                            LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg);
                        }
                    } else {
                        LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER_HTTP_STATUS, response.code(), json);
                    }
                }
            } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
                LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER, e);
            }
        }

        private void syncRecordings() {
            try {
                String msg = "{\"action\": \"recordings\"}";
                RequestBody body = RequestBody.create(JSON, msg);
                Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
                addHmacIfNeeded(msg, builder);
                Request request = builder.build();
                try (Response response = client.execute(request)) {
                    String json = response.body().string();
                    if (response.isSuccessful()) {
                        RecordingListResponse resp = recordingListResponseAdapter.fromJson(json);
                        if (resp.status.equals(SUCCESS)) {
                            List<Recording> newRecordings = resp.recordings;
                            // fire changed events
                            for (Iterator<Recording> iterator = recordings.iterator(); iterator.hasNext();) {
                                Recording recording = iterator.next();
                                if (newRecordings.contains(recording)) {
                                    int idx = newRecordings.indexOf(recording);
                                    Recording newRecording = newRecordings.get(idx);
                                    if (newRecording.getStatus() != recording.getStatus()) {
                                        File file = new File(recording.getPath());
                                        RecordingStateChangedEvent evt = new RecordingStateChangedEvent(file, newRecording.getStatus(), recording.getModel(),
                                                recording.getStartDate());
                                        EventBusHolder.BUS.post(evt);
                                    }
                                }
                            }
                            recordings = newRecordings;
                        } else {
                            LOG.error(SERVER_RETURNED_ERROR, resp.status, resp.msg);
                        }
                    } else {
                        LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER_HTTP_STATUS, response.code(), json);
                    }
                }
            } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
                LOG.error(COULDNT_SYNCHRONIZE_WITH_SERVER, e);
            }
        }

        private void sleep() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                // interrupted, probably by stopThread
            }
        }

        public void stopThread() {
            running = false;
            interrupt();
        }
    }

    private static class ModelListResponse {
        public String status;
        public String msg;
        public List<Model> models;
    }

    private static class SimpleResponse {
        public String status;
        public String msg;
    }

    private static class RecordingListResponse {
        public String status;
        public String msg;
        public List<Recording> recordings;
    }

    @Override
    public List<Recording> getRecordings() throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        return recordings;
    }

    @Override
    public void delete(Recording recording) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        sendRequest("delete", recording, () -> recordings.remove(recording));
    }

    public static class ModelRequest {
        private String action;
        private Model model;

        public ModelRequest(String action, Model model) {
            super();
            this.action = action;
            this.model = model;
        }

        public String getAction() {
            return action;
        }

        public void setAction(String action) {
            this.action = action;
        }

        public Model getModel() {
            return model;
        }

        public void setModel(Model model) {
            this.model = model;
        }
    }

    public static class RecordingRequest {
        private String action;
        private Recording recording;

        public RecordingRequest(String action, Recording recording) {
            super();
            this.action = action;
            this.recording = recording;
        }

        public String getAction() {
            return action;
        }

        public void setAction(String action) {
            this.action = action;
        }

        public Recording getRecording() {
            return recording;
        }

        public void setRecording(Recording recording) {
            this.recording = recording;
        }
    }

    @Override
    public void switchStreamSource(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("switch", model);
    }

    @Override
    public void suspendRecording(Model model) throws InvalidKeyException, NoSuchAlgorithmException, IOException {
        sendRequest("suspend", model);
        model.setSuspended(true);
        // update cached model
        int index = models.indexOf(model);
        if (index >= 0) {
            Model m = models.get(index);
            m.setSuspended(true);
        }
    }

    @Override
    public void resumeRecording(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("resume", model);
        model.setSuspended(false);
        // update cached model
        int index = models.indexOf(model);
        if (index >= 0) {
            Model m = models.get(index);
            m.setSuspended(false);
        }
    }

    @Override
    public List<Model> getOnlineModels() {
        return onlineModels;
    }

    @Override
    public HttpClient getHttpClient() {
        return client;
    }

    @Override
    public long getTotalSpaceBytes() throws IOException {
        return spaceTotal;
    }

    @Override
    public long getFreeSpaceBytes() {
        return spaceFree;
    }

    @Override
    public void pin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("pin", recording);
    }

    @Override
    public void unpin(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("unpin", recording);
    }

    @Override
    public void rerunPostProcessing(Recording recording) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        RecordingRequest recReq = new RecordingRequest("rerunPostProcessing", recording);
        String msg = recordingRequestAdapter.toJson(recReq);
        LOG.debug(msg);
        RequestBody body = RequestBody.create(JSON, msg);
        Request.Builder builder = new Request.Builder().url(getRecordingEndpoint()).post(body);
        addHmacIfNeeded(msg, builder);
        Request request = builder.build();
        try (Response response = client.execute(request)) {
            String json = response.body().string();
            SimpleResponse resp = simpleResponseAdapter.fromJson(json);
            if (response.isSuccessful()) {
                if (!resp.status.equals(SUCCESS)) {
                    throw new IOException("Couldn't start post-processing for recording: " + resp.msg);
                }
            } else {
                throw new HttpException(response.code(), response.message());
            }
        }
    }

    @Override
    public void priorityChanged(Model model) throws IOException, InvalidKeyException, NoSuchAlgorithmException {
        sendRequest("changePriority", model);
    }
}
