package com.nilhcem.hostseditor.core;

import android.content.Context;

import com.stericson.RootShell.RootShell;
import com.stericson.RootShell.exceptions.RootDeniedException;
import com.stericson.RootShell.execution.Command;
import com.stericson.RootTools.RootTools;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.LineIterator;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeoutException;

import javax.inject.Inject;
import javax.inject.Singleton;

import timber.log.Timber;

@Singleton
public class HostsManager {

    private static final String UTF_8 = "UTF-8";
    private static final String HOSTS_FILE_NAME = "hosts";
    private static final String HOSTS_FILE_PATH = "/system/etc/" + HOSTS_FILE_NAME;

    private static final String LINE_SEPARATOR = System.getProperty("line.separator", "\n");
    private static final String MOUNT_TYPE_RO = "ro";
    private static final String MOUNT_TYPE_RW = "rw";
    private static final String COMMAND_RM = "rm -f";
    private static final String COMMAND_CHOWN = "chown 0:0";
    private static final String COMMAND_CHMOD_644 = "chmod 644";

    // Do not access this field directly even in the same class, use getAllHosts() instead.
    private List<Host> mHosts;

    @Inject
    public HostsManager() {
    }

    /**
     * Gets all host entries from hosts file.
     * <p><b>Must be in an async call.</b></p>
     *
     * @param forceRefresh if we want to force using the hosts file (not the cache)
     * @return a list of host entries
     */
    public synchronized List<Host> getHosts(boolean forceRefresh) {
        if (mHosts == null || forceRefresh) {
            mHosts = Collections.synchronizedList(new ArrayList<Host>());

            LineIterator it = null;
            try {
                it = FileUtils.lineIterator(new File(HOSTS_FILE_PATH), UTF_8);
                while (it.hasNext()) {
                    Host host = Host.fromString(it.nextLine());
                    if (host != null) {
                        mHosts.add(host);
                    }
                }
            } catch (IOException e) {
                Timber.e(e, "I/O error while opening hosts file");
            } finally {
                if (it != null) {
                    LineIterator.closeQuietly(it);
                }
            }
        }
        return mHosts;
    }

    /**
     * Saves new hosts file and creates a backup of previous file.
     * <p><b>Must be in an async call.</b></p>
     *
     * @param appContext application context
     * @return {@code true} if everything was working as expected, or {@code false} otherwise
     */
    public synchronized boolean saveHosts(Context appContext) {
        if (!RootTools.isAccessGiven()) {
            Timber.w("Can't get root access");
            return false;
        }

        // Step 1: Create temporary hosts file in /data/data/project_package/files/hosts
        if (!createTempHostsFile(appContext)) {
            Timber.w("Can't create temporary hosts file");
            return false;
        }

        String tmpFile = String.format(Locale.US, "%s/%s", appContext.getFilesDir().getAbsolutePath(), HOSTS_FILE_NAME);
        String backupFile = String.format(Locale.US, "%s.bak", tmpFile);

        // Step 2: Get canonical path for /etc/hosts (it could be a symbolic link)
        String hostsFilePath = HOSTS_FILE_PATH;
        File hostsFile = new File(HOSTS_FILE_PATH);
        if (hostsFile.exists()) {
            try {
                if (FileUtils.isSymlink(hostsFile)) {
                    hostsFilePath = hostsFile.getCanonicalPath();
                }
            } catch (IOException e1) {
                Timber.e(e1, "Can't find hosts file");
            }
        } else {
            Timber.w("Hosts file was not found in filesystem");
        }

        try {
            // Step 3: Create backup of current hosts file (if any)
            RootTools.remount(hostsFilePath, MOUNT_TYPE_RW);
            runRootCommand(COMMAND_RM, backupFile);
            RootTools.copyFile(hostsFilePath, backupFile, false, true);

            // Step 4: Replace hosts file with generated file
            runRootCommand(COMMAND_RM, hostsFilePath);
            RootTools.copyFile(tmpFile, hostsFilePath, false, true);

            // Step 5: Give proper rights
            runRootCommand(COMMAND_CHOWN, hostsFilePath);
            runRootCommand(COMMAND_CHMOD_644, hostsFilePath);

            // Step 6: Delete local file
            appContext.deleteFile(HOSTS_FILE_NAME);
        } catch (Exception e) {
            Timber.e(e, "Failed running root command");
            return false;
        } finally {
            RootTools.remount(hostsFilePath, MOUNT_TYPE_RO);
        }
        return true;
    }

    /**
     * Returns a list of hosts matching the constraint parameter.
     */
    public List<Host> filterHosts(CharSequence constraint) {
        List<Host> all = getHosts(false);
        List<Host> hosts = new ArrayList<>();

        for (Host host : all) {
            if (host.isValid()) {
                if (host.getIp().contains(constraint)
                        || host.getHostName().contains(constraint)
                        || (host.getComment() != null && host.getComment().contains(constraint))) {
                    hosts.add(host);
                }
            }
        }
        return hosts;
    }

    /**
     * Creates a temporary hosts file in {@code /data/data/project_package/files/hosts}.
     * <p><b>Must be in an async call.</b></p>
     *
     * @param appContext application context
     * @return {@code true} if the temp file was created, or {@code false} otherwise
     */
    private boolean createTempHostsFile(Context appContext) {
        OutputStreamWriter writer = null;
        try {
            FileOutputStream out = appContext.openFileOutput(HOSTS_FILE_NAME, Context.MODE_PRIVATE);
            writer = new OutputStreamWriter(out);

            for (Host host : getHosts(false)) {
                writer.append(host.toString()).append(LINE_SEPARATOR);
            }
            writer.flush();
        } catch (IOException e) {
            Timber.e(e, "Error creating temporary hosts file");
            return false;
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    Timber.e(e, "Error while closing writer");
                }
            }
        }
        return true;
    }

    /**
     * Executes a single argument root command.
     * <p><b>Must be in an async call.</b></p>
     *
     * @param command   a command, ie {@code "rm -f"}, {@code "chmod 644"}...
     * @param uniqueArg the unique argument for the command, usually the file name
     */
    private void runRootCommand(String command, String uniqueArg) throws IOException, TimeoutException, RootDeniedException {
        Command cmd = new Command(0, false, String.format(Locale.US, "%s %s", command, uniqueArg));
        RootShell.getShell(true).add(cmd);
    }
}