import re
from datetime import datetime

import user_agents

class ApacheLogParserException(Exception): pass

class LineDoesntMatchException(ApacheLogParserException):
    def __init__(self, log_line=None, regex=None, *args, **kwargs):
        self.log_line = log_line
        self.regex = regex
    
    def __repr__(self):
        return u"LineDoesntMatchException(log_line={0!r}, regex={1!r})".format(self.log_line, self.regex)

    __str__ = __repr__

def extract_inner_value(output_prefix, input_suffix):
    """
    Given an input format like %{Referer}o return a function that will extract that 'Referer' from a match
    """
    regex = re.compile("^%\{([^\}]+?)\}"+input_suffix+"$")
    def matcher(matched_string):
        match = regex.match(matched_string)
        inner_value = match.groups()[0]
        inner_value = inner_value.strip().lower().replace("-", "_")
        return output_prefix+inner_value
    return matcher

def make_regex(format_template):
    """
    Turn a format_template from %s into something like %[<>]?s
    """
    # FIXME support the return code format
    percent, rest = format_template[0], format_template[1:]
    return percent+"[<>]?"+rest

def extra_request_from_first_line(matched_strings):
    first_line = matched_strings['request_first_line']
    match = re.match("^(?P<method>GET|HEAD|POST|OPTIONS|PUT|CONNECT|PATCH|PROPFIND)\s?(?P<url>.{,10000}?)(\s+HTTP/(?P<http_ver>1.[01]))?$", first_line)
    if match is None:
        # Possibly garbage, ignore it
        results = { 'request_first_line': first_line, 'request_method': '', 'request_url': '', 'request_http_ver': ''}
    else:
        results = { 'request_first_line': first_line, 'request_method': match.groupdict()['method'], 'request_url': match.groupdict()['url'], 'request_http_ver': match.groupdict()['http_ver']}
    return results

def parse_user_agent(matched_strings):
    ua = matched_strings['request_header_user_agent']
    parsed_ua = user_agents.parse(ua)
    matched_strings.update({
        'request_header_user_agent__browser__family': parsed_ua.browser.family,
        'request_header_user_agent__browser__version_string': parsed_ua.browser.version_string,
        'request_header_user_agent__os__family': parsed_ua.os.family,
        'request_header_user_agent__os__version_string': parsed_ua.os.version_string,
        'request_header_user_agent__is_mobile': parsed_ua.is_mobile,
    })

    return matched_strings



def apachetime(s):
    """
    Given a string representation of a datetime in apache format (e.g.
    "01/Sep/2012:06:05:11 +0000"), return the python datetime for that string
    """
    month_map = {'Jan': 1, 'Feb': 2, 'Mar':3, 'Apr':4, 'May':5, 'Jun':6, 'Jul':7, 
        'Aug':8,  'Sep': 9, 'Oct':10, 'Nov': 11, 'Dec': 12}
    s = s[1:-1]
    return datetime(int(s[7:11]), month_map[s[3:6]], int(s[0:2]), \
         int(s[12:14]), int(s[15:17]), int(s[18:20]))

def format_time(matched_strings):
    time_recieved = matched_strings['time_recieved']
    obj = apachetime(time_recieved)
    return {'time_recieved':time_recieved, 'time_recieved_datetimeobj': obj, 'time_recieved_isoformat': obj.isoformat()}


