package org.geoserver.shell;

import com.google.common.base.Strings;
import it.geosolutions.geoserver.rest.GeoServerRESTReader;
import it.geosolutions.geoserver.rest.HTTPUtils;
import it.geosolutions.geoserver.rest.decoder.RESTDataStore;
import it.geosolutions.geoserver.rest.decoder.RESTDataStoreList;
import org.jdom.Element;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.core.CommandMarker;
import org.springframework.shell.core.annotation.CliAvailabilityIndicator;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
import org.springframework.shell.support.util.OsUtils;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.*;

@Component
public class DataStoreCommands implements CommandMarker {

    @Autowired
    private Geoserver geoserver;

    public void setGeoserver(Geoserver gs) {
        this.geoserver = gs;
    }

    @CliAvailabilityIndicator({"datastore list", "datastore get", "datastore create", "datastore modify", "datastore delete", "datastore upload"})
    public boolean isCommandAvailable() {
        return geoserver.isSet();
    }

    @CliCommand(value = "datastore list", help = "List data stores.")
    public String list(
            @CliOption(key = "workspace", mandatory = false, help = "The workspace") String workspace
    ) throws Exception {
        StringBuilder builder = new StringBuilder();
        GeoServerRESTReader reader = new GeoServerRESTReader(geoserver.getUrl(), geoserver.getUser(), geoserver.getPassword());
        List<String> workspaces = new ArrayList<String>();
        if (workspace != null) {
            workspaces.add(workspace);
        } else {
            List<String> names = reader.getWorkspaceNames();
            Collections.sort(names);
            workspaces.addAll(names);
        }
        int counter = 0;
        for (String w : workspaces) {
            if (workspaces.size() > 1) {
                if (counter > 0) {
                    builder.append(OsUtils.LINE_SEPARATOR);
                }
                builder.append(w).append(OsUtils.LINE_SEPARATOR);
                builder.append(Strings.repeat("-", w.length())).append(OsUtils.LINE_SEPARATOR);
            }
            RESTDataStoreList dataStores = reader.getDatastores(w);
            List<String> names = dataStores.getNames();
            Collections.sort(names);
            for (String name : names) {
                builder.append(name + OsUtils.LINE_SEPARATOR);
            }
            counter++;
        }
        return builder.toString();
    }

    @CliCommand(value = "datastore create", help = "Create a new data store.")
    public boolean create(
            @CliOption(key = "workspace", mandatory = true, help = "The workspace") String workspace,
            @CliOption(key = "name", mandatory = true, help = "The name") String name,
            @CliOption(key = "connectionParams", mandatory = true, help = "The connection parameters") String connectionParams,
            @CliOption(key = "description", mandatory = false, help = "The description") String description,
            @CliOption(key = "enabled", mandatory = false, unspecifiedDefaultValue = "true", help = "The enabled flag") boolean enabled
    ) throws Exception {
        Map<String, String> params = getParametersFromString(connectionParams);
        // XML Element names can't have spaces, so use JSON instead
        String response = null;
        if (doesConnectionParamKeyHaveSpace(params.keySet())) {
            JSONObject jsonObject = new JSONObject();
            JSONObject dataStoreObject = new JSONObject();
            dataStoreObject.put("name", name);
            if (description != null) {
                dataStoreObject.put("description", description);
            }
            dataStoreObject.put("enabled", enabled);
            JSONObject connectionParamsObject = new JSONObject();
            JSONArray entryArray = new JSONArray();
            for (Map.Entry<String, String> param : params.entrySet()) {
                JSONObject entryObject = new JSONObject();
                entryObject.put("@key", param.getKey());
                entryObject.put("$", param.getValue());
                entryArray.put(entryObject);
            }
            connectionParamsObject.put("entry", entryArray);
            dataStoreObject.put("connectionParameters", connectionParamsObject);
            jsonObject.put("dataStore", dataStoreObject);
            String json = jsonObject.toString();
            String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores.json";
            response = HTTPUtils.post(url, json, "application/json", geoserver.getUser(), geoserver.getPassword());
        } else {
            Element dataStoreElement = new Element("dataStore");
            dataStoreElement.addContent(new Element("name").setText(name));
            if (description != null) {
                dataStoreElement.addContent(new Element("description").setText(description));
            }
            dataStoreElement.addContent(new Element("enabled").setText(String.valueOf(enabled)));
            Element connectionParamElement = new Element("connectionParameters");
            List<Map.Entry<String, String>> connectionParamList = new LinkedList<Map.Entry<String, String>>(params.entrySet());
            Collections.sort(connectionParamList, new Comparator<Map.Entry<String,String>>() {
                @Override
                public int compare( Map.Entry<String,String> o1, Map.Entry<String,String> o2 )
                {
                    return (o1.getKey()).compareTo( o2.getKey());
                }
            });
            for (Map.Entry<String, String> param : connectionParamList) {
                connectionParamElement.addContent(new Element(param.getKey()).setText(param.getValue()));
            }
            dataStoreElement.addContent(connectionParamElement);
            String xml = JDOMUtil.toString(dataStoreElement);
            String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores.xml";
            response = HTTPUtils.postXml(url, xml, geoserver.getUser(), geoserver.getPassword());
        }
        return response != null;
    }

