package de.tudarmstadt.informatik.hostage.sync.android;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.telephony.TelephonyManager;
import android.util.Log;

import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.security.KeyStore;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.tudarmstadt.informatik.hostage.location.MyLocationManager;
import de.tudarmstadt.informatik.hostage.logging.MessageRecord;
import de.tudarmstadt.informatik.hostage.logging.NetworkRecord;
import de.tudarmstadt.informatik.hostage.logging.Record;
import de.tudarmstadt.informatik.hostage.logging.SyncData;
import de.tudarmstadt.informatik.hostage.logging.SyncInfo;
import de.tudarmstadt.informatik.hostage.logging.SyncRecord;
import de.tudarmstadt.informatik.hostage.net.MySSLSocketFactory;
import de.tudarmstadt.informatik.hostage.sync.Synchronizer;
import de.tudarmstadt.informatik.hostage.ui.activity.MainActivity;

/**
 * Created by abrakowski
 */
public class SyncUtils {
    public static final int SYNC_SUCCESSFUL = 0x0;
    private static final long SYNC_FREQUENCY = 60 * 60;  // 1 hour (in seconds)

    public static final String CONTENT_AUTHORITY = "de.tudarmstadt.informatik.hostage.androidsync";
    private static final String PREF_SETUP_COMPLETE = "sync_setup_complete";

    private static final Map<String, Integer> protocolsTypeMap;

    static {
        protocolsTypeMap = new HashMap<String, Integer>();
        protocolsTypeMap.put("UNKNOWN", 0);
        protocolsTypeMap.put("ECHO", 1);
        protocolsTypeMap.put("GHOST", 2);
        protocolsTypeMap.put("PORTSCAN", 11);
        protocolsTypeMap.put("SSH", 20);
        protocolsTypeMap.put("MySQL", 31);
        protocolsTypeMap.put("SMB", 40);
        protocolsTypeMap.put("SIP", 50);
        protocolsTypeMap.put("FTP", 60);
        protocolsTypeMap.put("HTTP", 70);
        protocolsTypeMap.put("HTTPS", 71);
        protocolsTypeMap.put("TELNET", 80);
    }

    /**
     * Create an entry for this application in the system account list, if it isn't already there.
     *
     * @param context Context
     */
    public static void CreateSyncAccount(Context context) {
        boolean newAccount = false;
        boolean setupComplete = PreferenceManager
                .getDefaultSharedPreferences(context).getBoolean(PREF_SETUP_COMPLETE, false);

        // Create account, if it's missing. (Either first run, or user has deleted account.)
        Account account = HostageAccountService.GetAccount();
        AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
        if (accountManager.addAccountExplicitly(account, null, null)) {
            // Inform the system that this account supports sync
            ContentResolver.setIsSyncable(account, CONTENT_AUTHORITY, 1);
            // Inform the system that this account is eligible for auto sync when the network is up
            ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, true);
            // Recommend a schedule for automatic synchronization. The system may modify this based
            // on other scheduled syncs and network utilization.
            ContentResolver.addPeriodicSync(
                    account, CONTENT_AUTHORITY, new Bundle(),SYNC_FREQUENCY);
            newAccount = true;
        }

