"""
$URL: svn+ssh://sting.mems-exchange.org/repos/trunk/DurusWorks/qp/hub/web.py $
$Id: web.py 33633 2011-03-16 10:29:44Z dbinger $

Provides a HubServer-based HTTP/SCGI server.
"""
from durus.utils import as_bytes, join_bytes
from qp.hub.dispatcher import HubServer, worker
from qp.lib.site import Site
from qp.pub.common import get_publisher, clear_publisher
from qp.fill.html import url_unquote
from signal import signal, SIGHUP
import os
import qp
import socket
import sys

if sys.version < "3":
    from BaseHTTPServer import BaseHTTPRequestHandler
else:
    from http.server import BaseHTTPRequestHandler

def shutdown_connection(connection):
    try:
        connection.shutdown(socket.SHUT_RDWR)
    except socket.error:
        pass

class HTTPRequestHandler(BaseHTTPRequestHandler):

    server_version = "qp/1.0"

    def __init__(self, connection, address, info, **kwargs):
        self.required_cgi_environment = dict(
            SERVER_SOFTWARE=self.server_version,
            GATEWAY_INTERFACE='CGI/1.1')
        self.required_cgi_environment.update(kwargs)
        BaseHTTPRequestHandler.__init__(
            self, connection, address, info)

    def get_cgi_env(self, method):
        env = dict(
            SERVER_NAME=self.server.server_name,
            SERVER_PROTOCOL=self.protocol_version,
            SERVER_PORT=str(self.server.server_port),
            REQUEST_METHOD=method,
            REMOTE_ADDR=self.client_address[0],
            REMOTE_PORT=str(self.client_address[1]))
        if '?' in self.path:
            env['PATH_INFO'], env['QUERY_STRING'] = self.path.split('?', 1)
        else:
            env['PATH_INFO'] = self.path
        env['PATH_INFO'] = url_unquote(env['PATH_INFO'])
        script_name = self.required_cgi_environment.get('SCRIPT_NAME', '')
        if script_name:
            path_info = env['PATH_INFO']
            if path_info.startswith(script_name): # and it better!
                env['PATH_INFO'] = path_info[len(script_name):]
            else:
                print("\nAll paths are expected to start with the script_name.")
                print("  The script_name is %r." % script_name)
                print("  The requested path is %r.\n" % self.path)
        env['CONTENT_TYPE'] = self.headers.get(
            "content-type") or "plain/text"
        env['CONTENT_LENGTH'] = self.headers.get(
            'content-length') or "0"
        for name, value in self.headers.items():
            header_name = 'HTTP_' + name.upper().replace('-', '_')
            env[header_name] = value
        get_headers = (getattr(self.headers, 'get_all', None) or # py3k
            getattr(self.headers, 'getheaders'))
        accept = []
        for line in get_headers('accept'):
            for part in line.split(','):
                part = part.strip()
                if part:
                    accept.append(part)
        env['HTTP_ACCEPT'] = ','.join(accept)
        co = list(filter(None, get_headers('cookie') or []))
        if co:
            env['HTTP_COOKIE'] = ', '.join(co)
        env.update(self.required_cgi_environment)
        return env

    def process(self, env):
        hit = get_publisher().process(self.rfile, env)
        response = hit.get_response()
        try:
            self.send_response(*response.get_status())
            response.write(self.wfile, include_status=False,
                           include_body=(self.command!='HEAD'))
            self.wfile.flush()
        except (IOError, socket.error):
            err = sys.exc_info()[1]
            print("Error while sending response ignored: %s" % err)

    def handle_one_request(self):
        self.raw_requestline = self.rfile.readline()
        if not self.raw_requestline:
            self.close_connection = 1
            return
        if not self.parse_request():
            return
        return self.process(self.get_cgi_env(self.command))

    def finish(self):
        shutdown_connection(self.connection)

    def log_request(self, *args, **kwargs):
        pass

    def send_response(self, code, message=None):
        """
        Copied, with regret, from BaseHTTPRequestHandler, except that the line
        that adds the 'Date' header is removed.
        """
        self.log_request(code)
        if message is None:
            if code in self.responses:
                message = self.responses[code][0]
            else:
                message = ''
        if self.request_version != 'HTTP/0.9':
            self.wfile.write(as_bytes("%s %d %s\r\n" %
                             (self.protocol_version, code, message)))
        self.send_header('Server', self.version_string())


COLON = as_bytes(":")

# netstring utility functions
def ns_read_size(input):
    """
    The input is a binary-mode stream.
    If any bytes are available, the result is an int.
    If no bytes are available, the result is None.
    """
    size = []
    while 1:
        c = input.read(1)
        if c == COLON:
            break
        elif not c:
            if not size:
                return None
            raise IOError('short netstring read')
        size.append(c)
    return int(join_bytes(size))

