#!/usr/bin/env python
# encoding: utf-8

from __future__ import print_function, unicode_literals

import argparse
import os
import re
import shutil
import signal
import subprocess
import sys
import tempfile


def apache_2_point_4_or_higher(apache_version):
    """Return boolean of whether given version is 2.4 and higher."""
    return (apache_version[0] > 2) or (apache_version[0] == 2 and apache_version[1] >= 4)


def cleanup(signal, frame):
    """Delete the temp_dir directory, if we know about it and it exists."""

    global temp_dir

    if temp_dir:
        try:
            shutil.rmtree(temp_dir)
        except:
            pass


def construct_httpd_command(temp_dir, port):
    """Return a list of strings suitable for
    passing into subprocess.Popen()."""

    elements = ['httpd', '-k', 'start']

    if port_requires_sudo(port):
        elements = ['sudo'] + elements

    elements.extend(['-f', os.path.join(temp_dir, 'micropache-httpd.conf')])
    elements.extend(['-c', 'Listen {}'.format(port)])
    elements.extend(['-e', 'info'])
    elements.extend(['-D', 'FOREGROUND'])

    return elements


def create_httpd_conf(temp_dir, default_config, apache_user,
                      apache_group, apache_version):
    """Use the given variables to write an
    micropache-httpd.conf file to temp_dir."""

    config = """
    {general}

    DocumentRoot "{cwd}"
    ServerName localhost
    AccessFileName .htaccess
    ErrorLog /dev/stdout
    CustomLog /dev/stdout '%h %l %u %t %r %>s %b'
    PidFile "{pidfile}"

    <FilesMatch \.php$>
        SetHandler application/x-httpd-php
    </FilesMatch>

    <IfModule unixd_module>
    User {user}
    Group {group}
    </IfModule>

    <Directory "{cwd}">
    Options FollowSymLinks Indexes
    AllowOverride All
    {permissions}
    </Directory>

    <IfModule dir_module>
    DirectoryIndex index.html index.php
    </IfModule>
    """.format(
        general='\n'.join(default_config['general']),
        user=apache_user or default_config['user'],
        group=apache_group or default_config['group'],
        cwd=os.getcwd(),
        permissions=get_apache_permissions_for_version(apache_version),
        mutex=get_apache_mutex_for_version(apache_version, temp_dir),
        pidfile=os.path.join(temp_dir, 'httpd.pid')
    )

    with open(os.path.join(temp_dir, 'micropache-httpd.conf'), 'w') as temp:
        temp.write(config)

    return config


def extract_apache_version(httpd_settings):
    """Return apache version as a list,
    given the output of `httpd -V`."""

    apache_version_string = re.search(
        '^Server version[^0-9]+([0-9.]+)',
        httpd_settings
    ).group(1)
    apache_version_list = [int(n) for n in apache_version_string.split('.')]
    return apache_version_list


def extract_conf_path(httpd_settings):
    """Return absolute path to httpd.conf,
    given the output of `httpd -V`."""

    return re.search(
        'SERVER_CONFIG_FILE.+"([^"]+)"',
        httpd_settings
    ).group(1)


def extract_default_config(httpd_settings):
    """Extract a tuple of interesting things from httpd.conf,
    given the output of `httpd -V`."""

    defaults = {
        'user': None,
        'group': None,
        'general': []
    }

    conf_path = extract_conf_path(httpd_settings)

    with open(conf_path) as conf_file:
        ignore_lines_until = None

        for line in conf_file:

            # Strip spaces from start and \n newline from end.
            line = line.strip()

            # Skip empty lines and comment lines
            if re.match('^\s*(#|$)', line):
                continue

            # If we are ignoring a block, skip the line.
            if ignore_lines_until:
                if line.startswith(ignore_lines_until):
                    ignore_lines_until = None
                continue

            # Skip certain blocks with start and end tags.
            if line.startswith('<Directory'):
                ignore_lines_until = '</Directory'
                continue
            elif line.startswith('<File'):
                ignore_lines_until = '</File'
                continue

            if line.startswith('User'):
                defaults['user'] = re.search('User\s+(.+)', line).group(1)
            elif line.startswith('Group'):
                defaults['group'] = re.search('Group\s+(.+)', line).group(1)

            if line.startswith('ServerRoot'):
                defaults['general'].append(line)
            elif line.startswith('LoadModule'):
                defaults['general'].append(line)
            elif line.startswith('<IfModule') or line.startswith('</IfModule'):
                defaults['general'].append(line)
            elif line.startswith('Include'):
                defaults['general'].append(line)

    return defaults


def get_apache_group(runtime_args):
    """Return unix group ID the end-user requested
    via the given command-line arguments."""

    if runtime_args.as_me:
        return get_output('id', '-g', '-n')[0]
    elif runtime_args.gid:
        return get_output('id', '-g', '-n', runtime_args.gid)[0]
    elif runtime_args.group:
        return runtime_args.group
    else:
        return None


