package com.dragonssh.xhttpdemo.core.tunnel;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.ProxyInfo;
import android.os.Build;
import android.os.Handler;
//import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.dragonssh.xhttpdemo.core.R;
import com.dragonssh.xhttpdemo.core.config.Settings;
import com.dragonssh.xhttpdemo.core.logger.SkStatus;
import com.dragonssh.xhttpdemo.core.tunnel.vpn.TunnelState;
import com.dragonssh.xhttpdemo.core.tunnel.vpn.TunnelVpnManager;
import com.dragonssh.xhttpdemo.core.tunnel.vpn.TunnelVpnService;
import com.dragonssh.xhttpdemo.core.tunnel.vpn.TunnelVpnSettings;
import com.dragonssh.xhttpdemo.core.tunnel.vpn.VpnUtils;
import com.trilead.ssh2.Connection;
import com.trilead.ssh2.ConnectionMonitor;
import com.trilead.ssh2.DebugLogger;
import com.trilead.ssh2.DynamicPortForwarder;
import com.trilead.ssh2.InteractiveCallback;
import com.trilead.ssh2.KnownHosts;
import com.trilead.ssh2.ProxyData;
import com.trilead.ssh2.ServerHostKeyVerifier;
import com.trilead.ssh2.transport.TransportManager;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;