        // Schedule an initial sync if we detect problems with either our account or our local
        // data has been deleted. (Note that it's possible to clear app data WITHOUT affecting
        // the account list, so wee need to check both.)
        if (newAccount || !setupComplete) {
            TriggerRefresh();
            PreferenceManager.getDefaultSharedPreferences(context).edit()
                    .putBoolean(PREF_SETUP_COMPLETE, true).commit();
        }
    }

    public static void TriggerRefresh() {
        Bundle b = new Bundle();
        // Disable sync backoff and ignore sync preferences. In other words...perform sync NOW!
        b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        ContentResolver.requestSync(
                HostageAccountService.GetAccount(),      // Sync account
                CONTENT_AUTHORITY,                       // Content authority
                b);                                      // Extras
    }


    public static String getProtocolFromInt(int p){
        for(Map.Entry<String, Integer> entry: protocolsTypeMap.entrySet()){
            if(entry.getValue() == p) return entry.getKey();
        }

        return "UNKNOWN";
    }

    public static void appendRecordToStringWriter(Record record, Writer stream){
        try {
            stream.append(
            "{" +
                "\"sensor\":{" +
                    "\"name\":\"HosTaGe\"," +
                    "\"type\":\"Honeypot\"" +
                "}," +
                "\"src\":{" +
                    "\"ip\":\"" + record.getRemoteIP() + "\"," +
                    "\"port\":" + record.getRemotePort() +
                "}," +
                "\"dst\":{" +
                    "\"ip\":\"" + record.getExternalIP() /*record.getLocalIP()*/ + "\"," +
                    "\"port\":" + record.getLocalPort() +
                "}," +
                "\"type\":" + (protocolsTypeMap.containsKey(record.getProtocol()) ? protocolsTypeMap.get(record.getProtocol()) : 0) + "," +
                "\"log\":\"" + record.getProtocol() + "\"," +
                "\"md5sum\":\"\"," +
                "\"date\":" + (int)(record.getTimestamp() / 1000) + "," +
                "\"bssid\":\"" + record.getBssid() + "\"," +
                "\"ssid\":\"" + record.getSsid() + "\"," +
                "\"device\":\"" + record.getDevice() + "\"," +
                "\"sync_id\":\"" + record.getSync_id() + "\"" +
             "}\n"
            );
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static boolean uploadRecordsToServer(String entity, String serverAddress){
        HttpPost httppost;
        try {
            HttpClient httpClient = createHttpClient();
            // Create HttpPost
            httppost = new HttpPost(serverAddress);

            StringEntity se = new StringEntity(entity);
            httppost.addHeader("content-type", "application/json+newline");
            httppost.setEntity(se);

            // Execute HttpPost
            HttpResponse response = httpClient.execute(httppost);

            if(response.getStatusLine().getStatusCode() >= 400 && response.getStatusLine().getStatusCode() < 600){
                return false;
            }
            Log.i("TracingSyncService", "Status Code: " + response.getStatusLine().getStatusCode());
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    public static <T> T downloadFromServer(String address, Class<T> klass){
        HttpGet httpget;

        try {
            HttpClient httpClient = createHttpClient();

            httpget = new HttpGet(address);
            httpget.addHeader("Accept", "application/json");

            HttpResponse response = httpClient.execute(httpget);
            Log.i("downloadFromServer", "Status Code: " + response.getStatusLine().getStatusCode());

            if(response.getStatusLine().getStatusCode() >= 400 && response.getStatusLine().getStatusCode() < 600){
                return klass.newInstance();
            }

            return klass.getConstructor(klass).newInstance(readResponseToString(response));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String readResponseToString(HttpResponse response){
        StringBuilder builder = new StringBuilder();

        try {
            BufferedReader bReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
            String line;

            while ((line = bReader.readLine()) != null) {
                builder.append(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return builder.toString();
    }

    public static SyncData getSyncDataFromTracing(Context context, Synchronizer synchronizer, long fromTime){
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        String serverAddress = pref.getString("pref_upload_server", "https://ssi.cased.de");

        HttpPost httppost;
        try {
            HttpClient httpClient = createHttpClient();
            // Create HttpPost
            httppost = new HttpPost(serverAddress + "/sync");

            SyncInfo info = synchronizer.getSyncInfo();

            JSONArray deviceMap = new JSONArray();
            for(Map.Entry<String, Long> entry: info.deviceMap.entrySet()){
                JSONObject m = new JSONObject();
                m.put("sync_id", entry.getValue());
                m.put("device", entry.getKey());
                deviceMap.put(m);
            }

            JSONObject condition = new JSONObject();

            /**if(fromTime > 0){
                Calendar calendar = GregorianCalendar.getInstance();
                calendar.setTimeInMillis(fromTime);

                condition.put("date", fromCalendar(calendar));
            }**/

            /**
            TODO(alex): UNCOMMENT THIS AGAIN, WHEN WE ARE SURE ABOUT USING LOCATIONS THIS WAY.
            =======================================================================

            Location location = MyLocationManager.getNewestLocation();
            if(location != null){
                JSONArray ll = new JSONArray();
                ll.put(location.getLongitude());
                ll.put(location.getLatitude());

                condition.put("location", ll);
                condition.put("distance", 500);
            } else {
                // We could not get the gps coordinates, try to retrieve the country code from the SIM card
                TelephonyManager tm = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
                String countryCodeValue = tm.getNetworkCountryIso();

                condition.put("country", countryCodeValue);
            }*/

            JSONObject req = new JSONObject();
            req.put("condition", condition);
            req.put("info", deviceMap);

            StringEntity se = new StringEntity(req.toString());
            httppost.addHeader("content-type", "application/json");
            httppost.setEntity(se);

            // Execute HttpPost
            HttpResponse response = httpClient.execute(httppost);
            Log.i("TracingSyncService", "Status Code: " + response.getStatusLine().getStatusCode());
            if(response.getStatusLine().getStatusCode() >= 400 && response.getStatusLine().getStatusCode() < 600){
                return null;
            }

            String responseBody = readResponseToString(response);
            JSONArray syncData;

            // ensure, that the received data is an array
            try {
                syncData = new JSONArray(responseBody);
            } catch (JSONException ex){
                ex.printStackTrace();
                return null;
            }

            ArrayList<SyncRecord> syncRecords = new ArrayList<SyncRecord>();
            Map<String, NetworkRecord> networkRecordMap = new HashMap<String, NetworkRecord>();

            SyncData result = new SyncData();

            for(int i=0; i<syncData.length(); i++){
                try {
                    JSONObject item = syncData.getJSONObject(i);
                    JSONObject src = item.getJSONObject("src");
                    JSONArray src_ll = src.getJSONArray("ll");

                    JSONObject dst = item.getJSONObject("dst");
                    JSONArray dst_ll = dst.getJSONArray("ll");

                    Calendar date = toCalendar(item.getString("date"));

                    if(!networkRecordMap.containsKey(item.getString("bssid"))){
                        NetworkRecord networkRecord = new NetworkRecord();
                        networkRecord.setAccuracy(0);
                        networkRecord.setBssid(item.getString("bssid"));
                        networkRecord.setSsid(item.getString("ssid"));
                        networkRecord.setLatitude(dst_ll.getDouble(1));
                        networkRecord.setLatitude(dst_ll.getDouble(0));
                        networkRecord.setTimestampLocation(date.getTimeInMillis());
                        networkRecordMap.put(item.getString("bssid"), networkRecord);
                    }

                    SyncRecord record = new SyncRecord();
                    record.setBssid(item.getString("bssid"));
                    record.setAttack_id(i);
                    record.setDevice(item.getString("device"));
                    record.setSync_id(item.getLong("sync_id"));
                    record.setProtocol(getProtocolFromInt(item.getInt("type")));
                    record.setLocalIP(dst.getString("ip"));
                    record.setLocalPort(dst.getInt("port"));
                    record.setRemoteIP(src.getString("ip"));
                    record.setRemotePort(src.getInt("port"));
                    record.setExternalIP("0.0.0.0");
                    record.setWasInternalAttack(false);

                    syncRecords.add(record);
                } catch(Exception e){
                    e.printStackTrace();
                    continue;
                }
            }

            result.networkRecords = new ArrayList<NetworkRecord>(networkRecordMap.values());
            result.syncRecords = syncRecords;

            return result;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String urlEncodeUTF8(String s) {
        try {
            return URLEncoder.encode(s, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedOperationException(e);
        }
    }

    public static String[] convertMapToStringArray(Map<String, String> map){
        String[] array = new String[map.size() * 2];
        int i = 0;
        for(Map.Entry<String, String> entry: map.entrySet()){
            array[i] = entry.getKey();
            array[i + 1] = entry.getValue();
            i += 2;
        }

        return array;
    }

    public static String buildUrlFromBase(String baseUrl, String... query){
        StringBuilder sb = new StringBuilder(baseUrl);

        if(query.length >= 2){
            sb.append("?");
        }

        for(int i=0; i<query.length - 2; i+=2){
            if(i > 0){
                sb.append("&");
            }

            sb.append(String.format("%s=%s",
                urlEncodeUTF8(query[i]),
                urlEncodeUTF8(query[i + 1])
            ));
        }

        return sb.toString();
    }

    public static String buildUrlFromBase(String baseUrl, Map<String, String> query){
        return buildUrlFromBase(baseUrl, convertMapToStringArray(query));
    }

    public static String buildUrl(String protocol, String domain, int port, String path, String ... query){
        return buildUrlFromBase(
            String.format("%s://%s:%d/%s", urlEncodeUTF8(protocol), urlEncodeUTF8(domain), port, path),
            query
        );
    }

    public static String buildUrl(String protocol, String domain, int port, String path, Map<String, String> query){
        return buildUrl(protocol, domain, port, path, convertMapToStringArray(query));
    }

    public static List<String[]> getCountriesFromServer(String serverAddress){
        List<String[]> ret = new ArrayList<String[]>();
        JSONArray array = downloadFromServer(serverAddress + "/get_countries", JSONArray.class);

        try {
            for (int i = 0; i < array.length(); i++) {
                JSONObject ob = array.getJSONObject(i);
                ret.add(new String[]{ob.getString("cc"), ob.getString("country")});
            }
        } catch(Exception e){
            e.printStackTrace();
        }

        return ret;
    }

    public static String fromCalendar(final Calendar calendar) {
        Date date = calendar.getTime();
        String formatted = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
                .format(date);
        return formatted.substring(0, 22) + ":" + formatted.substring(22);
    }

    public static Calendar toCalendar(final String iso8601string)
            throws ParseException {
        Calendar calendar = GregorianCalendar.getInstance();
        String s = iso8601string.replace("Z", "0+0000");

        try {
            s = s.substring(0, 22) + s.substring(23);  // to get rid of the ":"
        } catch (IndexOutOfBoundsException e) {
            throw new ParseException("Invalid length", 0);
        }

        Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse(s);
        calendar.setTime(date);
        return calendar;
    }

    public static JSONArray retrieveNewAttacks(Context context, boolean fromPosition){
        SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context);
        String serverAddress = pref.getString("pref_download_server", "http://ssi.cased.de/api");
        long lastDownloadTime = pref.getLong("pref_download_last_time", 0);

        Calendar calendar = GregorianCalendar.getInstance();
        calendar.setTimeInMillis(lastDownloadTime);

        String baseUri = serverAddress + "/get_attacks";
        Map<String, String> query = new HashMap<String, String>();
        query.put("start", fromCalendar(calendar));

        if(fromPosition){
            Location location = MyLocationManager.getNewestLocation();

            if(location != null) {
                query.put("latitude", String.valueOf(location.getLatitude()));
                query.put("longitude", String.valueOf(location.getLongitude()));
                query.put("distance", "300");
            }
        }

        return downloadFromServer(buildUrlFromBase(baseUri, "start", fromCalendar(calendar)), JSONArray.class);
    }

    public static HttpClient createHttpClient() {
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(null, null);

            SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", sf, 443));

            ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

            return new DefaultHttpClient(ccm, params);
        } catch (Exception e) {
            e.printStackTrace();
            return new DefaultHttpClient();
        }
    }
}
