class KeyExchangeApp(object):


    """ These MUST include ALL headers exchanged. They may be 
        divided up into Inbound (returned via OPTIONS method)
        and Outbound (returned as part of the Response). 
        Failing to include a header will result in the user 
        agent rejecting the data.
    """
    CORS_HEADERS = [('Access-Control-Allow-Origin','*'), 
                ('Access-Control-Allow-Headers', 
                    ', '.join(['contenttype',
                        'x-keyexchange-cid',
                        'x-keyexchange-channel',
                        'x-keyexchange-id',
                        'x-keyexchange-log',
                        'if-match',
                        'if-none-match'])),
                ('Access-Control-Expose-Headers', 
                    ', '.join(['etag',
                        'x-status'])),
                ('Access-Control-Allow-Methods', 
                    ', '.join(['GET',
                        'POST', 
                        'PUT',
                        'OPTIONS']))]


    def __init__(self, config):
        self.config = config
        self.cid_len = config.get('keyexchange.cid_len', 4)
        self.ttl = config.get('keyexchange.ttl', 300)
        self.max_gets = config.get('keyexchange.max_gets', 6)
        self.root = self.config.get('keyexchange.root_redirect')
        servers = config.get('keyexchange.cache_servers', ['127.0.0.1:11211'])
        """ 
            * Allow any origin
            * Must list explicit headers to allow (case insensitive)
                * Any header included, but not listed will fail the result
            * Likewise, list all methods to be used.
        """
        if isinstance(servers, str):
            self.cache_servers = [servers]
        else:
            self.cache_servers = servers
        use_memory = config.get('keyexchange.use_memory', False)
        cache = get_memcache_class(use_memory)(self.cache_servers)
        self.cache = PrefixedCache(cache, _CPREFIX)

    def _get_new_cid(self, client_id):
        tries = 0
        ttl = time.time() + self.ttl
        content = ttl, [client_id], _EMPTY, None

        while tries < 100:
            new_cid = generate_cid(self.cid_len)
            if self.cache.get(new_cid) is not None:
                tries += 1
                continue   # already taken

            success = self.cache.add(new_cid, content, time=ttl)
            if success:
                break
            tries += 1

        if not success:
            raise HTTPServiceUnavailable()

        return new_cid

    def _health_check(self):
        """Checks that memcache is up and works as expected"""
        rand = ''.join([random.choice('abcdefgh1234567') for i in range(50)])
        key = 'test_%s' % rand
        success = self.cache.add(key, 'test')
        if not success:
            raise HTTPServiceUnavailable()
        stored = self.cache.get(key)
        if stored != 'test':
            raise HTTPServiceUnavailable()
        self.cache.delete(key)
        stored = self.cache.get(key)
        if stored is not None:
            raise HTTPServiceUnavailable()

    @wsgify
    def __call__(self, request):
        if request.method == 'OPTIONS':
            sys.stderr.write("###OPTIONS: \n");
            for h in self.CORS_HEADERS:
                sys.stderr.write("   %s: %s\n" % h);
            # Trace to see if this is actually setting the headers...
            return json_response('',
                headerlist = copy.deepcopy(self.CORS_HEADERS));
        request.config = self.config
        client_id = request.headers.get('X-KeyExchange-Id')
        method = request.method
        url = request.path_info

        # the root does a health check on memcached, then
        # redirects to services.mozilla.com
        if url == '/':
            if method != 'GET':
                raise HTTPMethodNotAllowed()
            self._health_check()
            raise HTTPMovedPermanently(location=self.root)

        match = _URL.match(url)
        if match is None:
            raise HTTPNotFound()

        url = match.group(1)
        if url == 'new_channel':
            # creation of a channel
            if method != 'GET':
                raise HTTPMethodNotAllowed()
            if not self._valid_client_id(client_id):
                # The X-KeyExchange-Id is valid
                try:
                    log = 'Invalid X-KeyExchange-Id'
                    log_cef(log, 5, request.environ, self.config,
                            msg=_cid2str(client_id))
                finally:
                    raise HTTPBadRequest()
            cid = self._get_new_cid(client_id)
            headers = [('X-KeyExchange-Channel', cid),
                       ('Content-Type', 'application/json')]
            headers.extend(self.CORS_HEADERS)
            return json_response(cid, headerlist=headers)

        elif url == 'report':
            if method != 'POST':
                raise HTTPMethodNotAllowed()
            return self.report(request, client_id)

        # validating the client id - or registering id #2
        channel_content = self._check_client_id(url, client_id, request)

        # actions are dispatched in this class
        sys.stderr.write('calling ' + method + "\n");
        method = getattr(self, '%s_channel' % method.lower(), None)
        if method is None:
            sys.stderr.write("not found\n");
            raise HTTPNotFound()

        return method(request, url, channel_content)

    def _valid_client_id(self, client_id):
        return client_id is not None and len(client_id) == 256

    def _check_client_id(self, channel_id, client_id, request):
        """Registers the client id into the channel.

        If there are already two registered ids, the channel is closed
        and we send back a 400. Also returns the new channel content.
        """
        if not self._valid_client_id(client_id):
            # the key is invalid
            try:
                log = 'Invalid X-KeyExchange-Id'
                log_cef(log, 5, request.environ, self.config,
                        msg=_cid2str(client_id))
            finally:
                # we need to kill the channel
                if not self._delete_channel(channel_id):
                    log_cef('Could not delete the channel', 5,
                            request.environ, self.config,
                            msg=_cid2str(channel_id))

                raise HTTPBadRequest()

        content = self.cache.get(channel_id)
        if content is None:
            # we have a valid channel id but it does not exists.
            log = 'Invalid X-KeyExchange-Channel'
            log_cef(log, 5, request.environ, self.config,
                    _cid2str(channel_id))
            raise HTTPNotFound()

        ttl, ids, data, etag = content
        if len(ids) < 2:
            # first or second id, if not already registered
            if client_id in ids:
                return content   # already registered
            ids.append(client_id)
        else:
            # already full, so either the id is present, either it's a 3rd one
            if client_id in ids:
                return  content  # already registered

            # that's an unknown id, hu-ho
            try:
                log = 'Unknown X-KeyExchange-Id'
                log_cef(log, 5, request.environ, self.config,
                        msg=_cid2str(client_id))
            finally:
                if not self._delete_channel(channel_id):
                    log_cef('Could not delete the channel', 5,
                            request.environ, self.config,
                            msg=_cid2str(channel_id))

                raise HTTPBadRequest()

        content = ttl, ids, data, etag

        # looking good
        if not self.cache.set(channel_id, content, time=ttl):
            raise HTTPServiceUnavailable()
        return content

    def _etag(self, data):
        return md5(data).hexdigest()

    def _etag_match(self, etag, header):
        if not hasattr(header, 'etags'):
            return False
        return etag in getattr(header, 'etags')

    def put_channel(self, request, channel_id, existing_content):
        sys.stderr.write("###Rcv'd PUT \n'" );
        """Append data into channel."""
        ttl, ids, old_data, old_etag = existing_content

        data = request.body
        sys.stderr.write("   body len: %s \n" % len(data));
        etag = self._etag(data)

        # check the If-Match header
        if 'If-Match' in request.headers:
            if str(request.if_match) != '*':
                # if If-Match is provided, it must be the value of
                # the etag before the update is applied
                if not self._etag_match(old_etag, request.if_match):
                    raise HTTPPreconditionFailed(etag=etag)
        elif 'If-None-Match' in request.headers:
            if str(request.if_none_match) == '*':
                # we will put data in the channel only if it's
                # empty (== first PUT)
                if old_data != _EMPTY:
                    raise HTTPPreconditionFailed(etag=etag,
                            headers=copy.deepcopy(self.CORS_HEADERS))

        if not self.cache.set(channel_id, (ttl, ids, request.body, etag),
                              time=ttl):
            raise HTTPServiceUnavailable(headers=
                    copy.deepcopy(self.CORS_HEADERS))

        sys.stderr.write("### Return success \n");

        return json_response('', etag=etag, 
                headers=copy.deepcopy(self.CORS_HEADERS))

    def get_channel(self, request, channel_id, existing_content):
        """Grabs data from channel if available."""
        ttl, ids, data, etag = existing_content

        # check the If-None-Match header
        if request.if_none_match is not None:
            if self._etag_match(etag, request.if_none_match):
                raise HTTPNotModified(headers=
                        copy.deepcopy(self.CORS_HEADERS))

        # keep the GET counter up-to-date
        # the counter is a separate key
        deletion = False
        ckey = 'GET:%s' % channel_id
        count = self.cache.get(ckey)
        if count is None:
            self.cache.set(ckey, '1')
        else:
            if int(count) + 1 == self.max_gets:
                # we reached the last authorized call, the channel is remove
                # after that
                deletion = True
            else:
                self.cache.incr(ckey)

        try:
            import pdb; pdb.set_trace();
            sys.stderr.write('dumping data: ' + json.dumps(data) + "\n")
            return json_response(data, dump=False, etag=etag, 
                    headers=copy.deepcopy(self.CORS_HEADERS))
        finally:
            # deleting the channel in case we did all GETs
            if deletion:
                if not self._delete_channel(channel_id):
                    log_cef('Could not delete the channel', 5,
                            request.environ, self.config,
                            msg=_cid2str(channel_id))

    def _delete_channel(self, channel_id):
        self.cache.delete('GET:%s' % channel_id)
        res = self.cache.get(channel_id)
        if res is None:
            return True   # already gone
        return self.cache.delete(channel_id)

    def blacklisted(self, ip, environ):
        log_cef('BlackListed IP', 5, environ, self.config, msg=ip)

    def report(self, request, client_id):
        """Reports a log and delete the channel if relevant"""
        # logging the report
        log = []
        header_log = request.headers.get('X-KeyExchange-Log')
        if header_log is not None:
            log.append(header_log)

        body_log = request.body[:2000].strip()
        if body_log != '':
            log.append(body_log)

        # logging only if the log is not empty
        if len(log) > 0:
            log = '\n'.join(log)
            log_cef('Report', 5, request.environ, self.config, msg=log)

        # removing the channel if present
        channel_id = request.headers.get('X-KeyExchange-Cid')
        if client_id is not None and channel_id is not None:
            content = self.cache.get(channel_id)
            if content is not None:
                # the channel is still existing
                ttl, ids, data, etag = content

                # if the client_ids is in ids, we allow the deletion
                # of the channel
                if not self._delete_channel(channel_id):
                    log_cef('Could not delete the channel', 5,
                            request.environ, self.config,
                            msg=_cid2str(channel_id))
        return json_response('', 
                headers=copy.deepcopy(self.CORS_HEADERS))