package net.i2p.router.update;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.Locale;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.router.RouterContext;
import net.i2p.router.RouterVersion;
import net.i2p.router.web.ConfigUpdateHandler;
import net.i2p.update.*;
import static net.i2p.update.UpdateMethod.*;
import net.i2p.util.EepGet;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.PartialEepGet;
import net.i2p.util.PortMapper;
import net.i2p.util.SSLEepGet;
import net.i2p.util.VersionComparator;

/**
 *  The downloader for router signed updates,
 *  and the base class for all the other Checkers and Runners.
 *
 *  @since 0.9.4 moved from UpdateHandler
 *
 */
class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusListener {
    protected final RouterContext _context;
    protected final Log _log;
    protected final ConsoleUpdateManager _mgr;
    protected final UpdateType _type;
    protected final UpdateMethod _method;
    protected final List<URI> _urls;
    protected final String _updateFile;
    protected volatile boolean _isRunning;
    protected boolean done;
    protected EepGet _get;
    /** tells the listeners what mode we are in - set to true in extending classes for checks */
    protected boolean _isPartial;
    /** set by the listeners on completion */
    protected String _newVersion;
    /** 56 byte header, only used for suds */
    protected final ByteArrayOutputStream _baos;
    protected URI _currentURI;
    private final String _currentVersion;

    protected static final long CONNECT_TIMEOUT = 55*1000;
    protected static final long INACTIVITY_TIMEOUT = 5*60*1000;
    protected static final long NOPROXY_INACTIVITY_TIMEOUT = 60*1000;