COMMA = as_bytes(",")

def ns_reads(input):
    """
    The input is a binary-mode stream.
    If any bytes are available, the result is a byte_string.
    If no bytes are available, the result is None.
    """
    size = ns_read_size(input)
    if size is None:
        return None
    data = []
    while size > 0:
        s = input.read(size)
        if not s:
            raise IOError('short netstring read')
        data.append(s)
        size -= len(s)
    if input.read(1) != COMMA:
        raise IOError('missing netstring terminator')
    return join_bytes(data)

def read_env(input):
    """
    The input is a binary-mode stream.
    The result is a dict whose keys and values are str instances.
    """
    headers = ns_reads(input)
    if headers is None:
        return None
    if not isinstance(headers, str):
        headers = headers.decode('iso-8859-1')
    items = headers.split("\0")
    items = items[:-1]
    assert len(items) % 2 == 0, items
    env = {}
    for i in range(0, len(items), 2):
        env[items[i]] = items[i+1]
    return env

SCRIPT_NAME = "SCRIPT_NAME"
PATH_INFO = "PATH_INFO"

class SCGIHandler(object):

    def __init__(self, script_name=''):
        self.script_name = script_name

    def handle_connection(self, conn):
        input = conn.makefile("rb")
        output = conn.makefile("wb")
        env = read_env(input)
        if env is None:
            # Nothing here.
            return
        if (env.get(SCRIPT_NAME) and
            env.get(SCRIPT_NAME).startswith(self.script_name) and
            env.get(PATH_INFO) is None):
            # This looks like it is coming through mod_scgi and
            # needs repair.
            env[PATH_INFO] = env[SCRIPT_NAME][len(self.script_name):]
            env[SCRIPT_NAME] = self.script_name
        hit = get_publisher().process(input, env)
        response = hit.get_response()
        try:
            response.write(output)
            input.close()
            output.close()
            conn.close()
        except IOError:
            err = sys.exc_info()[1]
            print("IOError while sending response: %s" % err)
        shutdown_connection(conn)


def run_web(site):
    logfile = open(site.get_logfile(), 'a')
    sys.stderr = sys.stdout = logfile
    site.start_durus_logging()
    site.get_publisher() # Make sure that the Publisher constructor works.
    clear_publisher()
    server = HubServer(site.get_name(),
                       max_children=site.get_max_children(),
                       busy_limit=site.get_busy_limit(),
                       banned=site.get_banned(),
                       dispatcher_command_prefix=site.get_dispatcher_command_prefix())
    server.listen(
        site.get_scgi_address(),
        site.get_http_address(),
        site.get_as_https_address(),
        *(site.get_other_addresses() or []))
    site.ensure_uid_gid_not_root()
    def reexec(*args):
        server.do_stop()
        source = __file__
        if source.endswith('.pyc') or source.endswith('.pyo'):
            source = source[:-1]
        argv = [sys.executable, source, site.get_name()]
        print("RESTART:", " ".join(argv))
        os.execv(sys.executable, argv)
    signal(SIGHUP, reexec)
    server.run(handle_connection=get_connection_handler(site.get_name()))

def get_connection_handler(site_name):
    site = Site(site_name)
    scgi_address = site.get_scgi_address()
    http_address = site.get_http_address()
    as_https_address = site.get_as_https_address()
    https_address = site.get_https_address()
    scgi_port = scgi_address and scgi_address[1]
    http_port = http_address and http_address[1]
    as_https_port = as_https_address and as_https_address[1]
    script_name = site.get_script_name()
    class ConnectionHandler(object):
        def handle_connection(self, connection):
            port = connection.getsockname()[1]
            if port == scgi_port:
                return SCGIHandler(
                    script_name=script_name).handle_connection(connection)
            elif port == http_port:
                self.server_name, self.server_port = http_address
                client_address = connection.getpeername()
                HTTPRequestHandler(connection, client_address, self,
                                   SCRIPT_NAME=script_name)
            elif port == as_https_port:
                self.server_name, self.server_port = https_address
                client_address = connection.getpeername()
                HTTPRequestHandler(
                    connection, client_address, self, HTTPS='on',
                    SCRIPT_NAME=script_name)
            else:
                site.get_publisher().handle_connection(connection)
    site.get_create_publisher()(hub=True)
    return ConnectionHandler().handle_connection

def run_worker(site_name, parent_fd):
    worker(get_connection_handler(site_name), parent_fd)


if __name__ == '__main__':
    if len(sys.argv) == 2:
        run_web(Site(sys.argv[-1]))
    else:
        run_worker(*sys.argv[-2:])

