package ctbrec;

import static java.nio.file.StandardOpenOption.*;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import ctbrec.io.ModelJsonAdapter;
import ctbrec.sites.Site;

public class Config {

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

    private static Config instance;
    private Settings settings;
    private String filename;
    private List<Site> sites;
    private File configDir;
    public static final String RECORDING_DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss_SSS";

    private Config(List<Site> sites) {
        this.sites = sites;
        if(System.getProperty("ctbrec.config.dir") != null) {
            configDir = new File(System.getProperty("ctbrec.config.dir"));
        } else {
            configDir = OS.getConfigDir();
        }

        if(System.getProperty("ctbrec.config") != null) {
            filename = System.getProperty("ctbrec.config");
        } else {
            filename = "settings.json";
        }
    }

    private void load() throws IOException {
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter(sites))
                .build();
        JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).lenient();
        File configFile = new File(configDir, filename);
        LOG.info("Loading config from {}", configFile.getAbsolutePath());
        if (configFile.exists()) {
            try {
                byte[] fileContent = Files.readAllBytes(configFile.toPath());
                if (fileContent[0] == -17 && fileContent[1] == -69 && fileContent[2] == -65) {
                    // found BOM (byte order mark)
                    LOG.debug("Removing BOM from config file");
                    fileContent[0] = ' ';
                    fileContent[1] = ' ';
                    fileContent[2] = ' ';
                }
                String json = new String(fileContent, StandardCharsets.UTF_8.name()).trim();
                settings = adapter.fromJson(json);
                settings.httpTimeout = Math.max(settings.httpTimeout, 10_000);
                if (settings.recordingsDir.endsWith("/")) {
                    settings.recordingsDir = settings.recordingsDir.substring(0, settings.recordingsDir.length() - 1);
                }
            } catch (Exception e) {
                settings = OS.getDefaultSettings();
                for (Site site : sites) {
                    site.setEnabled(!settings.disabledSites.contains(site.getName()));
                }
                makeBackup(configFile);
                throw e;
            }
        } else {
            LOG.error("Config file does not exist. Falling back to default values.");
            settings = OS.getDefaultSettings();
        }
        for (Site site : sites) {
            site.setEnabled(!settings.disabledSites.contains(site.getName()));
        }
    }

    private void makeBackup(File source) {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
            String timestamp = sdf.format(new Date());
            String backup = source.getName() + '.' + timestamp;
            File target = new File(source.getParentFile(), backup);
            Files.copy(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
        } catch (Exception e) {
            LOG.error("Couldn't create backup of settings file", e);
        }
    }

    public static synchronized void init(List<Site> sites) throws IOException {
        if (instance == null) {
            instance = new Config(sites);
            instance.load();
        }
    }

    public static synchronized Config getInstance() {
        if (instance == null) {
            throw new IllegalStateException("Config not initialized, call init() first");
        }
        return instance;
    }

    public Settings getSettings() {
        return settings;
    }

    public void save() throws IOException {
        Moshi moshi = new Moshi.Builder()
                .add(Model.class, new ModelJsonAdapter())
                .build();
        JsonAdapter<Settings> adapter = moshi.adapter(Settings.class).indent("  ");
        String json = adapter.toJson(settings);
        File configFile = new File(configDir, filename);
        LOG.debug("Saving config to {}", configFile.getAbsolutePath());
        Files.createDirectories(configDir.toPath());
        Files.write(configFile.toPath(), json.getBytes("utf-8"), CREATE, WRITE, TRUNCATE_EXISTING);
    }

    public static boolean isServerMode() {
        return Objects.equals(System.getProperty("ctbrec.server.mode"), "1");
    }

    public static boolean isDevMode() {
        return Objects.equals(System.getenv("CTBREC_DEV"), "1");
    }

    public File getConfigDir() {
        return configDir;
    }

    public File getFileForRecording(Model model, String suffix, Instant startTime) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(RECORDING_DATE_FORMAT);
        LocalDateTime startDateTime = LocalDateTime.ofInstant(startTime, ZoneId.systemDefault());
        String formattedDate = dateTimeFormatter.format(startDateTime);
        File dirForRecording = getDirForRecording(model, formattedDate);
        File targetFile = new File(dirForRecording, model.getSanitizedNamed() + '_' + formattedDate + '.' + suffix);
        return targetFile;
    }

    private File getDirForRecording(Model model, String formattedDate) {
        switch (getSettings().recordingsDirStructure) {
        case ONE_PER_MODEL:
            return new File(getSettings().recordingsDir, model.getSanitizedNamed());
        case ONE_PER_RECORDING:
            File modelDir = new File(getSettings().recordingsDir, model.getSanitizedNamed());
            return new File(modelDir, formattedDate);
        case FLAT:
        default:
            return new File(getSettings().recordingsDir);
        }
    }

    public String getServerUrl() {
        String scheme = getSettings().transportLayerSecurity ? "https" : "http";
        // int port = getSettings().transportLayerSecurity ? getSettings().httpSecurePort : getSettings().httpPort;
        int port = getSettings().httpPort;
        String baseUrl = scheme + "://" + getSettings().httpServer + ":" + port + getContextPath();
        return baseUrl;
    }

    public String getContextPath() {
        String context = Optional.ofNullable(getSettings().servletContext).orElse("");
        if (!context.startsWith("/") && !context.isEmpty()) {
            context = '/' + context;
        }
        if (context.endsWith("/")) {
            context = context.substring(0, context.length() - 1);
        }
        return context;
    }
}