    /**
     *  Uses router version for partial checks
     */
    public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type, List<URI> uris) { 
        this(ctx, mgr, type, uris, RouterVersion.VERSION);
    }

    /**
     *  Uses router version for partial checks
     *  @since 0.9.9
     */
    public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type,
                        UpdateMethod method, List<URI> uris) {
        this(ctx, mgr, type, method, uris, RouterVersion.VERSION);
    }

    /**
     *  @param currentVersion used for partial checks
     *  @since 0.9.7
     */
    public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type,
                        List<URI> uris, String currentVersion) { 
        this(ctx, mgr, type, HTTP, uris, currentVersion);
    }

    /**
     *  @param method HTTP, HTTP_CLEARNET, or HTTPS_CLEARNET
     *  @param currentVersion used for partial checks
     *  @since 0.9.9
     */
    public UpdateRunner(RouterContext ctx, ConsoleUpdateManager mgr, UpdateType type,
                        UpdateMethod method, List<URI> uris, String currentVersion) { 
        super("Update Runner");
        setDaemon(true);
        _context = ctx;
        _log = ctx.logManager().getLog(getClass());
        _mgr = mgr;
        _type = type;
        _method = method;
        _urls = uris;
        _baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
        _updateFile = (new File(ctx.getTempDir(), "update" + ctx.random().nextInt() + ".tmp")).getAbsolutePath();
        _currentVersion = currentVersion;
    }

    //////// begin UpdateTask methods

    public boolean isRunning() { return _isRunning; }

    public void shutdown() {
        _isRunning = false;
        interrupt();
    }

    public UpdateType getType() { return _type; }

    public UpdateMethod getMethod() { return _method; }

    public URI getURI() { return _currentURI; }

    public String getID() { return ""; }

    //////// end UpdateTask methods

    @Override
    public void run() {
        _isRunning = true;
        try {
            update();
        } catch (Throwable t) {
            _mgr.notifyTaskFailed(this, "", t);
        } finally {
            _isRunning = false;
        }
    }

    /**
     *  Loop through the entire list of update URLs.
     *  For each one, first get the version from the first 56 bytes and see if
     *  it is newer than what we are running now.
     *  If it is, get the whole thing.
     */
    protected void update() {
        // Do a PartialEepGet on the selected URL, check for version we expect,
        // and loop if it isn't what we want.
        // This will allows us to do a release without waiting for the last host to install the update.
        // Alternative: In bytesTransferred(), Check the data in the output file after
        // we've received at least 56 bytes. Need a cancel() method in EepGet ?

        boolean shouldProxy;
        String proxyHost;
        int proxyPort;
        boolean isSSL = false;
        if (_method == HTTP) {
            shouldProxy = _context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY);
            if (shouldProxy) {
                proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
                proxyPort = ConfigUpdateHandler.proxyPort(_context);
                if (proxyPort == ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT &&
                    proxyHost.equals(ConfigUpdateHandler.DEFAULT_PROXY_HOST) &&
                    _context.portMapper().getPort(PortMapper.SVC_HTTP_PROXY) < 0) {
                    String msg = _t("HTTP client proxy tunnel must be running");
                    if (_log.shouldWarn())
                        _log.warn(msg);
                    updateStatus("<b>" + msg + "</b>");
                    _mgr.notifyTaskFailed(this, msg, null);
                    return;
                }
            } else {
                // TODO, wrong method, fail
                proxyHost = null;
                proxyPort = 0;
            }
        } else if (_method == HTTP_CLEARNET) {
            shouldProxy = false;
            proxyHost = null;
            proxyPort = 0;
        } else if (_method == HTTPS_CLEARNET) {
            shouldProxy = false;
            proxyHost = null;
            proxyPort = 0;
            isSSL = true;
        } else {
            throw new IllegalArgumentException();
        }

        if (_urls.isEmpty()) {
            // not likely, don't bother translating
            String msg = "Update source list is empty, cannot download update";
            updateStatus("<b>" + msg + "</b>");
            _log.error(msg);
            _mgr.notifyTaskFailed(this, msg, null);
            return;
        }

        for (URI uri : _urls) {
            _currentURI = uri;
            String updateURL = uri.toString();
            if ((_method == HTTP && !"http".equals(uri.getScheme())) ||
                (_method == HTTP_CLEARNET && !"http".equals(uri.getScheme())) ||
                (_method == HTTPS_CLEARNET && !"https".equals(uri.getScheme())) ||
                uri.getHost() == null ||
                (_method != HTTP && uri.getHost().toLowerCase(Locale.US).endsWith(".i2p"))) {
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Bad update URI " + uri + " for method " + _method);
                continue;
            }

            updateStatus("<b>" + _t("Updating from {0}", linkify(updateURL)) + "</b>");
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Selected update URL: " + updateURL);

            // Check the first 56 bytes for the version
            // FIXME PartialEepGet works with clearnet but not with SSL
            _newVersion = null;
            if (!isSSL) {
                _isPartial = true;
                _baos.reset();
                try {
                    // no retries
                    _get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, updateURL, TrustedUpdate.HEADER_BYTES);
                    _get.addStatusListener(UpdateRunner.this);
                    _get.fetch(CONNECT_TIMEOUT);
                } catch (Throwable t) {
                }
                _isPartial = false;
                if (_newVersion == null)
                    continue;
            }

            // Now get the whole thing
            try {
                if (shouldProxy)
                    // 40 retries!!
                    _get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, updateURL, false);
                else if (isSSL)
                    _get = new SSLEepGet(_context, _updateFile, updateURL);
                else
                    _get = new EepGet(_context, 1, _updateFile, updateURL, false);
                _get.addStatusListener(UpdateRunner.this);
                _get.fetch(CONNECT_TIMEOUT, -1, shouldProxy ? INACTIVITY_TIMEOUT : NOPROXY_INACTIVITY_TIMEOUT);
            } catch (Throwable t) {
                _log.error("Error updating", t);
            }
            if (this.done)
                break;
        }
        (new File(_updateFile)).delete();
        if (!this.done)
            _mgr.notifyTaskFailed(this, "", null);
    }
    
    // EepGet Listeners below.
    // We use the same for both the partial and the full EepGet,
    // with a couple of adjustments depending on which mode.

    public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Attempt failed on " + url, cause);
        // ignored
        _mgr.notifyAttemptFailed(this, url, null);
    }

    /** subclasses should override */
    public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
        if (_isPartial)
            return;
        long d = currentWrite + bytesTransferred;
        String status = "<b>" + _t("Updating") + "</b>";
        _mgr.notifyProgress(this, status, d, d + bytesRemaining);
    }

    /** subclasses should override */
    public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
        if (_isPartial) {
            // Compare version with what we have now
            String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
            boolean newer = VersionComparator.comp(newVersion, _currentVersion) > 0;
            if (newer) {
                _newVersion = newVersion;
            } else {
                updateStatus("<b>" + _t("No new version found at {0}", linkify(url)) + "</b>");
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Found old version \"" + newVersion + "\" at " + url);
            }
            return;
        }

        // FIXME if we didn't do a partial, we don't know
        if (_newVersion == null)
            _newVersion = "unknown";
        File tmp = new File(_updateFile);
        if (_mgr.notifyComplete(this, _newVersion, tmp))
            this.done = true;
        else
            tmp.delete();  // corrupt
    }

    /** subclasses should override */
    public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
        // don't display bytesTransferred as it is meaningless
        if (_log.shouldLog(Log.WARN))
            _log.warn("Update from " + url + " did not download completely (" +
                           bytesRemaining + " remaining after " + currentAttempt + " tries)");
        updateStatus("<b>" + _t("Transfer failed from {0}", linkify(url)) + "</b>");
        _mgr.notifyAttemptFailed(this, url, null);
        // update() will call notifyTaskFailed() after last URL
    }

    public void headerReceived(String url, int attemptNum, String key, String val) {}

    public void attempting(String url) {}

    protected void updateStatus(String s) {
        _mgr.notifyProgress(this, s);
    }

    protected static String linkify(String url) {
        return ConsoleUpdateManager.linkify(url);
    }

    /** translate a string */
    protected String _t(String s) {
        return _mgr._t(s);
    }

    /**
     *  translate a string with a parameter
     */
    protected String _t(String s, Object o) {
        return _mgr._t(s, o);
    }

    @Override
    public String toString() {
        return getClass().getName() + ' ' + getType() + ' ' + getID() + ' ' + getMethod() + ' ' + getURI();
    }
}
