#!/usr/local/bin/python2.7
""" Command-line script to check the availability of a ports distfiles """

from __future__ import print_function  # Imports print_function from python 3

from multiprocessing import Pool
from functools import partial

import argparse
import os
import re
import shlex
import socket
import subprocess
import sys
import requests
import requests_ftp

if sys.version_info > (3, 0):
    from urllib.parse import urlparse
else:
    from urlparse import urlparse

__version__ = "1.2"


class distfiles(object):
    """ Class to interact with FreeBSD ports distfiles """
    def __init__(self, directory):
        self.author = "Emanuel Haupt <ehaupt@FreeBSD.org>"
        self.directory = directory

        if not os.path.isdir(self.directory):
            print(self.directory, "Directory does not exist.")
            sys.exit()

    def __get_dist_urls__(self):
        make_cmd = 'make -C %s fetch-urlall-list \
                DISTDIR=%s MASTER_SITE_BACKUP=""' % (self.directory, "/tmp")
        proc = subprocess.Popen(shlex.split(make_cmd), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        out, err = proc.communicate()

        if err:
            print(self.directory, "does not seem to be a valid port.")
            sys.exit(2)

        if out:
            urls = out.decode('utf-8').split()
            return urls

        return None

    def __get_www_urls__(self):
        make_cmd = 'make -C %s describe \
                DISTDIR=%s MASTER_SITE_BACKUP=""' %  (self.directory, "/tmp")
        proc = subprocess.Popen(shlex.split(make_cmd), stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE)
        out, err = proc.communicate()

        if err:
            print(self.directory, "does not seem to be a valid port.")
            sys.exit(2)

        if out:
            wwws = out.decode('utf-8').split('|')[-1].rstrip("\n")
            if not wwws == "":
                return [wwws]

        return None

    def get_all_urls(self):
        """ Public method that will search both WWW and DISTFILES URLs of a port """
        url = {}
        dist = self.__get_dist_urls__()
        www = self.__get_www_urls__()

        if not dist is None:
            url['dist_urls'] = dist

        if not www is None:
            url['www_urls'] = www

        return url


def hostname_resolves(hostname):
    """ Return true if a hostname resolves, return false if not """
    try:
        socket.gethostbyname(hostname)
        return True
    except socket.error:
        return False

def resolve_or_timeout_error(url, urltype, args):
    """ print whether there is a timeout or a host name resolution error """
    tmout_code = "500"
    nxdomain_code = '500'
    hostname = urlparse(url).hostname
    if hostname_resolves(hostname):
        print("%s [%s]\t%s (timeout: %s)" % (tmout_code, urltype, url, args.timeout))
    else:
        # host does not resolve
        print("%s [%s]\t%s (%s does not resolve)" % (nxdomain_code, urltype, url, hostname))


def validate_url_pool(args, iterable):
    """ method to validate a list of URLs """
    for url, urltype in iterable.items():
        if re.match(r'^http', url):
            try:
                user_agent = ("distilator/%s (%s)" % (__version__, os.uname()[0]))
                headers = {'User-Agent': user_agent}
                req = requests.head(url, timeout=args.timeout, headers=headers)

                if req.is_redirect:
                    print("%s [%s]\t%s -> %s"
                          % (req.status_code, urltype, url, req.headers['Location']))
                else:
                    if(req.status_code >= 200 and req.status_code < 300 and
                       args.silent):
                        continue

                    print("%s [%s]\t%s" % (req.status_code, urltype, url))

            except IOError:
                resolve_or_timeout_error(url, urltype, args)

        elif re.match(r'^ftp', url):
            try:
                requests_ftp.monkeypatch_session()
                session = requests.Session()
                req = session.head(url, timeout=args.timeout)
                status_code = req.status_code
                if req.status_code >= 200 and req.status_code < 300 and args.silent:
                    continue

                print("%s [%s]\t%s" % (status_code, urltype, url))

            except IOError:
                resolve_or_timeout_error(url, urltype, args)

        else:
            print("%s not supported" % url)


def main():
    """ main function for the tool """
    parser = argparse.ArgumentParser(
        description='Script to check the availability of a ports distfiles',
        epilog='For further testing, please consult \
            https://www.freebsd.org/doc/en_US.ISO8859-1/books/porters-handbook/'
    )

    parser.add_argument('directory', nargs='?', default=os.getcwd(),
                        help="ports directory")
    parser.add_argument('-j', '--jobs', type=int, default=30,
                        help='number of validation threads (default 30)')
    parser.add_argument('-s', '--silent', action='store_true', default=False,
                        help='silent mode, only print errors')
    parser.add_argument('-t', '--timeout', type=float, default=5,
                        help='link check timeout in seconds (default 5)')
    parser.add_argument('-v', '--version', action='store_true', help='print version')

    args = parser.parse_args()

    dist = distfiles(args.directory)

    if args.version:
        print("distilator version", __version__)
        sys.exit()

    url = dist.get_all_urls()

    iterable = []
    for urltype, urls in url.items():
        for url in urls:
            if urltype == "dist_urls":
                iterable.append({url : 'DISTFILE'})
            elif urltype == "www_urls":
                iterable.append({url : 'WWW'})
            else:
                iterable.append({url : 'UNKNOWN'})

    pool = Pool(processes=args.jobs)
    func = partial(validate_url_pool, args)
    pool.map(func, iterable)
    pool.close()
    pool.join()


# Main section of the script
if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:  # Catch Ctrl-C
        sys.exit(0)
    except SystemExit as sysexit:
        if sysexit.code != 0:
            raise
        else:
            sys.exit(0)
    except:
        raise
