#!/usr/bin/env node

'use strict';

const chalk = require('chalk');
const fs = require('fs');
const markdownLinkCheck = require('markdown-link-check');
const program = require('commander');
const request = require('request');
const url = require('url');
const path = require('path');

const statusLabels = {
    alive: chalk.green('✓'),
    dead: chalk.red('✖'),
    ignored: chalk.gray('/'),
    error: chalk.yellow('⚠'),
};

const opts = {};
let filenameForOutput = '';
let stream = process.stdin; // read from stdin unless a filename is given

program
    .option('-p, --progress', 'show progress bar')
    .option('-c, --config [config]', 'apply a config file (JSON), holding e.g. url specific header configuration')
    .option('-q, --quiet', 'displays errors only')
    .option('-v, --verbose', 'displays detailed error information')
    .arguments('[filenameOrUrl]')
    .action(function (filenameOrUrl) {
    filenameForOutput = filenameOrUrl;
    if (/https?:/.test(filenameOrUrl)) {
        request(filenameOrUrl, function (error, response, body) {
            if (error) {
                console.error(chalk.red('\nERROR: Unable to connect! Please provide a valid URL as an argument.'));
                process.exit(1);
            }
            else if (response.statusCode === 404){
                console.error(chalk.red('\nERROR: 404 - File not found! Please provide a valid URL as an argument.'));
                process.exit(1);
            } else {
                stream = request.get(filenameOrUrl);
            }

        });
        try { // extract baseUrl from supplied URL
            const parsed = url.parse(filenameOrUrl);
            delete parsed.search;
            delete parsed.hash;
            if (parsed.pathname.lastIndexOf('/') !== -1) {
                parsed.pathname = parsed.pathname.substr(0, parsed.pathname.lastIndexOf('/') + 1);
            }
            opts.baseUrl = url.format(parsed);
        } catch (err) { /* ignore error */
            }
    } else {
        fs.stat(filenameOrUrl, function(error , stats){
            if (!error && stats.isDirectory()){
                console.error(chalk.red('\nERROR: ' + filenameOrUrl + ' is a directory! Please provide a vaild filename as an argument.'));
                process.exit(1);
            }
        });
        opts.baseUrl = 'file://' + path.dirname(path.resolve(filenameOrUrl));
        stream = fs.createReadStream(filenameOrUrl);
    }

}).parse(process.argv);

opts.showProgressBar = (program.progress === true); // force true or undefined to be true or false.
opts.quiet = (program.quiet === true);
opts.verbose = (program.verbose === true);

let markdown = ''; // collect the markdown data, then process it

stream
  .on('data', function (chunk) {
    markdown += chunk.toString();
  })
  .on('error', function(error) {
    if (error.code === 'ENOENT') {
      console.error(chalk.red('\nERROR: File not found! Please provide a vaild filename as an argument.'));
    } else {
      console.error(chalk.red(error));
    }
    return process.exit(1);
  })
  .on('end', function () {
    if (filenameForOutput) {
        console.log(chalk.cyan('\nFILE: ' + filenameForOutput));
    }

    if (program.config) {
        fs.access(program.config, (fs.constants || fs).R_OK, function (err) {
            if (!err) {
                let configStream = fs.createReadStream(program.config);
                let configData = '';

                configStream.on('data', function (chunk) {
                    configData += chunk.toString();
                }).on('end', function () {
                    let config = JSON.parse(configData);

                    opts.ignorePatterns = config.ignorePatterns;
                    opts.replacementPatterns = config.replacementPatterns;
                    opts.httpHeaders = config.httpHeaders;
                    opts.ignoreDisable = config.ignoreDisable;

                    runMarkdownLinkCheck(markdown, opts);
                });
            }
            else {
                console.error(chalk.red('\nERROR: Config file not accessible.'));
                process.exit(1);
            }
        });
    }
    else {
        runMarkdownLinkCheck(markdown, opts);
    }
});

function runMarkdownLinkCheck(markdown, opts) {
    markdownLinkCheck(markdown, opts, function (err, results) {
        if (err) {
            console.error(chalk.red('\nERROR: something went wrong!'));
            console.error(err.stack);
            process.exit(1);
        }

        if (results.length === 0 && !opts.quiet) {
            console.log(chalk.yellow('No hyperlinks found!'));
        }
        results.forEach(function (result) {
            // workaround to ignore 429 responses (too many requests)
            if (result.statusCode === 429) {
                result.status = 'ignored'
            }

            // Skip messages for non-deadlinks in quiet mode.
            if (opts.quiet && result.status !== 'dead') {
                return;
            }

            if (opts.verbose) {
                if (result.err) {
                    console.log('[%s] %s → Status: %s %s', statusLabels[result.status], result.link, result.statusCode, result.err);
                } else {
                    console.log('[%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
                }
            }
            else {
                console.log('[%s] %s', statusLabels[result.status], result.link);
            }
        });
        console.log('\n%s links checked.', results.length);
        if (results.some((result) => result.status === 'dead')) {
            let deadLinks = results.filter(result => { return result.status === 'dead'; });
            console.error(chalk.red('\nERROR: %s dead links found!'), deadLinks.length);
            deadLinks.forEach(function (result) {
                console.log('[%s] %s → Status: %s', statusLabels[result.status], result.link, result.statusCode);
            });
            process.exit(1);
        }
    });
}
