import re
from subprocess import check_output
from datetime import datetime, timedelta
from email.utils import parsedate_tz, mktime_tz
from licenses.models import LicenseModel


def parseOutput(pattern2action, output):
    for line in output.split('\n'):
        for pattern, action in pattern2action:
            m = re.match(pattern, line)
            if m:
                action(line, m.groups())


def updateModelFromNode(serial):
    info = serialInfo(serial)
    found = info['found']
    activation_date = info.get('activationDate')
    days_left = info.get('daysLeft')
    if found:
        try:
            license = LicenseModel.objects.get(pk=serial)
        except LicenseModel.DoesNotExist:
            license = LicenseModel(serial=serial)
        expiration = None
        if days_left and activation_date:
            expiration = activation_date + timedelta(days=days_left)
        license.expiration_date = expiration
        license.activation_date = activation_date
        license.activated = activation_date != None
        license.save()
    return found


def serialDeactivate(serialString):
    check_output(['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'deactivate', '-serial', serialString], text=True)

def serialDeactivateWithORM(serialString):
    serialDeactivate(serialString)
    updateModelFromNode(serialString)


def serialDelete(serialString):
    check_output(['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'delete', '-serial', serialString], text=True)

def serialDeleteWithORM(serialString):
    serialDelete(serialString)
    try:
        license = LicenseModel.objects.get(pk=serialString)
        license.delete()
    except LicenseModel.DoesNotExist:
        pass


def serialGenerate(numberOfSerials, limitInDays=None):
    numberOfSerials = int(numberOfSerials)
    args = ['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'generate', '-n', str(numberOfSerials)]
    if limitInDays is not None:
        args.append('-acive_days')
        args.append(str(int(limitInDays)))
    output = check_output(args, text=True)
    if numberOfSerials == 1:
        return [output.split(' ')[4].strip()]
    return output.split('\n')[1:-1]

def serialGenerateWithORM(*l, **kv):
    serials = serialGenerate(*l, **kv)
    objects = map(lambda s: LicenseModel(serial=s,
                                         expiration_date=None,
                                         activation_date=None,
                                         activated=False), serials)
    LicenseModel.objects.bulk_create(objects)
    return serials


def serialList(numberOfSerials=None, offset=None, onlyActivated=None, totalState=None):
    serial_pattern = r'serial key: ([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}) (inactive|activated)'
    serial_start_pattern = r'serial keys:'
    serial_key_pattern = r'([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}) (inactive|activated)'
    total_pattern = r'total (\d+) keys'
    empty_pattern = r'keys not found'

    result = {
        'serials': [],
    }

    pattern2action = [
        (serial_pattern, lambda string, groups: result['serials'].append(groups[0])),
        (serial_key_pattern, lambda string, groups: result['serials'].append(groups[0])),
        (total_pattern, lambda string, groups: result.__setitem__('total', int(groups[0]))),
    ]

    args = ['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'list']
    if numberOfSerials is not None:
        args.append('-n')
        args.append(str(numberOfSerials))
    if offset is not None:
        args.append('-shift')
        args.append(str(offset))
    if onlyActivated is not None:
        if onlyActivated:
            args.append('-activated_only')
        else:
            args.append('-inactive_only')
    if totalState is not None:
        if totalState == 'total_only':
            args.append('-total_only')
        elif totalState == 'nototal':
            args.append('-nototal')
        else:
            raise Exception("totalState is invalid. Must be 'total_only' or 'nototal'")

    output = check_output(args, text=True)
    parseOutput(pattern2action, output)

    return result


def serialUpdate(serialString, newLimitInDays):
    args = ['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'update', '-serial', serialString, '-active_days', str(newLimitInDays)]
    check_output(args, text=True)

def serialUpdateWithORM(serialString, newLimitInDays):
    serialUpdate(serialString, newLimitInDays)
    updateModelFromNode(serialString)


def serialInfo(serialString):
    not_activated_pattern = r'serial ([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}) not acti[vt]ated'
    activated_pattern = r'serial ([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}) acti[vt]ated (.+)'
    no_limit_pattern = r'(?:license length|expired): no time limit'
    days_left_pattern = r'(?:license length|expired): (\d+) days'
    not_found_pattern = r"serial '[^']*' not found"

    result = {
        'found': True
    }

    def update(serialStr, adate):
        nonlocal result
        result['serial'] = serialStr
        if adate:
            result['activationDate'] = datetime.fromtimestamp(mktime_tz(parsedate_tz(adate)))
        else:
            result['activationDate'] = adate

    pattern2action = [
        (not_activated_pattern, lambda string, groups: update(groups[0], None)),
        (activated_pattern, lambda string, groups: update(groups[0], groups[1])),
        (days_left_pattern, lambda string, groups: result.__setitem__('daysLeft', int(groups[0]))),
        (not_found_pattern, lambda string, groups: result.__setitem__('found', False)),
    ]

    output = check_output(['/opt/cellframe-node/bin/cellframe-node-cli', 'vpn_cdb', 'serial', 'info', '-serial', serialString], text=True)

    parseOutput(pattern2action, output)

    return result
