#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2005 Patrik Simons, Neotide Ab
# License: MIT
"""
A Quixote handler for Python ISAPI (in pywin32).
Needs a threadsafe publisher.

The Quixote demo:
# Use as a script to install. Pass remove as a command line
# argument to uninstall. Keep this script where you want the DLL to be stored.
# Remember that the "Launch IIS Process Account" (IWAM_something) account needs
# access rights to the DLL.
from isapi_handler import setup, QuixoteExtension

def create_publisher():
    from quixote.publish import Publisher
    from quixote.demo.mini_demo import RootDirectory
    return Publisher(RootDirectory(), display_exceptions='plain')

name = 'quixote'

# The entry point for the ISAPI extension.
def __ExtensionFactory__():
    return QuixoteExtension(name)

setup(name, 'Quixote demo', create_publisher)
"""
import platform
import sys
import traceback
import urllib.parse
from io import BytesIO

import win32security
from isapi import isapicon, ExtensionError
from isapi.threaded_extension import ThreadPoolExtension
from quixote.publish import get_publisher
from quixote.http_request import HTTPRequest


class EcbDict(dict):
    def __init__(self, ecb):
        self.ecb = ecb

    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            item = self.ecb.GetServerVariable(key, None)
            if item is None:
                raise
            if not isinstance(item, str):
                item = item.decode('latin-1')
            self[key] = item
            return item

    def __contains__(self, key):
        if dict.__contains__(self, key):
            return True
        item = self.ecb.GetServerVariable(key, None)
        if item is None:
            return False
        if not isinstance(item, str):
            item = item.decode('latin-1')
        self[key] = item
        return True

    def has_key(self, key):
        return self.__contains__(key)

    def get(self, key, default=None):
        try:
            return self[key]
        except KeyError:
            return default


class QuixoteExtension(ThreadPoolExtension):
    """Quixote PyISAPI extension"""
    def __init__(self, script_name):
        self.script_name = script_name
        ThreadPoolExtension.__init__(self)

    def dispatch(self, ecb):
        win32security.RevertToSelf()  # Terminate the impersonation so we run as the application pool user
        env = EcbDict(ecb)
        env['SCRIPT_NAME'] = '/' + self.script_name  # It's wrong in the ecb
        env['REQUEST_URI'] = env.get('URL', '')  # Apache style
        # Cache these, so we get them if env is copied.
        env.get('HTTP_HOST')  # Used by HttpRequest.get_server().
        env.get('REMOTE_ADDR')  # Used by session.user.log_user().
        # Fix PATH_INFO, IIS first decodes it and then encodes it into latin-1 or something.
        url = env.get('UNENCODED_URL')
        if url:
            q = url.find('?')
            if q > -1:
                url = url[:q]
            env['PATH_INFO'] = urllib.parse.unquote(url)
        if env.get('SERVER_PROTOCOL') == 'HTTP/1.0':
            keep_alive = False  # HTTP/1.0 doesn't understand transfer-codings
        else:
            keep_alive = env.get('HTTP_CONNECTION', '').lower() != 'close'
        try:
            publisher = get_publisher()
            if ecb.AvailableBytes > 0:
                data = ecb.AvailableData
                if ecb.TotalBytes > ecb.AvailableBytes:
                    data += ecb.ReadClient(ecb.TotalBytes - ecb.AvailableBytes)
            else:
                data = b''
            request = HTTPRequest(BytesIO(data), env)
            response = publisher.process_request(request)
            length = response.get_content_length()
            headers = ['%s: %s' % (n, v) for (n, v) in response.generate_headers()]
            chunked = (response.headers.get('transfer-encoding') == 'chunked')
            if keep_alive and (not length or length <= 1):  # Greater than one because IIS treats space as nothing
                headers = [h for h in headers if not h.startswith('Content-Length')]
                if not chunked:
                    headers.append('Transfer-Encoding: chunked')
            headers.append('\r\n')
            status = '%s' % response.status_code
            # If keep-alive is enabled, IIS stops responding if many requests from the same session
            # comes in quick succession. The problem is that for empty responses IIS does not add a
            # Content-Length header, so the browser doesn't know when the request ends. Therefore,
            # it doesn't reuse the connection before a timeout expires and this is experienced as a
            # hang. We can't add the header ourselves as IIS strips it out. The solution is to use
            # the header "Transfer-Encoding: chunked" and send the response body in length prefixed
            # chunks.
            ecb.SendResponseHeaders(status, '\r\n'.join(headers).encode('latin-1', 'strict'), keep_alive)
            if not chunked and keep_alive and (not length or length <= 1):
                for chunk in response.generate_body_chunks():
                    chunk_length = len(chunk)
                    if chunk_length:
                        ecb.WriteClient('%x\r\n' % chunk_length)
                        ecb.WriteClient(chunk + b'\r\n')
                ecb.WriteClient(b'0\r\n\r\n')  # Last chunk is empty
            else:
                for chunk in response.generate_body_chunks():
                    ecb.WriteClient(chunk)
            ecb.DoneWithSession(isapicon.HSE_STATUS_SUCCESS_AND_KEEP_CONN if keep_alive else
                                isapicon.HSE_STATUS_SUCCESS)
        except ExtensionError:
            ecb.DoneWithSession()  # Client disconnected.
        except Exception:
            publisher.log(''.join(traceback.format_exception(*sys.exc_info())))
            try:
                publisher.log('data: %r' % data)
                publisher.log('env: %r' % env)
                publisher.log('headers: %r' % headers)
                publisher.log('status: %r' % status)
                publisher.log('keep_alive: %r' % keep_alive)
            except NameError:
                pass
            ecb.DoneWithSession()
    Dispatch = dispatch