public class TunnelManagerThread
	implements Runnable, ConnectionMonitor, InteractiveCallback,
		ServerHostKeyVerifier, DebugLogger
{
	private static final String TAG = TunnelManagerThread.class.getSimpleName();
	
	private OnStopClient mListener;
	private Context mContext;
	private Handler mHandler;
	private Settings mConfig;

	// Immutable XHTTP settings for this service session. Automatic reconnect must
	// recreate the same transport that originally established the VPN, regardless
	// of any UI/preference change that happens while the tunnel is active.
	private final String mSessionXhttpEndpoint;
	private final String mSessionXhttpPort;
	private final String mSessionUsername;
	private final String mSessionPassword;
	private final String mSessionXhttpSni;
	private final String mSessionXhttpHost;
	private final String mSessionXhttpPath;
	private final boolean mSessionXhttpTls;

	private volatile boolean mRunning = false, mStopping = false, mStarting = false;
	
	private CountDownLatch mTunnelThreadStopSignal;
	//private ConnectivityManager mCmgr;
	
	public interface OnStopClient {
		void onStop();
	}
	
	public TunnelManagerThread(Handler handler, Context context, String password) {
		mContext = context;
		mHandler = handler;
		
		mConfig = new Settings(context);
		mSessionXhttpEndpoint = mConfig.getPrivString(Settings.XHTTP_ENDPOINT_KEY);
		mSessionXhttpPort = mConfig.getPrivString(Settings.XHTTP_PORT_KEY);
		mSessionUsername = mConfig.getPrivString(Settings.USUARIO_KEY);
		mSessionPassword = password;
		mSessionXhttpSni = mConfig.getPrivString(Settings.CUSTOM_SNI);
		mSessionXhttpHost = mConfig.getPrivString(Settings.XHTTP_HOST_KEY);
		String configuredPath = mConfig.getPrivString(Settings.XHTTP_PATH_KEY);
		mSessionXhttpPath = configuredPath == null || configuredPath.isEmpty()
				? "/xhttp" : configuredPath;
		mSessionXhttpTls = !"0".equals(
				mConfig.getPrivString(Settings.XHTTP_TLS_KEY));
	}
	
	public void setOnStopClientListener(OnStopClient listener) {
		mListener = listener;
	}

	@Override
	public void run()
	{
		mStarting = true;
		mTunnelThreadStopSignal = new CountDownLatch(1);
		
		SkStatus.logInfo(mContext.getString(R.string.starting_service_ssh));
		
		int tries = 0;
		while (!mStopping) {
			try {
				if (!TunnelUtils.isNetworkOnline(mContext)) {
					SkStatus.updateStateString(SkStatus.SSH_AGUARDANDO_REDE, mContext.getString(R.string.state_nonetwork));

					SkStatus.logInfo(R.string.state_nonetwork);
					
					try {
						Thread.sleep(5000);
					} catch(InterruptedException e2) {
						stopAll();
						break;
					}
				}
				else {
					if (tries > 0)
						SkStatus.logInfo(mContext.getString(R.string.state_reconnecting));

					try {
						Thread.sleep(500);
					} catch(InterruptedException e2) {
						stopAll();
						break;
					}

					startSshClient();
					break;
				}
			} catch(Exception e) {

				SkStatus.logError(mContext.getString(R.string.state_disconnected));
				closeSSH();
				
				try {
					Thread.sleep(500);
				} catch(InterruptedException e2) {
					stopAll();
					break;
				}
			}
			
			tries++;
		}
		
		mStarting = false;
		
		if (!mStopping) {
			try {
				mTunnelThreadStopSignal.await();
			} catch(InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
		
		if (mListener != null) {
			mListener.onStop();
		}
	}
	
	public void stopAll() {
		if (mStopping) return;

		SkStatus.updateStateString(SkStatus.SSH_PARANDO, mContext.getString(R.string.stopping_service_ssh));
		SkStatus.logInfo(mContext.getString(R.string.stopping_service_ssh));

		new Thread(new Runnable() {
			@Override
			public void run() {
				mStopping = true;

				if (mTunnelThreadStopSignal != null)
					mTunnelThreadStopSignal.countDown();

				closeSSH();

				try {
					Thread.sleep(1000);
				} catch(InterruptedException e){
					SkStatus.updateStateString(SkStatus.SSH_DESCONECTADO, mContext.getString(R.string.state_disconnected));
					mRunning = false;
					mStarting = false;
					mReconnecting = false;
				}

				SkStatus.updateStateString(SkStatus.SSH_DESCONECTADO, mContext.getString(R.string.state_disconnected));
				//SkStatus.logInfo("Wakelock desativado!");
				mRunning = false;
				mStarting = false;
				mReconnecting = false;
			}
		}).start();
	}
	
	
	/**
	 * Forwarder
	*/

	protected void startForwarder(int portaLocal) throws Exception {
		if (!mConnected) {
			throw new Exception();
		}
		
		startForwarderSocks(portaLocal);
		startPinger(15);
		
		startTunnelVpnService();
		
		new Thread(new Runnable() {
			@Override
			public void run() {
				while (true) {
					if (!mConnected) break;
					
					try {
						Thread.sleep(2000);
					} catch(InterruptedException e) {
						break;
					}
					
					if (lastPingLatency > 0) {
						// SkStatus.logInfo(String.format("Average ping: %d ms", lastPingLatency));
						break;
					}
				}
			}
		}).start();
	}

	protected void stopForwarder() {
		stopTunnelVpnService();
		
		stopForwarderSocks();
	}
	
	
	/**
	* Cliente SSH
	*/
	
	private final static int AUTH_TRIES = 1;
	private final static int RECONNECT_TRIES = 5;
	
	private volatile Connection mConnection;
	
	private volatile boolean mConnected = false;
	
	protected void startSshClient() throws Exception {
		mStopping = false;
		mRunning = true;

		String server = mSessionXhttpEndpoint;
		int port = Integer.parseInt(mSessionXhttpPort);
		String username = mSessionUsername;
		String password = mSessionPassword;
		int localPort = Integer.parseInt(mConfig.getPrivString(Settings.PORTA_LOCAL_KEY));

		try {
			connectSshTransport(server, port);
			for (int i = 0; i < AUTH_TRIES; i++) {
				if (mStopping) return;
				try {
					authenticate(username, password, null);
					break;
				} catch (IOException e) {
					if (i + 1 >= AUTH_TRIES) throw e;
					Thread.sleep(3000);
				}
			}

			SkStatus.updateStateString(SkStatus.SSH_CONECTADO, mContext.getString(R.string.conectssh));
			SkStatus.logInfo(mContext.getString(R.string.state_connected));
			startForwarder(localPort);
		} catch (Exception e) {
			mConnected = false;
			throw e;
		}
	}


	/**
	 * Closes the complete SSH stack, including the Android VPN interface.
	 * This path is used for an explicit disconnect or after reconnect retries
	 * have been exhausted.
	 */
	public synchronized void closeSSH() {
		closeSSH(false);
	}

	/**
	 * Closes only the replaceable SSH/XHTTP transport when {@code keepVpnAlive}
	 * is true. The VpnService, TUN file descriptor, routes and tun2socks process
	 * remain alive and continue pointing at the same local SOCKS address. Once
	 * SSH reconnects, the dynamic SOCKS listener is recreated on that address,
	 * avoiding an Android VPN interface teardown during a transient outage.
	 */
	private synchronized void closeSSH(boolean keepVpnAlive) {
		stopVpnWatchdog();

		if (keepVpnAlive && isServiceVpnRunning()) {
			SkStatus.logDebug("Silent reconnect: keeping Android VPN and local SOCKS relay active");
			stopForwarderSocks(true);
		} else {
			stopForwarder();
		}

		stopPinger();
		mConnected = false;


		Connection connection = mConnection;
		mConnection = null;
		if (connection != null) {
			SkStatus.logDebug(mContext.getString(R.string.stpssh));
			connection.close();
		}
	}
	
	protected void connectSshTransport(String server, int port) throws Exception {
		if (!mStarting) {
			throw new Exception();
		}
		

		// Establish the SSH transport through XHTTP.
		try {

			mConnection = new Connection(server, port);

			
			// delay sleep
			if (mConfig.getIsDisabledDelaySSH()) {
				mConnection.setTCPNoDelay(true);
			}

			if (mConfig.ssh_compression()){
				mConnection.setCompression(true);
				SkStatus.logInfo(mContext.getString(R.string.compressg));
			}

			// XHTTP is the only transport exposed by this sample.
			addProxy(mConnection);

			// Monitor only this concrete Connection instance. During a silent
			// reconnect the old transport is closed intentionally, and Trilead may
			// deliver its close callback after the replacement connection is already
			// authenticated. Without this identity check, that stale callback can
			// stop or reconnect the new session.
			final Connection monitoredConnection = mConnection;
			monitoredConnection.addConnectionMonitor(new ConnectionMonitor() {
				@Override
				public void connectionLost(Throwable reason) {
					if (mConnection != monitoredConnection) {
						SkStatus.logDebug("Ignoring stale SSH/XHTTP close callback");
						return;
					}
					TunnelManagerThread.this.connectionLost(reason);
				}

				@Override
				public void onReceiveInfo(int infoId, String infoMsg) {
					if (mConnection != monitoredConnection) {
						SkStatus.logDebug("Ignoring stale SSH/XHTTP info callback");
						return;
					}
					TunnelManagerThread.this.onReceiveInfo(infoId, infoMsg);
				}
			});
			
			if (Build.VERSION.SDK_INT >= 23) {
				ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
				ProxyInfo proxy = cm.getDefaultProxy();
				if (proxy != null) {
					SkStatus.logInfo("Network proxy: " + String.format("%s:%d", proxy.getHost(), proxy.getPort()));
				}
			}
			SkStatus.updateStateString(SkStatus.SSH_CONECTANDO, mContext.getString(R.string.state_connecting));
			SkStatus.logInfo(R.string.state_connecting);
			
			mConnection.connect(this, 10*1000, 20*1000);

			mConnected = true;

		} catch(Exception e) {

			StringWriter sw = new StringWriter();
			e.printStackTrace(new PrintWriter(sw));

			Throwable root = e.getCause() != null ? e.getCause() : e;
			String cause = root.toString();
			String stage = useProxy && cause.contains("Key exchange was not finished")
					? "SSH key exchange failed" : "SSH/XHTTP connection failed";
			SkStatus.logError(stage + ": " + cause);
			
			throw new Exception(e);
		}
	}


	/**
	 * Authentication
	 */

	private static final String AUTH_PASSWORD = "password";

	protected void authenticate(String username, String password, String ignoredKeyPath) throws IOException {
		if (!mConnected) throw new IOException("SSH transport is not connected");
		SkStatus.updateStateString(SkStatus.SSH_AUTENTICANDO, mContext.getString(R.string.state_auth));
		boolean authenticated = false;
		try {
			if (mConnection.isAuthMethodAvailable(username, AUTH_PASSWORD)) {
				authenticated = mConnection.authenticateWithPassword(username, password);
			}
		} catch (Exception e) {
			throw new IOException("SSH password authentication failed", e);
		}
		if (!authenticated || !mConnection.isAuthenticationComplete()) {
			stopAll();
			throw new IOException(mContext.getString(R.string.dataerror));
		}
		SkStatus.logInfo(mContext.getString(R.string.state_auth_success));
	}

	// XXX: Is it right?
	@Override
	public String[] replyToChallenge(String name, String instruction,
			int numPrompts, String[] prompt, boolean[] echo) throws Exception {
		String[] responses = new String[numPrompts];
		for (int i = 0; i < numPrompts; i++) {
			if (prompt[i].toLowerCase().contains("password")) {
				responses[i] = mSessionPassword;
			}
		}
		return responses;
	}


	/**
	 * ServerHostKeyVerifier
	 * Fingerprint
	 */

	@Override
	public boolean verifyServerHostKey(String hostname, int port,
		String serverHostKeyAlgorithm, byte[] serverHostKey)
	throws Exception {

		String fingerPrint = KnownHosts.createHexFingerprint(
			serverHostKeyAlgorithm, serverHostKey);
		String key = Settings.SSH_HOST_KEY_PREFIX + hostname + ":" + port;
		String pinned = mConfig.getPrefsPrivate().getString(key, "");
		if (pinned == null || pinned.isEmpty()) {
			mConfig.getPrefsPrivate().edit().putString(key, fingerPrint).apply();
			SkStatus.logInfo("SSH host key trusted on first use: " + fingerPrint);
			return true;
		}
		if (!pinned.equals(fingerPrint)) {
			SkStatus.logError("SSH host key changed. Expected " + pinned + " but received " + fingerPrint);
			return false;
		}
		SkStatus.logDebug("SSH host key verified: " + fingerPrint);
		return true;
	}


	/**
	 * Proxy
	 */

	private boolean useProxy = false;

	protected void addProxy(Connection conn) throws Exception {
		useProxy = true;
		try {
			ProxyData xhttpData = new XHttpProxy(
				mSessionXhttpEndpoint,
				Integer.parseInt(mSessionXhttpPort),
				mSessionXhttpTls,
				mSessionXhttpSni,
				mSessionXhttpPath,
				mSessionXhttpHost,
				mContext);
			conn.setProxyData(xhttpData);
		} catch (Exception e) {
			throw new Exception("Unable to create XHTTP transport", e);
		}
	}




	/**
	 * Socks5 Forwarder
	 */

	private DynamicPortForwarder dpf;
	private PersistentSocksRelay socksRelay;
	private int socksRelayPort = -1;

	private synchronized void startForwarderSocks(int portaLocal) throws Exception {
		if (!mConnected || mConnection == null) {
			throw new Exception();
		}

		try {
			ensurePersistentSocksRelay(portaLocal);

			// A reconnect gets a fresh internal SOCKS port. The Android VPN keeps
			// using portaLocal, which belongs to the persistent relay.
			closeDynamicForwarderOnly();

			int nThreads = mConfig.getMaximoThreadsSocks();
			InetSocketAddress backendAddress = new InetSocketAddress("127.0.0.1", 0);

			if (nThreads > 0) {
				dpf = mConnection.createDynamicPortForwarder(backendAddress, nThreads);
			} else {
				dpf = mConnection.createDynamicPortForwarder(backendAddress);
			}

			int backendPort = dpf.getLocalPort();
			if (backendPort <= 0) {
				throw new IOException("Invalid SSH SOCKS backend port");
			}

			socksRelay.setBackendPort(backendPort);
			SkStatus.logDebug("Persistent SOCKS relay backend ready");
		} catch (Exception e) {
			closeDynamicForwarderOnly();
			if (socksRelay != null) {
				socksRelay.clearBackendPort();
			}
			throw new Exception(e);
		}
	}

	private void ensurePersistentSocksRelay(int portaLocal) throws IOException {
		if (socksRelay != null && socksRelay.isRunning() && socksRelayPort == portaLocal) {
			return;
		}

		stopPersistentSocksRelay();
		PersistentSocksRelay relay = new PersistentSocksRelay(portaLocal);
		relay.start();
		socksRelay = relay;
		socksRelayPort = portaLocal;
	}

	private synchronized void stopForwarderSocks() {
		stopForwarderSocks(false);
	}

	private synchronized void stopForwarderSocks(boolean keepRelayAlive) {
		if (socksRelay != null) {
			socksRelay.clearBackendPort();
		}
		closeDynamicForwarderOnly();

		if (!keepRelayAlive) {
			stopPersistentSocksRelay();
		}
	}

	private void closeDynamicForwarderOnly() {
		DynamicPortForwarder forwarder = dpf;
		dpf = null;
		if (forwarder != null) {
			try {
				forwarder.close();
			} catch (IOException ignored) {
			}
		}
	}

	private void stopPersistentSocksRelay() {
		PersistentSocksRelay relay = socksRelay;
		socksRelay = null;
		socksRelayPort = -1;
		if (relay != null) {
			relay.stop();
		}
	}


	/**
	 * Pinger
	 */

	private Thread thPing;
	private long lastPingLatency = -1;
	private static final long SSH_PING_TIMEOUT_MS = 20_000L;
	
	private void startPinger(final int timePing) throws Exception {
		if (!mConnected) {
			throw new Exception();
		}


		thPing = new Thread() {
			@Override
			public void run() {
				while (mConnected) {
					try {
						makePinger();
					} catch(InterruptedException e) {
						break;
					}
				}
			}
			
			private synchronized void makePinger() throws InterruptedException {
				try {
					Connection connection = mConnection;
					if (connection != null) {
						long ping = connection.ping(SSH_PING_TIMEOUT_MS);
						if (lastPingLatency < 0) {
							lastPingLatency = ping;
						}
					}
					else throw new InterruptedException();
				} catch(Exception e) {
					if (!mStopping && !mReconnecting) {
						String detail = e.getMessage();
						requestVpnReconnect("SSH ping failed" +
								(detail != null && !detail.isEmpty() ? ": " + detail : ""));
					}
					throw new InterruptedException();
				}
				
				if (timePing == 0)
					return;

				if (timePing > 0)
					sleep(timePing*1000);
				else {

					throw new InterruptedException();
				}
			}
		};

		// Start the pinger thread.
		thPing.start();
	}

	private synchronized void stopPinger() {
		if (thPing != null && thPing.isAlive()) {

			
			thPing.interrupt();
			thPing = null;
		}
	}
	
	/**
	 * Connection Monitor
	 */

	@Override
	public void connectionLost(Throwable reason)
	{
		if (mStarting || mStopping || mReconnecting) {
			return;
		}
		
		SkStatus.logError(mContext.getString(R.string.log_conection_lost));

		if (reason != null) {
			String reasonMessage = reason.getMessage();
			SkStatus.logDebug("SSH transport closed: " + reason);
			if (reasonMessage != null && reasonMessage.contains(
					"There was a problem during connect")) {
				reconnectSSH();
				return;
			} else if (reasonMessage != null && reasonMessage.contains(
						   "Closed due to user request")) {
				// Local close operations are already coordinated by stopAll() or
				// closeSSH(true). Never turn this asynchronous library callback into
				// another full service shutdown.
				SkStatus.logDebug("Ignoring locally requested transport close");
				return;
			} else if (reasonMessage != null && reasonMessage.contains(
						   "The connect timeout expired")) {
				reconnectSSH();
				return;
			}
		} else {
			reconnectSSH();
			return;
		}
		
		reconnectSSH();
	}

	public volatile boolean mReconnecting = false;
	
	public void reconnectSSH() {
		if (mStarting || mStopping || mReconnecting) {
			return;
		}
		
		mReconnecting = true;
		
		// Keep the Android VpnService/TUN alive. Only the failed SSH/XHTTP
		// transport and its local dynamic forwarder are replaced.
		closeSSH(true);
		
		SkStatus.updateStateString(SkStatus.SSH_RECONECTANDO, mContext.getString(R.string.reconecti));

		try {
			Thread.sleep(1000);
		} catch(InterruptedException e) {
			mReconnecting = false;
			return;
		}

		for (int i = 0; i < RECONNECT_TRIES; i++) {
			if (mStopping) {
				mReconnecting = false;
				return;
			}

			int sleepTime = 5;
			if (!TunnelUtils.isNetworkOnline(mContext)) {
				SkStatus.updateStateString(SkStatus.SSH_AGUARDANDO_REDE, mContext.getString(R.string.wnetwork));

				SkStatus.logInfo(R.string.state_nonetwork);
			}
			else {
				sleepTime = 3;
				mStarting = true;
				SkStatus.updateStateString(SkStatus.SSH_RECONECTANDO, mContext.getString(R.string.reconecti));

				SkStatus.logWarning(mContext.getString(R.string.state_reconnecting));

				try {
					startSshClient();
					
					mStarting = false;
					mReconnecting = false;
					//mConnected = true;

					return;
				} catch(Exception e) {
					SkStatus.logError(mContext.getString(R.string.state_disconnected));
					closeSSH(true);
				}
				
				mStarting = false;
			}

			try {
				Thread.sleep(sleepTime*1000);
				i--;
			} catch(InterruptedException e2){
				mReconnecting = false;
				return;
			}
		}
		
		mReconnecting = false;

		stopAll();
	}

	@Override
	public void onReceiveInfo(int id, String msg) {
		if (id == SERVER_BANNER) {
			SkStatus.logInfo(mContext.getString(R.string.log_server_banner) + " " + msg);
		}
	}


	/**
	 * Debug Logger
	 */

	@Override
	public void log(int level, String className, String message)
	{
		SkStatus.logDebug(String.format("%s: %s", className, message));
	}
	

	/**
	 * Vpn Tunnel
	 */
	 
	String serverAddr;
	private static final int VPN_WATCHDOG_GRACE_MS = 8000;
	private static final int VPN_WATCHDOG_INTERVAL_MS = 3000;
	private static final int VPN_WATCHDOG_MAX_MISSES = 3;
	private volatile boolean mVpnTunnelBroadcastRegistered = false;
	private volatile boolean mVpnWatchdogRunning = false;
	private volatile boolean mVpnReconnectRequested = false;
	private Thread mVpnWatchdogThread;

	protected void startTunnelVpnService() throws IOException {
		if (!mConnected) {
			throw new IOException();
		}
		

		registerVpnTunnelReceiver();

		String m_socksServerAddress = String.format("127.0.0.1:%s", mConfig.getPrivString(Settings.PORTA_LOCAL_KEY));
		boolean m_dnsForward = mConfig.getVpnDnsForward();
		String m_udpResolver = mConfig.getVpnUdpForward() ? mConfig.getVpnUdpResolver() : null;

		String servidorIP = mConfig.getPrivString(Settings.XHTTP_ENDPOINT_KEY);

		try {
			InetAddress servidorAddr = TransportManager.createInetAddress(servidorIP);
			serverAddr = servidorIP = servidorAddr.getHostAddress();
		} catch(UnknownHostException e) {
			throw new IOException(mContext.getString(R.string.error_server_ip_invalid));
		}
		
		String[] m_excludeIps = mergeExcludeIps(servidorIP, mConfig.getVpnBypassList());

		String[] m_dnsResolvers = null;
		if (m_dnsForward) {
			List<String> configuredDnsResolvers = new ArrayList<String>();
			String primaryDnsResolver = mConfig.getVpnDnsResolver();
			String secondaryDnsResolver = mConfig.getVpnDnsResolverSecondary();

			if (primaryDnsResolver != null && !primaryDnsResolver.trim().isEmpty()) {
				configuredDnsResolvers.add(primaryDnsResolver.trim());
			}

			if (secondaryDnsResolver != null && !secondaryDnsResolver.trim().isEmpty() &&
				!secondaryDnsResolver.trim().equals(primaryDnsResolver != null ? primaryDnsResolver.trim() : "")) {
				configuredDnsResolvers.add(secondaryDnsResolver.trim());
			}

			if (configuredDnsResolvers.isEmpty()) {
				configuredDnsResolvers.add("8.8.8.8");
				configuredDnsResolvers.add("8.8.4.4");
			}

			m_dnsResolvers = configuredDnsResolvers.toArray(new String[0]);
		}
		else {
			List<String> lista = VpnUtils.getNetworkDnsServer(mContext);
			m_dnsResolvers = lista.toArray(new String[0]);
		}

		if (isServiceVpnRunning()) {
			//Log.d(TAG, "already running service");

			TunnelVpnManager tunnelManager = TunnelState.getTunnelState()
				.getTunnelManager();
			
			if (tunnelManager != null) {
				tunnelManager.restartTunnel(m_socksServerAddress);
			}

			return;
		}

		Intent startTunnelVpn = new Intent(mContext, TunnelVpnService.class);
		startTunnelVpn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

		TunnelVpnSettings settings = new TunnelVpnSettings(m_socksServerAddress, m_dnsForward, m_dnsResolvers,
			(m_dnsForward && m_udpResolver == null || !m_dnsForward && m_udpResolver != null), m_udpResolver, m_excludeIps,
				true, mConfig.getIsFilterBypassMode(), mConfig.getFilterApps(), mConfig.getIsTetheringSubnet(), mConfig.getDisableIpv6Tunnel());
		startTunnelVpn.putExtra(TunnelVpnManager.VPN_SETTINGS, settings);

		if (mContext.startService(startTunnelVpn) == null) {
			SkStatus.logInfo(mContext.getString(R.string.failedvpn));

			throw new IOException(mContext.getString(R.string.failedvpn));
		}

		TunnelState.getTunnelState().setStartingTunnelManager();
	}

	public static boolean isServiceVpnRunning() {
		TunnelState tunnelState = TunnelState.getTunnelState();
		return tunnelState.getStartingTunnelManager() || tunnelState.getTunnelManager() != null;
	}


	private String[] mergeExcludeIps(String serverIp, String[] extraIps) {
		LinkedHashSet<String> merged = new LinkedHashSet<String>();

		// Do not automatically bypass the SSH/proxy server IP.
		// The control sockets are protected with VpnService.protect().

		if (extraIps != null) {
			for (String host : extraIps) {
				if (host == null) {
					continue;
				}
				String normalized = host.trim();
				if (normalized.isEmpty()) {
					continue;
				}

				try {
					InetAddress[] resolved = InetAddress.getAllByName(normalized);
					for (InetAddress address : resolved) {
						if (address != null && address.getHostAddress() != null && !address.getHostAddress().trim().isEmpty()) {
							merged.add(address.getHostAddress().trim());
						}
					}
				} catch (UnknownHostException e) {
					SkStatus.logDebug("Unable to resolve VPN bypass host: " + normalized + " -> " + e);
				}
			}
		}

		return merged.toArray(new String[0]);
	}

	protected synchronized void stopTunnelVpnService() {
		stopVpnWatchdog();
		unregisterVpnTunnelReceiver();

		if (!isServiceVpnRunning()) {
			return;
		}
		
		// Use signalStopService to asynchronously stop the service.
		// 1. VpnService doesn't respond to stopService calls
		// 2. The UI will not block while waiting for stopService to return
		// This scheme assumes that the UI will monitor that the service is
		// running while the Activity is not bound to it. This is the state
		// while the tunnel is shutting down.
		
		
		TunnelVpnManager currentTunnelManager = TunnelState.getTunnelState()
			.getTunnelManager();
		
		if (currentTunnelManager != null) {
			currentTunnelManager.signalStopService();
		}
		
		/*if (mThreadLocation != null && mThreadLocation.isAlive()) {
			mThreadLocation.interrupt();
		}
		mThreadLocation = null;*/

	}

	private synchronized void registerVpnTunnelReceiver() {
		if (mVpnTunnelBroadcastRegistered) {
			return;
		}

		IntentFilter broadcastFilter =
			new IntentFilter(TunnelVpnService.TUNNEL_VPN_DISCONNECT_BROADCAST);
		broadcastFilter.addAction(TunnelVpnService.TUNNEL_VPN_START_BROADCAST);

		LocalBroadcastManager.getInstance(mContext)
			.registerReceiver(m_vpnTunnelBroadcastReceiver, broadcastFilter);
		mVpnTunnelBroadcastRegistered = true;
	}

	private synchronized void unregisterVpnTunnelReceiver() {
		if (!mVpnTunnelBroadcastRegistered) {
			return;
		}

		try {
			LocalBroadcastManager.getInstance(mContext)
				.unregisterReceiver(m_vpnTunnelBroadcastReceiver);
		} catch (Exception ignored) {
		}
		mVpnTunnelBroadcastRegistered = false;
	}

	private synchronized void startVpnWatchdog() {
		/*
		 * Do not use ConnectivityManager.getActiveNetwork() to validate this
		 * VPN. V4 intentionally excludes this package UID from the TUN so its
		 * control sockets can reconnect on the physical network. For a disallowed
		 * UID Android correctly reports Wi-Fi/mobile as the active network, not
		 * TRANSPORT_VPN. The previous watchdog therefore generated a false VPN
		 * loss every few seconds and tore down a healthy connection.
		 *
		 * Real VPN loss is already reported by TunnelVpnService.onDestroy(),
		 * onRevoke(), VPN-start failure, and TunnelVpnManager.onTunnelStopped().
		 */
		stopVpnWatchdog();
	}

	private synchronized void stopVpnWatchdog() {
		mVpnWatchdogRunning = false;
		if (mVpnWatchdogThread != null) {
			mVpnWatchdogThread.interrupt();
			mVpnWatchdogThread = null;
		}
	}

	private synchronized void requestVpnReconnect(final String reason) {
		if (mStopping || mReconnecting || mVpnReconnectRequested) {
			return;
		}

		mVpnReconnectRequested = true;
		SkStatus.logWarning("VPN lost. Reconnecting: " + reason);

		new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					for (int i = 0; i < 10 && mStarting && !mStopping; i++) {
						Thread.sleep(500);
					}
					reconnectSSH();
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				} finally {
					mVpnReconnectRequested = false;
				}
			}
		}, "vpn-reconnect-request").start();
	}
	
	//private Thread mThreadLocation;

	// Local BroadcastReceiver
	private BroadcastReceiver m_vpnTunnelBroadcastReceiver = new BroadcastReceiver() {
		@Override
		public synchronized void onReceive(Context context, Intent intent) {
			final String action = intent.getAction();

			if (TunnelVpnService.TUNNEL_VPN_START_BROADCAST.equals(action)) {
				boolean startSuccess = intent.getBooleanExtra(TunnelVpnService.TUNNEL_VPN_START_SUCCESS_EXTRA, true);

				if (!startSuccess) {
					requestVpnReconnect("VPN start failed");
				} else {
					startVpnWatchdog();
				}
				
			} else if (TunnelVpnService.TUNNEL_VPN_DISCONNECT_BROADCAST.equals(action)) {
				requestVpnReconnect("VPN service disconnected");
			}
		}
	};
	
}