FORMAT_STRINGS = [
    ['%%', '%', lambda match: '', lambda matched_strings: matched_strings],
    [make_regex('%a'), '\d{,3}(.\d{\3}){3}', lambda match: 'remote_ip', lambda matched_strings: matched_strings], #	Remote IP-address
    [make_regex('%A'), '\d{,3}(.\d{\3}){3}', lambda match: 'local_ip', lambda matched_strings: matched_strings], #	Local IP-address
    [make_regex('%B'), '(\d+|-)', lambda match: 'response_bytes', lambda matched_strings: matched_strings], #	Size of response in bytes, excluding HTTP headers.
    [make_regex('%b'), '(\d+|-)', lambda match: 'response_bytes_clf', lambda matched_strings: matched_strings], #	Size of response in bytes, excluding HTTP headers. In CLF format, i.e. a '-' rather than a 0 when no bytes are sent.
    [make_regex('%\{[^\]]+?\}C'), '.*?', extract_inner_value("cookie_", "C") , lambda matched_strings: matched_strings], #	The contents of cookie Foobar in the request sent to the server. Only version 0 cookies are fully supported.
    [make_regex('%D'), '-?\d+', lambda match: 'time_us', lambda matched_strings: matched_strings], #	The time taken to serve the request, in microseconds.
    [make_regex('%\{[^\}]+?\}e'), '.*?', 'env', lambda matched_strings: matched_strings], #	The contents of the environment variable FOOBAR
    [make_regex('%f'), '.*?', lambda match: 'filename', lambda matched_strings: matched_strings], #	Filename
    [make_regex('%h'), '.*?', lambda match: 'remote_host', lambda matched_strings: matched_strings], #	Remote host
    [make_regex('%H'), '.*?', lambda match: 'protocol', lambda matched_strings: matched_strings], #	The request protocol

    # Special case of below, for matching just user agent
    [make_regex('%\{User-Agent\}i'), '.*?', lambda match: "request_header_user_agent" , parse_user_agent],
    [make_regex('%\{[^\]]+?\}i'), '.*?', extract_inner_value("request_header_", "i") , lambda matched_strings: matched_strings], #	The contents of Foobar: header line(s) in the request sent to the server. Changes made by other modules (e.g. mod_headers) affect this. If you're interested in what the request header was prior to when most modules would have modified it, use mod_setenvif to copy the header into an internal environment variable and log that value with the %\{VARNAME}e described above.

    [make_regex('%k'), '.*?', lambda match: 'num_keepalives', lambda matched_strings: matched_strings], #	Number of keepalive requests handled on this connection. Interesting if KeepAlive is being used, so that, for example, a '1' means the first keepalive request after the initial one, '2' the second, etc...; otherwise this is always 0 (indicating the initial request). Available in versions 2.2.11 and later.
    [make_regex('%l'), '.*?', lambda match: 'remote_logname', lambda matched_strings: matched_strings], #	Remote logname (from identd, if supplied). This will return a dash unless mod_ident is present and IdentityCheck is set On.
    [make_regex('%m'), '.*?', lambda match: 'method', lambda matched_strings: matched_strings], #	The request method
    [make_regex('%\{[^\]]+?\}n'), '.*?', extract_inner_value("note_", "n") , lambda matched_strings: matched_strings], #	The contents of note Foobar from another module.
    [make_regex('%\{[^\]]+?\}o'), '.*?',extract_inner_value("response_header_", "o") , lambda matched_strings: matched_strings], #	The contents of Foobar: header line(s) in the reply.
    [make_regex('%p'), '.*?', lambda match: 'server_port', lambda matched_strings: matched_strings], #	The canonical port of the server serving the request
    [make_regex('%\{[^\]]+?\}p'), '.*?', extract_inner_value("server_port_", "p") , lambda matched_strings: matched_strings], #	The canonical port of the server serving the request or the server's actual port or the client's actual port. Valid formats are canonical, local, or remote.
    [make_regex('%P'), '.*?', lambda match: 'pid', lambda matched_strings: matched_strings], #	The process ID of the child that serviced the request.
    [make_regex('%\{[^\]]+?\}P'), '.*?', extract_inner_value("pid_", "P") , lambda matched_strings: matched_strings], #	The process ID or thread id of the child that serviced the request. Valid formats are pid, tid, and hextid. hextid requires APR 1.2.0 or higher.
    [make_regex('%q'), '.*?', lambda match: 'query_string' , lambda matched_strings: matched_strings], #	The query string (prepended with a ? if a query string exists, otherwise an empty string)
    [make_regex('%r'), '.*?', lambda match: 'request_first_line', extra_request_from_first_line], #	First line of request
    [make_regex('%R'), '.*?', lambda match: 'handler', lambda matched_strings: matched_strings], #	The handler generating the response (if any).
    [make_regex('%s'), '[0-9]+?', lambda match: 'status', lambda matched_strings: matched_strings], #	Status. For requests that got internally redirected, this is the status of the *original* request --- %>s for the last.
    [make_regex('%t'), '.*?', lambda match: 'time_recieved', format_time], #	Time the request was received (standard english format)
    [make_regex('%\{[^\]]+?\}t'), '.*?', extract_inner_value("time_", "t") , lambda matched_strings: matched_strings], #	The time, in the form given by format, which should be in strftime(3) format. (potentially localized)
    [make_regex('%T'), '.*?', lambda match: 'time_s', lambda matched_strings: matched_strings], #	The time taken to serve the request, in seconds.
    [make_regex('%u'), '.*?', lambda match: 'remote_user', lambda matched_strings: matched_strings], #	Remote user (from auth; may be bogus if return status (%s) is 401)
    [make_regex('%U'), '.*?', lambda match: 'url_path' , lambda matched_strings: matched_strings], #	The URL path requested, not including any query string.
    [make_regex('%v'), '.*?', lambda match: 'server_name', lambda matched_strings: matched_strings], #	The canonical ServerName of the server serving the request.
    [make_regex('%V'), '.*?', lambda match: 'server_name2', lambda matched_strings: matched_strings], #	The server name according to the UseCanonicalName setting.
    [make_regex('%X'), '.*?', lambda match: 'conn_status', lambda matched_strings: matched_strings], #	Connection status when response is completed:
        # X =	connection aborted before the response completed.
        # + =	connection may be kept alive after the response is sent.
        # - =	connection will be closed after the response is sent.
        # (This directive was %c in late versions of Apache 1.3, but this conflicted with the historical ssl %{var}c syntax.)
    [make_regex('%I'), '.*?', lambda match: 'bytes_rx', lambda matched_strings: matched_strings], #	Bytes received, including request and headers, cannot be zero. You need to enable mod_logio to use this.
    [make_regex('%O'), '.*?', lambda match: 'bytes_tx', lambda matched_strings: matched_strings], #	Bytes sent, including headers, cannot be zero. You need to enable mod_logio to use this.
]

def make_parser(format_string):
    pattern = "("+"|".join(x[0] for x in FORMAT_STRINGS)+")"
    parts = re.split(pattern, format_string)

    
    functions_to_parse = {}

    log_line_regex = ""
    while True:
        if len(parts) == 0:
            break
        if len(parts) == 1:
            raw, regex = parts.pop(0), None
        elif len(parts) >= 2:
            raw, regex = parts.pop(0), parts.pop(0)
        if len(raw) > 0:
            log_line_regex += re.escape(raw)
        if regex is not None:
            for format_spec in FORMAT_STRINGS:
                pattern_regex, log_part_regex, name_func, values_func = format_spec
                match = re.match("^"+pattern_regex+"$", regex)
                if match:
                    name = name_func(match.group())
                    functions_to_parse[name] = values_func
                    log_line_regex += "(?P<"+name+">"+log_part_regex+")"
                    break

    log_line_regex = re.compile(log_line_regex)

    def matcher(log_line):
        match = log_line_regex.match(log_line)
        if match is None:
            raise LineDoesntMatchException(log_line=log_line, regex=log_line_regex.pattern)
        else:
            results = {}
            for name in functions_to_parse:
                values = {name: match.groupdict()[name]}
                values = functions_to_parse[name](values)
                results.update(values)

            return results
    return matcher
    