def create_pool(name, username=None, password=None, app_pool_identity_type=None, **attributes):
    from isapi.install import GetObject, _IIS_OBJECT
    import pywintypes
    pool_name = name + '_pool'
    try:
        GetObject(_IIS_OBJECT + '/AppPools/' + pool_name)
    except pywintypes.com_error:
        # The pool does not exist
        pools = GetObject(_IIS_OBJECT + '/AppPools')
        pool = pools.Create('IIsApplicationPool', pool_name)
        if username:
            pool.WAMUserName = username
            pool.WAMUserPass = password
            pool.AppPoolIdentityType = 3  # Specified user account
        elif app_pool_identity_type is not None:
            # 0: LocalSystem
            # 1: LocalService
            # 2: NetworkService
            # 3: ApplicationPoolIdentity
            pool.AppPoolIdentityType = app_pool_identity_type

        # Prevent IIS from restarting the process as we sometimes have long jobs
        # pool.PeriodicRestartTime = 0
        # pool.IdleTimeout = 0
        if platform.architecture()[0] == '32bit':
            try:
                pool.Enable32BitAppOnWin64 = True
            except AttributeError:
                pass
        for attr, value in attributes.items():
            setattr(pool, attr, value)
        pool.SetInfo()
    return pool_name


def delete_pool(name):
    from isapi.install import GetObject, _IIS_OBJECT
    import pywintypes
    pool_name = name.replace('/', '_') + '_pool'
    pools = GetObject(_IIS_OBJECT + '/AppPools')
    try:
        pools.Delete('IIsApplicationPool', pool_name)
    except pywintypes.com_error:
        pass  # The pool didn't exist


def install(name, description, argv=None, auth_anonymous=False, username=None, password=None,
            app_pool_identity_type=None, only_ntlm_authentication=None, **attributes):
    """Installs the DLL and sets up the virtual directory in IIS.

    If name is a path, then you will have to create the parent directories
    in IIS yourself, using, for instance, the Internet Service Manager GUI."""
    from isapi.install import (HandleCommandLine, ISAPIParameters, ScriptMapParams, VirtualDirParameters,
                               _APP_INPROC, _APP_OUTPROC)
    release, version, csd, ptype = platform.win32_ver()
    version = tuple(int(part) for part in version.split('.'))
    if release == '2000':
        # Windows 2000
        add_extension_file = False
        if csd <= 'SP2':
            app_protection = _APP_INPROC  # Doesn't work with more than one extension.
        else:  # Service pack 3 is untested
            app_protection = _APP_OUTPROC  # Works with many extensions.
    else:
        add_extension_file = True
        app_protection = _APP_OUTPROC  # Works with many extensions.

    def set_authentication_and_app_pool(params, options, webdir=None):
        if webdir:
            if version >= (5, 2):
                # Windows 2003 or later, _APP_OUTPROC does not work, we need our own AppPool.
                pool_name = create_pool(name.replace('/', '_'),
                                        username=username,
                                        password=password,
                                        app_pool_identity_type=app_pool_identity_type,
                                        **attributes)
                webdir.AppPoolID = pool_name
            if auth_anonymous:
                webdir.AuthNTLM = False
                webdir.AuthAnonymous = True
            else:
                webdir.AuthNTLM = True
                webdir.AuthAnonymous = False
                if only_ntlm_authentication:
                    # Kerberos authentication doesn't always work, see http://support.microsoft.com/kb/871179
                    webdir.NTAuthenticationProviders = 'NTLM'
            webdir.AspAllowSessionState = False
            webdir.SetInfo()

    def remove_pool(params, options):
        if version >= (5, 2):
            delete_pool(name)
    params = ISAPIParameters()
    sm = [ScriptMapParams(Extension='*', Flags=0, AddExtensionFile=add_extension_file)]
    if '/' in name:
        path = name.rsplit('/', 1)[0]
    else:
        path = None
    vd = VirtualDirParameters(Name=name,
                              Description=description,
                              AppProtection=app_protection,
                              Path=path,
                              ScriptMaps=sm,
                              ScriptMapUpdate='replace',
                              PostInstall=set_authentication_and_app_pool,
                              PostRemove=remove_pool)
    params.VirtualDirs = [vd]
    HandleCommandLine(params, argv=argv)


def restart_pool(name):
    from isapi.install import GetObject, _IIS_OBJECT
    import pywintypes
    pool_name = name.replace('/', '_') + '_pool'
    try:
        GetObject(_IIS_OBJECT + '/AppPools/' + pool_name).Recycle()
    except pywintypes.com_error:
        # The pool does not exist
        raise EnvironmentError('The pool %r does not exist' % pool_name)


def setup(name, description, create_publisher, argv=None, auth_anonymous=False):
    if hasattr(sys, 'isapidllhandle'):
        # Catch early errors with PythonWin and
        # the Tools/Trace Collector Debugging tool.
        # noinspection PyUnresolvedReferences
        import win32traceutil  # noqa: F401
        # Imported by IIS. Create the publisher.
        create_publisher()
    else:
        # Install. Set argv=['remove'] or give remove
        # as a command line argument to uninstall.
        install(name, description, argv, auth_anonymous)