def get_apache_mutex_for_version(apache_version, temp_dir):
    """Return the correct Mutex/LockFile line
    for the given version of Apache."""

    if apache_2_point_4_or_higher(apache_version):
        return 'Mutex file:{} default'.format(temp_dir)
    else:
        return 'LockFile {}/accept.lock'.format(temp_dir)


def get_apache_permissions_for_version(apache_version):
    """Return the correct permissions line
    for the given version of Apache."""

    if apache_2_point_4_or_higher(apache_version):
        return 'Require all granted'
    else:
        return 'Order deny,allow\nAllow from all'


def get_apache_user(runtime_args):
    """Return unix user ID the end-user requested via
    the given command-line arguments."""

    if runtime_args.as_me:
        return get_output('id', '-u', '-n')[0]
    elif runtime_args.uid:
        return get_output('id', '-u', '-n', runtime_args.uid)[0]
    elif runtime_args.user:
        return runtime_args.user
    else:
        return None


def get_arguments():
    """Parse command-line arguments."""

    examples = [
        'micropache',
        'micropache --port 80',
        'micropache --port 4000',
        'micropache --user www-data --group www-data',
        'micropache --uid 520 --gid 20',
        'micropache --as-me'
    ]

    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description='Run an Apache server in the current directory.',
        epilog='examples:\n  {}'.format("\n  ".join(examples))
    )
    parser.add_argument(
        '--port',
        type=int,
        help='Run the Apache server on the given port. Requesting a port below 1024 will run the httpd process under sudo, requiring an administrator password. If you do not specify a port, a free one will be chosen automatically, starting at 8000.'
    )
    parser.add_argument(
        '--user',
        type=str,
        help='Run httpd as the given filesystem user.'
    )
    parser.add_argument(
        '--group',
        type=str,
        help='Run httpd as the given filesystem group.'
    )
    parser.add_argument(
        '--uid',
        type=int,
        help='Like --user, except you provide a UID rather than a name.'
    )
    parser.add_argument(
        '--gid',
        type=int,
        help='Like --group, except you provide a GID rather than a name.'
    )
    parser.add_argument(
        '--as-me',
        action='store_true',
        help='Run httpd as your current user, with your username and group.'
    )

    return parser.parse_args()


def get_httpd_settings():
    """Return diagnostic information about
    whatever httpd command is on the PATH."""

    return get_output("httpd", "-V")[0]


def get_output(*args):
    """Run a given shell command, returning a tuple of
    (stdout, stderr) once it has completed."""

    return subprocess.Popen(
        map(str, list(args)),
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    ).communicate()


def get_port(runtime_args):
    """Return a port number as an integer - either the one requested by
    the end-user, or the first available port in the 8000-9000 range."""

    if runtime_args.port:
        if port_is_available(runtime_args.port):
            return runtime_args.port
        else:
            print('Port {} is already in use'.format(runtime_args.port))
            return sys.exit(3)

    else:
        for i in range(8000, 9000):
            if port_is_available(i):
                return i

        print('No free local ports available')
        return sys.exit(2)


def get_temp_dir():
    """Return file descriptor for a temporary directory."""

    return tempfile.mkdtemp(prefix='micropache_')


def port_is_available(port):
    """Return True if netcat can find no daemons
    listening on the given TCP port."""

    return not produces_output('nc', '-z', 'localhost', port)


def port_requires_sudo(port):
    """Return True if given number is under 1024."""

    return port < 1024


def produces_output(*args):
    """Return True if given shell command
    outputs anything to stdout or stderr."""

    return any(get_output(*args))


for sig in (signal.SIGABRT, signal.SIGINT, signal.SIGTERM):
    signal.signal(sig, cleanup)

runtime_args = get_arguments()

port = get_port(runtime_args)
apache_user = get_apache_user(runtime_args)
apache_group = get_apache_group(runtime_args)

temp_dir = get_temp_dir()

httpd_settings = get_httpd_settings()
apache_version = extract_apache_version(httpd_settings)
default_config = extract_default_config(httpd_settings)

create_httpd_conf(
    temp_dir,
    default_config,
    apache_user,
    apache_group,
    apache_version
)

httpd_command = construct_httpd_command(temp_dir, port)

print('Listening on port {}'.format(port))

process = subprocess.Popen(
    map(str, httpd_command),
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    stdin=subprocess.PIPE
)

while True:
    stdout_line = process.stdout.readline()
    stderr_line = process.stderr.readline()

    if stdout_line != b'':
        os.write(1, stdout_line)

    if stderr_line != b'':
        # print(' '.join(httpd_command), file=sys.stderr)
        os.write(2, stderr_line)

    if stdout_line == b'' and stderr_line == b'':
        break