    @CliCommand(value = "datastore modify", help = "Create a new data store.")
    public boolean modify(
            @CliOption(key = "workspace", mandatory = true, help = "The workspace") String workspace,
            @CliOption(key = "name", mandatory = true, help = "The name") String name,
            @CliOption(key = "connectionParams", mandatory = false, help = "The connection parameters") String connectionParams,
            @CliOption(key = "description", mandatory = false, help = "The description") String description,
            @CliOption(key = "enabled", mandatory = false, help = "The enabled flag") String enabled
    ) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        if (connectionParams != null) {
            params = getParametersFromString(connectionParams);
        }
        // XML Element names can't have spaces, so use JSON instead
        String response = null;
        if (doesConnectionParamKeyHaveSpace(params.keySet())) {
            JSONObject jsonObject = new JSONObject();
            JSONObject dataStoreObject = new JSONObject();
            dataStoreObject.put("name", name);
            if (description != null) {
                dataStoreObject.put("description", description);
            }
            if (enabled != null) {
                dataStoreObject.put("enabled", enabled);
            }
            if (connectionParams != null && !connectionParams.isEmpty()) {
                JSONObject connectionParamsObject = new JSONObject();
                JSONArray entryArray = new JSONArray();
                for (Map.Entry<String, String> param : params.entrySet()) {
                    JSONObject entryObject = new JSONObject();
                    entryObject.put("@key", param.getKey());
                    entryObject.put("$", param.getValue());
                    entryArray.put(entryObject);
                }
                connectionParamsObject.put("entry", entryArray);
                dataStoreObject.put("connectionParameters", connectionParamsObject);
            }
            jsonObject.put("dataStore", dataStoreObject);
            String json = jsonObject.toString();
            String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores/" + URLUtil.encode(name) + ".json";
            response = HTTPUtils.put(url, json, "application/json", geoserver.getUser(), geoserver.getPassword());
        } else {
            Element dataStoreElement = new Element("dataStore");
            dataStoreElement.addContent(new Element("name").setText(name));
            if (description != null) {
                dataStoreElement.addContent(new Element("description").setText(description));
            }
            if (enabled != null) {
                dataStoreElement.addContent(new Element("enabled").setText(enabled));
            }
            if (connectionParams != null && !connectionParams.isEmpty()) {
                Element connectionParamElement = new Element("connectionParameters");
                List<Map.Entry<String, String>> connectionParamList = new LinkedList<Map.Entry<String, String>>(params.entrySet());
                Collections.sort(connectionParamList, new Comparator<Map.Entry<String,String>>() {
                    @Override
                    public int compare( Map.Entry<String,String> o1, Map.Entry<String,String> o2 )
                    {
                        return (o1.getKey()).compareTo( o2.getKey());
                    }
                });
                for (Map.Entry<String, String> param : connectionParamList) {
                    connectionParamElement.addContent(new Element(param.getKey()).setText(param.getValue()));
                }
                dataStoreElement.addContent(connectionParamElement);
            }
            String xml = JDOMUtil.toString(dataStoreElement);
            String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores/" + URLUtil.encode(name) + ".xml";
            response = HTTPUtils.putXml(url, xml, geoserver.getUser(), geoserver.getPassword());
        }
        return response != null;
    }

    @CliCommand(value = "datastore get", help = "Get a data store.")
    public String get(
            @CliOption(key = "workspace", mandatory = true, help = "The workspace") String workspace,
            @CliOption(key = "name", mandatory = true, help = "The name") String name
    ) throws Exception {
        GeoServerRESTReader reader = new GeoServerRESTReader(geoserver.getUrl(), geoserver.getUser(), geoserver.getPassword());
        RESTDataStore dataStore = reader.getDatastore(workspace, name);
        final String TAB = "   ";
        StringBuilder builder = new StringBuilder();
        builder.append(dataStore.getName()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Enabled? ").append(dataStore.isEnabled()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Description: ").append(dataStore.getDescription()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Store Type: ").append(dataStore.getStoreType()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Type: ").append(dataStore.getType()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Workspace: ").append(dataStore.getWorkspaceName()).append(OsUtils.LINE_SEPARATOR);
        builder.append(TAB).append("Connection Parameters:").append(OsUtils.LINE_SEPARATOR);
        Map<String, String> params = dataStore.getConnectionParameters();
        List<Map.Entry<String, String>> connectionParamList = new LinkedList<Map.Entry<String, String>>(params.entrySet());
        Collections.sort(connectionParamList, new Comparator<Map.Entry<String,String>>() {
            @Override
            public int compare( Map.Entry<String,String> o1, Map.Entry<String,String> o2 )
            {
                return (o1.getKey()).compareTo( o2.getKey());
            }
        });
        for (Map.Entry<String, String> param : connectionParamList) {
            builder.append(TAB).append(TAB).append(param.getKey()).append(": ").append(param.getValue()).append(OsUtils.LINE_SEPARATOR);
        }
        return builder.toString();
    }

    @CliCommand(value = "datastore delete", help = "Delete an existing data store.")
    public boolean delete(
            @CliOption(key = "workspace", mandatory = true, help = "The workspace") String workspace,
            @CliOption(key = "name", mandatory = true, help = "The name") String name,
            @CliOption(key = "recurse", mandatory = false, help = "Whether to recursively delete all layers (true) or not (false)", unspecifiedDefaultValue = "false", specifiedDefaultValue = "false") boolean recurse
    ) throws Exception {
        String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores/" + URLUtil.encode(name) + ".xml?recurse=" + recurse;
        return HTTPUtils.delete(url, geoserver.getUser(), geoserver.getPassword());
    }

    @CliCommand(value = "datastore upload", help = "Upload a File to a data store.")
    public boolean upload(
            @CliOption(key = "workspace", mandatory = true, help = "The workspace") String workspace,
            @CliOption(key = "name", mandatory = true, help = "The name") String name,
            @CliOption(key = "type", mandatory = true, help = "The datastore type (shp, properties, h2, spatialite)") String type,
            @CliOption(key = "file", mandatory = true, help = "The file") File file,
            @CliOption(key = "configure", mandatory = false, help = "The configure parameter can be: first, none, all", unspecifiedDefaultValue = "first", specifiedDefaultValue = "first") String configure,
            @CliOption(key = "target", mandatory = false, help = "The target type (shp, properties, h2, spatialite)") String target,
            @CliOption(key = "update", mandatory = false, help = "The update parameter can be append or overwrite", unspecifiedDefaultValue = "append", specifiedDefaultValue = "append") String update,
            @CliOption(key = "charset", mandatory = false, help = "The charset") String charset
    ) {
        String url = geoserver.getUrl() + "/rest/workspaces/" + URLUtil.encode(workspace) + "/datastores/" + URLUtil.encode(name) + "/file." + type;
        url += "?configure=" + configure + "&update=" + update;
        if (target != null) {
            url += "&target=" + target;
        }
        if (charset != null) {
            url += "&charset=" + charset;
        }
        String response = HTTPUtils.put(url, file, "application/zip", geoserver.getUser(), geoserver.getPassword());
        return response != null;
    }

    /**
     * Get a Map from a parameter string: "dbtype=h2 database=roads.db"
     *
     * @param str The parameter string is a space delimited collection of key=value parameters.  Use single
     *            quotes around key or values with internal spaces.
     * @return A Map of parameters
     */
    protected static Map getParametersFromString(String str) throws Exception {
        Map<String, String> params = new HashMap<String, String>();
        if (str.indexOf("=") == -1) {
            if (str.endsWith(".shp")) {
                params.put("url", new File(str).getAbsoluteFile().getParentFile().toURI().toURL().toString());
            } else if (str.endsWith(".properties")) {
                String dir;
                File f = new File(str);
                if (f.exists()) {
                    dir = f.getAbsoluteFile().getParentFile().getAbsolutePath();
                } else {
                    dir = f.getAbsolutePath().substring(0, f.getAbsolutePath().lastIndexOf(File.separator));
                }
                params.put("directory", dir);
            } else if (new File(str).isDirectory()) {
                params.put("url", new File(str).getAbsoluteFile().toURI().toURL().toString());
            } else {
                throw new IllegalArgumentException("Unknown Workspace parameter string: " + str);
            }
        } else {
            String[] segments = str.split("[ ]+(?=([^\']*\'[^\']*\')*[^\']*$)");
            for (String segment : segments) {
                String[] parts = segment.split("=");
                String key = parts[0].trim();
                if ((key.startsWith("'") && key.endsWith("'")) ||
                        (key.startsWith("\"") && key.endsWith("\""))) {
                    key = key.substring(1, key.length() - 1);
                }
                String value = parts[1].trim();
                if ((value.startsWith("'") && value.endsWith("'")) ||
                        (value.startsWith("\"") && value.endsWith("\""))) {
                    value = value.substring(1, value.length() - 1);
                }
                if (key.equalsIgnoreCase("url")) {
                    value = new File(value).getAbsoluteFile().toURI().toURL().toString();
                }
                params.put(key, value);
            }
        }
        return params;
    }

    /**
     * When creating or modifying a datastore, the connection param key can't have a space in it because
     * XML Element names can't have spaces.
     *
     * @param keys The keys from a connection parameter map
     * @return Whether any of the keys have a space in it
     */
    private boolean doesConnectionParamKeyHaveSpace(Set<String> keys) {
        for (String key : keys) {
            if (key.contains(" ")) {
                return true;
            }
        }
        return false;
    }

}