var util = require('util');
var Q = require('q');
var which = require('../../util/which');
var LRU = require('lru-cache');
var mout = require('mout');
var Resolver = require('./Resolver');
var semver = require('../../util/semver');
var createError = require('../../util/createError');
var cmd = require('../../util/cmd');

var hasSvn;

// Check if svn is installed
try {
    which.sync('svn');
    hasSvn = true;
} catch (ex) {
    hasSvn = false;
}

function SvnResolver(decEndpoint, config, logger) {
    Resolver.call(this, decEndpoint, config, logger);

    if (!hasSvn) {
        throw createError('svn is not installed or not in the PATH', 'ENOSVN');
    }
}

util.inherits(SvnResolver, Resolver);
mout.object.mixIn(SvnResolver, Resolver);

// -----------------

SvnResolver.getSource = function(source) {
    var uri = this._source || source;

    return uri
        .replace(/^svn\+(https?|file):\/\//i, '$1://') // Change svn+http or svn+https or svn+file to http(s), file respectively
        .replace('svn://', 'http://') // Change svn to http
        .replace(/\/+$/, ''); // Remove trailing slashes
};

SvnResolver.prototype._hasNew = function(pkgMeta) {
    var oldResolution = pkgMeta._resolution || {};

    return this._findResolution().then(function(resolution) {
        // Check if resolution types are different
        if (oldResolution.type !== resolution.type) {
            return true;
        }

        // If resolved to a version, there is new content if the tags are not equal
        if (
            resolution.type === 'version' &&
            semver.neq(resolution.tag, oldResolution.tag)
        ) {
            return true;
        }

        // As last check, we compare both commit hashes
        return resolution.commit !== oldResolution.commit;
    });
};

SvnResolver.prototype._resolve = function() {
    var that = this;

    return this._findResolution().then(function() {
        return that._export();
    });
};

// -----------------

SvnResolver.prototype._export = function() {
    var promise;
    var timer;
    var reporter;
    var that = this;
    var resolution = this._resolution;

    this.source = SvnResolver.getSource(this._source);

    this._logger.action(
        'export',
        resolution.tag || resolution.branch || resolution.commit,
        {
            resolution: resolution,
            to: this._tempDir
        }
    );

    if (resolution.type === 'commit') {
        promise = cmd('svn', [
            'export',
            '--force',
            '--non-interactive',
            this._source + '/trunk',
            '-r' + resolution.commit,
            this._tempDir
        ]);
    } else if (resolution.type === 'branch' && resolution.branch === 'trunk') {
        promise = cmd('svn', [
            'export',
            '--force',
            '--non-interactive',
            this._source + '/trunk',
            this._tempDir
        ]);
    } else if (resolution.type === 'branch') {
        promise = cmd('svn', [
            'export',
            '--force',
            '--non-interactive',
            this._source + '/branches/' + resolution.branch,
            this._tempDir
        ]);
    } else {
        promise = cmd('svn', [
            'export',
            '--force',
            '--non-interactive',
            this._source + '/tags/' + resolution.tag,
            this._tempDir
        ]);
    }

    // Throttle the progress reporter to 1 time each sec
    reporter = mout.fn.throttle(function(data) {
        var lines;

        lines = data.split(/[\r\n]+/);
        lines.forEach(function(line) {
            if (/\d{1,3}\%/.test(line)) {
                // TODO: There are some strange chars that appear once in a while (\u001b[K)
                //       Trim also those?
                that._logger.info('progress', line.trim());
            }
        });
    }, 1000);

    // Start reporting progress after a few seconds
    timer = setTimeout(function() {
        promise.progress(reporter);
    }, 8000);

    return (
        promise
            // Add additional proxy information to the error if necessary
            .fail(function(err) {
                throw err;
            })
            // Clear timer at the end
            .fin(function() {
                clearTimeout(timer);
                reporter.cancel();
            })
    );
};

// -----------------

SvnResolver.prototype._findResolution = function(target) {
    var err;
    var self = this.constructor;
    var that = this;

    target = target || this._target || '*';

    this._source = SvnResolver.getSource(this._source);

    // Target is a revision, so it's a stale target (not a moving target)
    // There's nothing to do in this case
    if (/^r\d+/.test(target)) {
        target = target.split('r');

        this._resolution = { type: 'commit', commit: target[1] };
        return Q.resolve(this._resolution);
    }

    // Target is a range/version
    if (semver.validRange(target)) {
        return self.versions(this._source, true).then(function(versions) {
            var versionsArr, version, index;

            versionsArr = versions.map(function(obj) {
                return obj.version;
            });

            // If there are no tags and target is *,
            // fallback to the latest commit on trunk
            if (!versions.length && target === '*') {
                return that._findResolution('trunk');
            }

            versionsArr = versions.map(function(obj) {
                return obj.version;
            });
            // Find a satisfying version, enabling strict match so that pre-releases
            // have lower priority over normal ones when target is *
            index = semver.maxSatisfyingIndex(versionsArr, target, true);
            if (index !== -1) {
                version = versions[index];
                return (that._resolution = {
                    type: 'version',
                    tag: version.tag,
                    commit: version.commit
                });
            }

            // Check if there's an exact branch/tag with this name as last resort
            return Q.all([
                self.branches(that._source),
                self.tags(that._source)
            ]).spread(function(branches, tags) {
                // Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
                if (mout.object.hasOwn(tags, target)) {
                    return (that._resolution = {
                        type: 'tag',
                        tag: target,
                        commit: tags[target]
                    });
                }
                if (mout.object.hasOwn(branches, target)) {
                    return (that._resolution = {
                        type: 'branch',
                        branch: target,
                        commit: branches[target]
                    });
                }

                throw createError(
                    'No tag found that was able to satisfy ' + target,
                    'ENORESTARGET',
                    {
                        details: !versions.length
                            ? 'No versions found in ' + that._source
                            : 'Available versions in ' +
                              that._source +
                              ': ' +
                              versions
                                  .map(function(version) {
                                      return version.version;
                                  })
                                  .join(', ')
                    }
                );
            });
        });
    }

    // Otherwise, target is either a tag or a branch
    return Q.all([self.branches(that._source), self.tags(that._source)]).spread(
        function(branches, tags) {
            // Use hasOwn because a branch/tag could have a name like "hasOwnProperty"
            if (mout.object.hasOwn(tags, target)) {
                return (that._resolution = {
                    type: 'tag',
                    tag: target,
                    commit: tags[target]
                });
            }
            if (mout.object.hasOwn(branches, target)) {
                return (that._resolution = {
                    type: 'branch',
                    branch: target,
                    commit: branches[target]
                });
            }

            branches = Object.keys(branches);
            tags = Object.keys(tags);

            err = createError(
                'target ' + target + ' does not exist',
                'ENORESTARGET'
            );
            err.details = !tags.length
                ? 'No tags found in ' + that._source
                : 'Available tags: ' + tags.join(', ');
            err.details += '\n';
            err.details += !branches.length
                ? 'No branches found in ' + that._source
                : 'Available branches: ' + branches.join(', ');

            throw err;
        }
    );
};

SvnResolver.prototype._savePkgMeta = function(meta) {
    var version;

    if (this._resolution.type === 'version') {
        version = semver.clean(this._resolution.tag);

        // Warn if the package meta version is different than the resolved one
        if (
            typeof meta.version === 'string' &&
            semver.neq(meta.version, version)
        ) {
            this._logger.warn(
                'mismatch',
                'Version declared in the json (' +
                    meta.version +
                    ') is different than the resolved one (' +
                    version +
                    ')',
                {
                    resolution: this._resolution,
                    pkgMeta: meta
                }
            );
        }

        // Ensure package meta version is the same as the resolution
        meta.version = version;
    } else {
        // If resolved to a target that is not a version,
        // remove the version from the meta
        delete meta.version;
    }

    // Save version/tag/commit in the release
    // Note that we can't store branches because _release is supposed to be
    // an unique id of this ref.
    meta._release = version || this._resolution.tag || this._resolution.commit;

    // Save resolution to be used in hasNew later
    meta._resolution = this._resolution;

    return Resolver.prototype._savePkgMeta.call(this, meta);
};

// ------------------------------

SvnResolver.versions = function(source, extra) {
    source = SvnResolver.getSource(source);

    var value = this._cache.versions.get(source);

    if (value) {
        return Q.resolve(value).then(
            function() {
                var versions = this._cache.versions.get(source);

                // If no extra information was requested,
                // resolve simply with the versions
                if (!extra) {
                    versions = versions.map(function(version) {
                        return version.version;
                    });
                }

                return versions;
            }.bind(this)
        );
    }

    value = this.tags(source).then(
        function(tags) {
            var tag;
            var version;
            var versions = [];

            // For each tag
            for (tag in tags) {
                version = semver.clean(tag);
                if (version) {
                    versions.push({
                        version: version,
                        tag: tag,
                        commit: tags[tag]
                    });
                }
            }

            // Sort them by DESC order
            versions.sort(function(a, b) {
                return semver.rcompare(a.version, b.version);
            });

            this._cache.versions.set(source, versions);

            // Call the function again to keep it DRY
            return this.versions(source, extra);
        }.bind(this)
    );

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.versions.set(source, value);

    return value;
};

SvnResolver.tags = function(source) {
    source = SvnResolver.getSource(source);

    var value = this._cache.tags.get(source);

    if (value) {
        return Q.resolve(value);
    }

    value = cmd('svn', [
        'list',
        source + '/tags',
        '--verbose',
        '--non-interactive'
    ]).spread(
        function(stout) {
            var tags = SvnResolver.parseSubversionListOutput(stout.toString());

            this._cache.tags.set(source, tags);
            return tags;
        }.bind(this)
    );

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.tags.set(source, value);

    return value;
};

SvnResolver.branches = function(source) {
    source = SvnResolver.getSource(source);

    var value = this._cache.branches.get(source);

    if (value) {
        return Q.resolve(value);
    }

    value = cmd('svn', [
        'list',
        source + '/branches',
        '--verbose',
        '--non-interactive'
    ]).spread(
        function(stout) {
            var branches = SvnResolver.parseSubversionListOutput(
                stout.toString()
            );

            // trunk is a branch!
            branches.trunk = '*';

            this._cache.branches.set(source, branches);
            return branches;
        }.bind(this)
    );

    // Store the promise to be reused until it resolves
    // to a specific value
    this._cache.branches.set(source, value);

    return value;
};

SvnResolver.parseSubversionListOutput = function(stout) {
    var entries = {};
    var lines = stout.trim().split(/[\r\n]+/);

    // For each line in the refs, match only the branches
    lines.forEach(function(line) {
        var match = line.match(/\s+([0-9]+)\s.+\s([\w.$-]+)\//i);

        if (match && match[2] !== '.') {
            entries[match[2]] = match[1];
        }
    });

    return entries;
};

SvnResolver.clearRuntimeCache = function() {
    // Reset cache for branches, tags, etc
    mout.object.forOwn(SvnResolver._cache, function(lru) {
        lru.reset();
    });
};

SvnResolver._cache = {
    branches: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
    tags: new LRU({ max: 50, maxAge: 5 * 60 * 1000 }),
    versions: new LRU({ max: 50, maxAge: 5 * 60 * 1000 })
};

module.exports = SvnResolver;
