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

from multiprocessing import Pool
from functools import partial

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

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

__version__ = "1.4"


class Distfiles:
    """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_valid_urls(self, urls: list):
        valid_urls = []
        for url in urls:
            thisurl = urlparse(url)
            if validators.url(url):
                valid_urls.append(thisurl.geturl())
        return valid_urls

    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:
            return self._get_valid_urls(out.decode("utf-8").split())

        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:
            return self._get_valid_urls(
                [out.decode("utf-8").split("|")[-1].rstrip("\n")]
            )

        return None

    def get_all_urls(self):
        """Method that will search both WWW and DISTFILES URLs of a port."""
        url = {}
        dist = self._get_dist_urls()
        www = self._get_www_urls()

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

        if www is not 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):
        # timeout
        print("%s [%s]\t%s (timeout: %ss)" % (tmout_code, urltype, url, args.timeout))
        return tmout_code
    else:
        # host does not resolve
        print(
            "%s [%s]\t%s (%s does not resolve)"
            % (nxdomain_code, urltype, url, hostname)
        )
        return nxdomain_code


def validate_url_pool(args, iterable):
    """Method to validate a list of URLs."""
    for url, urltype in iterable.items():
        if urlparse(url).scheme == "http" or "https":
            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"])
                    )
                    return {
                        "status_code": req.status_code,
                        "urltype": urltype,
                        "url": url,
                    }
                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))
                    return {
                        "status_code": req.status_code,
                        "urltype": urltype,
                        "url": url,
                    }

            except IOError:
                status_code = resolve_or_timeout_error(url, urltype, args)
                return {"status_code": status_code, "urltype": urltype, "url": url}

        if urlparse(url).scheme == "ftp":
            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))
                return {"status_code": status_code, "urltype": urltype, "url": url}

            except IOError:
                status_code = resolve_or_timeout_error(url, urltype, args)
                return {"status_code": status_code, "urltype": urltype, "url": url}

        else:
            print("%s not supported" % url)
            return {"status_code": 500, "urltype": urltype, "url": 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)
    result = 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 Exception:
        raise
