var mout = require('mout');
var Q = require('q');
var ResolveCache = require('./ResolveCache');
var resolverFactory = require('./resolverFactory');
var createError = require('../util/createError');
var RegistryClient = require('bower-registry-client');

function PackageRepository(config, logger) {
    var registryOptions;

    this._config = config;
    this._logger = logger;

    // Instantiate the registry
    registryOptions = mout.object.deepMixIn({}, this._config);
    registryOptions.cache = this._config.storage.registry;

    this._registryClient = new RegistryClient(registryOptions, logger);

    // Instantiate the resolve cache
    this._resolveCache = new ResolveCache(this._config);
}

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

PackageRepository.prototype.fetch = function(decEndpoint) {
    var logger;
    var that = this;
    var isTargetable;
    var info = {
        decEndpoint: decEndpoint
    };

    // Create a new logger that pipes everything to ours that will be
    // used to fetch
    logger = this._logger.geminate();
    // Intercept all logs, adding additional information
    logger.intercept(function(log) {
        that._extendLog(log, info);
    });

    return (
        this._getResolver(decEndpoint, logger)
            // Decide if we retrieve from the cache or not
            // Also decide if we validate the cached entry or not
            .then(function(resolver) {
                info.resolver = resolver;
                isTargetable = resolver.constructor.isTargetable;

                if (!resolver.isCacheable()) {
                    return that._resolve(resolver, logger);
                }

                // If force flag is used, bypass cache, but write to cache anyway
                if (that._config.force) {
                    logger.action(
                        'resolve',
                        resolver.getSource() + '#' + resolver.getTarget()
                    );
                    return that._resolve(resolver, logger);
                }

                // Note that we use the resolver methods to query the
                // cache because transformations/normalisations can occur
                return (
                    that._resolveCache
                        .retrieve(resolver.getSource(), resolver.getTarget())
                        // Decide if we can use the one from the resolve cache
                        .spread(function(canonicalDir, pkgMeta) {
                            // If there's no package in the cache
                            if (!canonicalDir) {
                                // And the offline flag is passed, error out
                                if (that._config.offline) {
                                    throw createError(
                                        'No cached version for ' +
                                            resolver.getSource() +
                                            '#' +
                                            resolver.getTarget(),
                                        'ENOCACHE',
                                        {
                                            resolver: resolver
                                        }
                                    );
                                }

                                // Otherwise, we have to resolve it
                                logger.info(
                                    'not-cached',
                                    resolver.getSource() +
                                        (resolver.getTarget()
                                            ? '#' + resolver.getTarget()
                                            : '')
                                );
                                logger.action(
                                    'resolve',
                                    resolver.getSource() +
                                        '#' +
                                        resolver.getTarget()
                                );

                                return that._resolve(resolver, logger);
                            }

                            info.canonicalDir = canonicalDir;
                            info.pkgMeta = pkgMeta;

                            logger.info(
                                'cached',
                                resolver.getSource() +
                                    (pkgMeta._release
                                        ? '#' + pkgMeta._release
                                        : '')
                            );

                            // If offline flag is used, use directly the cached one
                            if (that._config.offline) {
                                return [canonicalDir, pkgMeta, isTargetable];
                            }

                            // Otherwise check for new contents
                            logger.action(
                                'validate',
                                (pkgMeta._release
                                    ? pkgMeta._release + ' against '
                                    : '') +
                                    resolver.getSource() +
                                    (resolver.getTarget()
                                        ? '#' + resolver.getTarget()
                                        : '')
                            );

                            return resolver
                                .hasNew(pkgMeta)
                                .then(function(hasNew) {
                                    // If there are no new contents, resolve to
                                    // the cached one
                                    if (!hasNew) {
                                        return [
                                            canonicalDir,
                                            pkgMeta,
                                            isTargetable
                                        ];
                                    }

                                    // Otherwise resolve to the newest one
                                    logger.info(
                                        'new',
                                        'version for ' +
                                            resolver.getSource() +
                                            '#' +
                                            resolver.getTarget()
                                    );
                                    logger.action(
                                        'resolve',
                                        resolver.getSource() +
                                            '#' +
                                            resolver.getTarget()
                                    );

                                    return that._resolve(resolver, logger);
                                });
                        })
                );
            })
            // If something went wrong, also extend the error
            .fail(function(err) {
                that._extendLog(err, info);
                throw err;
            })
    );
};

PackageRepository.prototype.versions = function(source) {
    // Resolve the source using the factory because the
    // source can actually be a registry name
    return this._getResolver({ source: source }).then(
        function(resolver) {
            // If offline, resolve using the cached versions
            if (this._config.offline) {
                return this._resolveCache.versions(resolver.getSource());
            }

            // Otherwise, fetch remotely
            return resolver.constructor.versions(resolver.getSource());
        }.bind(this)
    );
};

PackageRepository.prototype.eliminate = function(pkgMeta) {
    return Q.all([
        this._resolveCache.eliminate(pkgMeta),
        Q.nfcall(
            this._registryClient.clearCache.bind(this._registryClient),
            pkgMeta.name
        )
    ]);
};

PackageRepository.prototype.clear = function() {
    return Q.all([
        this._resolveCache.clear(),
        Q.nfcall(this._registryClient.clearCache.bind(this._registryClient))
    ]);
};

PackageRepository.prototype.reset = function() {
    this._resolveCache.reset();
    this._registryClient.resetCache();
};

PackageRepository.prototype.list = function() {
    return this._resolveCache.list();
};

PackageRepository.prototype.getRegistryClient = function() {
    return this._registryClient;
};

PackageRepository.prototype.getResolveCache = function() {
    return this._resolveCache;
};

PackageRepository.clearRuntimeCache = function() {
    ResolveCache.clearRuntimeCache();
    RegistryClient.clearRuntimeCache();
    resolverFactory.clearRuntimeCache();
};

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

PackageRepository.prototype._getResolver = function(decEndpoint, logger) {
    logger = logger || this._logger;

    // Get the appropriate resolver
    return resolverFactory(
        decEndpoint,
        { config: this._config, logger: logger },
        this._registryClient
    );
};

PackageRepository.prototype._resolve = function(resolver, logger) {
    var that = this;

    // Resolve the resolver
    return (
        resolver
            .resolve()
            // Store in the cache
            .then(function(canonicalDir) {
                if (!resolver.isCacheable()) {
                    return canonicalDir;
                }

                return that._resolveCache.store(
                    canonicalDir,
                    resolver.getPkgMeta()
                );
            })
            // Resolve promise with canonical dir and package meta
            .then(function(dir) {
                var pkgMeta = resolver.getPkgMeta();

                logger.info(
                    'resolved',
                    resolver.getSource() +
                        (pkgMeta._release ? '#' + pkgMeta._release : '')
                );
                return [dir, pkgMeta, resolver.constructor.isTargetable()];
            })
    );
};

PackageRepository.prototype._extendLog = function(log, info) {
    log.data = log.data || {};

    // Store endpoint info in each log
    if (info.decEndpoint) {
        log.data.endpoint = mout.object.pick(info.decEndpoint, [
            'name',
            'source',
            'target'
        ]);
    }

    // Store the resolver info in each log
    if (info.resolver) {
        log.data.resolver = {
            name: info.resolver.getName(),
            source: info.resolver.getSource(),
            target: info.resolver.getTarget()
        };
    }

    // Store the canonical dir and its meta in each log
    if (info.canonicalDir) {
        log.data.canonicalDir = info.canonicalDir;
        log.data.pkgMeta = info.pkgMeta;
    }

    return log;
};

module.exports = PackageRepository;
