import os
import re
import gzip
import fnmatch
import logging
import subprocess
from gpstime import gpstime
from dateutil.tz import tzutc, tzlocal

from . import util

##################################################

GUARD_TARGET = os.getenv('GUARD_TARGET', '/etc/guardian')
GUARD_NODE_DIR = os.path.join(GUARD_TARGET, 'nodes')
GUARD_NODE_LOG_DIR = os.path.join(GUARD_TARGET, 'logs')

TIME_RE = re.compile('(\d{4}-?\d{2}-?\d{2}[T_]\d{2}:\d{2}:\d{2}\S*) ?(.*)')

##################################################

def full_node_list():
    """return list of all active nodes"""
    return os.listdir(GUARD_NODE_DIR)


def nodes_expand(glob_list=None):
    """return list of nodes matching patterns

    Match a list of globbed node names with the full node list,
    returning a list of matching nodes.  If no glob list is provided
    all nodes will be returned.

    An exception is raised if a pattern matches no known nodes.

    """
    node_list = full_node_list()
    if not glob_list:
        return sorted(node_list)
    expanded = set()
    for glob in glob_list:
        matches = fnmatch.filter(node_list, glob)
        if not matches:
            raise util.GuardCtrlError("Pattern {} matches no known nodes".format(glob))
        expanded |= set(matches)
    return sorted(expanded)

##########

def in_interval(t, after=None, before=None):
    """determine if time within interval"""
    ab = True
    bb = True
    if after and t < after:
        ab = False
    if before and t > before:
        bb = False
    return ab and bb

##########

def opportunistically_decompress(filename, mode='r'):
    """trick to opportunistically decompress gzip files

    Opens filename and looks for the gzip "magic number" in the first
    couple of bytes.  If it is not found the open file object is
    returned.  If it is found, the file object is un-gziped and then
    returned.

    """
    GZIP_MAGIC_NUMBER = '\x1f\x8b\x08'
    fd = open(filename, mode)
    # gzip magic number
    data = fd.read(len(GZIP_MAGIC_NUMBER))
    fd.seek(0)
    if data != GZIP_MAGIC_NUMBER:
        return fd
    else:
        return gzip.GzipFile(filename, mode, fileobj=fd)

##################################################

def find_log_files(logdir):
    """generator of all svlogd log files in directory"""
    # for python3:
    # with subprocess.Popen(['/usr/bin/tai64nlocal'],
    #                       stdin=subprocess.PIPE,
    #                       stdout=subprocess.PIPE,
    #                       #universal_newlines=True,
    #                       ) as p:
    # FIXME: this is obviously non-portable
    p = subprocess.Popen(['/usr/bin/tai64nlocal'],
                         stdin=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         )
    if True:
        for f in os.listdir(logdir):
            if f[0] != '@':
                continue
            i = f.split('.')[0]
            p.stdin.write(i+'\n')
            o = p.stdout.readline().strip().split('.')[0]
            dt = gpstime.strptime(o, '%Y-%m-%d %H:%M:%S')
            # tai64n log file names are local time
            dt = dt.replace(tzinfo=tzlocal())
            yield (f, dt)
        p.terminate()
        p.wait()
        if os.path.exists(os.path.join(logdir, 'current')):
            # include 'current' at the end
            yield ('current', gpstime.now(tz=tzlocal()))

def filter_files(logdir, after=None, before=None):
    """generator of svlogd log files from directory covering interval (after,before)"""
    last = gpstime.fromtimestamp(0, tz=tzutc())
    for f, dt in sorted(find_log_files(logdir), key=lambda k: k[1]):
        bb = True
        ab = True
        if after and after > dt:
            bb = False
        if before and before < last:
            ab = False
        last = dt
        if bb and ab:
            yield (f, dt)

def filter_files_multidir(logdirs, after=None, before=None):
    """generator of all svlogd files in a list of directories, filtered by time interval"""
    for logdir in logdirs:
        for f, dt in filter_files(logdir, after, before):
            yield (os.path.join(logdir, f), dt)

def parse_log_line(line):
    """parse log line

    Take a log line string and return a tuple of (datetime, msg)
    extracted from the line.

    """
    line = line.strip()
    match = TIME_RE.match(line)
    if not match:
        return None, line
    ds = match.group(1)
    msg = match.group(2)
    # dt = gpstime.strptime(ds.split('.')[0], '%Y-%m-%dT%H:%M:%S')
    try:
        dt = gpstime.strptime(ds, '%Y-%m-%dT%H:%M:%S.%f')
    except ValueError:
        try:
            dt = gpstime.strptime(ds, '%Y-%m-%dT%H:%M:%S.%fZ')
        except ValueError:
            try:
                dt = gpstime.strptime(ds, '%Y%m%d_%H:%M:%S.%f')
            except:
                return None, msg
    # guardian log times are UTC
    dt = dt.replace(tzinfo=tzutc())
    return dt, msg

def dump_lines_multifile(fs):
    """generator of log lines from specified files"""
    for f in fs:
        logging.debug("dumping file: %s" % f)
        with opportunistically_decompress(f) as fd:
            for line in fd:
                yield line.strip()

def filter_lines(lines, after=None, before=None):
    """filter lines within specified range"""
    for line in lines:
        dt, msg = parse_log_line(line)
        if dt:
            if in_interval(dt, after, before):
                yield (dt, msg)
        else:
            yield (dt, msg)

def filter_lines_multifile(fs, after=None, before=None):
    """generator of log lines within specified range from specified files"""
    for f in fs:
        logging.debug("filtering file: %s" % f)
        with opportunistically_decompress(f) as fd:
            for log in filter_lines(fd, after, before):
                yield log

##################################################

def gen_node_log_dirs(nodes):
    """generator of node log directory paths"""
    for node in nodes:
        yield os.path.join(GUARD_NODE_LOG_DIR, node)

def find_node_log_files(nodes, after=None, before=None):
    """find all log files for the specified nodes within time range"""
    nodedirs = list(gen_node_log_dirs(nodes))
    if after is True:
        return [os.path.join(d, 'current') for d in nodedirs]
    else:
        return [p for p,dt in sorted(filter_files_multidir(nodedirs, after, before), key=lambda k: k[1])]

def sort_log_lines(lines, sort='time'):
    """sort log lines by date"""
    if sort == 'time':
        return sorted(lines, key=lambda k: k[0])
    else:
        return lines

def format_log_line(dt, msg, time_format='utc'):
    """format output of log (datetime, message) tuples"""
    if dt:
        if time_format == 'utc':
            dts = dt.strftime('%Y-%m-%d_%H:%M:%S.%fZ')
        elif time_format == 'local':
            dts = dt.astimezone(tzlocal()).strftime('%Y-%m-%d_%H:%M:%S.%f')
        elif time_format == 'gps':
            dts = '{:.3f}'.format(dt.gps())
        else:
            dts = str(dt)
    else:
        dts = '??????????'
    return '{} {}'.format(dts, msg)

##################################################

def node_log_search(nodes, after=None, before=None):
    """generator of node log lines in some time range

    """
    paths = find_node_log_files(nodes, after, before)

    # opportunistically sort the lines only if more then one node is
    # specified
    if len(nodes) == 1:
        sort = None
    else:
        sort = 'time'

    it = filter_lines_multifile(paths, after, before)
    return sort_log_lines(it, sort)
