import argparse
import logging as log
import os
import json
import sys
import re
import subprocess
from typing import Optional


def load_cvedata(dbpath: str) -> (dict, dict, dict):
    with open(os.path.join(dbpath, 'data', 'kernel_cves.json'), 'r', encoding='UTF-8') as f:
        kernel_cves = json.load(f)
    with open(os.path.join(dbpath, 'data', 'stream_data.json'), 'r', encoding='UTF-8') as f:
        stream_data = json.load(f)
    with open(os.path.join(dbpath, 'data', 'stream_fixes.json'), 'r', encoding='UTF-8') as f:
        stream_fixes = json.load(f)
    return kernel_cves, stream_data, stream_fixes


def extract_version() -> (int, int, int):
    version = patchlevel = sublevel = None
    verpat = re.compile(r'VERSION\s*=\s*([0-9]+)')
    plpat = re.compile(r'PATCHLEVEL\s*=\s*([0-9]+)')
    subpat = re.compile(r'SUBLEVEL\s*=\s*([0-9]+)')
    version = patchlevel = sublevel = 0
    with open('Makefile', 'r', encoding='UTF-8') as f:
        for line in f:
            if version and patchlevel and sublevel:
                break
            m = re.match(verpat, line)
            if m is not None:
                version = int(m.group(1))
                continue
            m = re.match(plpat, line)
            if m is not None:
                patchlevel = int(m.group(1))
                continue
            m = re.match(subpat, line)
            if m is not None:
                sublevel = int(m.group(1))
    return version, patchlevel, sublevel


def cve_in_stream(cve: str, stream: dict) -> Optional[str]:
    for ver, substream in stream.items():
        if cve in substream:
            return ver
    return None


def runcmd(cmd) -> (bool, str):
    try:
        proc = subprocess.run(cmd, stdin=subprocess.DEVNULL,
                              capture_output=True, encoding='UTF-8')
        return True, proc.stdout
    except subprocess.CalledProcessError:
        return False, None


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument('-P', '--database-path', help='path to CVE database repository', required=True)
    parser.add_argument('-d', '--debug', help='print debug messages', action='store_true')
    parser.add_argument('-s', '--summary', help='print only summary information', action='store_true')
    parser.add_argument('cve', nargs='+', help='CVE identifier')
    args = parser.parse_args()
    log.basicConfig(format="%(message)s", level=log.DEBUG if args.debug else log.INFO)
    ok, output = runcmd(['git', 'symbolic-ref', '--short', 'HEAD'])
    if not ok:
        log.error("Cannot get current branch")
        return 1
    branch = output.split('\n')[0]
    log.info("Current branch: {}".format(branch))
    kernel_cves, stream_data, stream_fixes = load_cvedata(args.database_path)
    ver, pl, sub = extract_version()
    log.info("Kernel version: {}.{}.{}".format(ver, pl, sub))
    # Reduce the stream for the major version to just the substreams with above current sub-version
    stream = {}
    for subver, substream in stream_data["{}.{}".format(ver, pl)].items():
        if subver == 'outstanding' or int((subver + '.0').split('.')[2]) > sub:
            stream[subver] = substream
    patchable = {}
    outstanding = {}
    not_applicable = []
    for cve in args.cve:
        if cve not in kernel_cves:
            log.warning("WARN: not found in kernel CVE database: {}".format(cve))
            continue
        found = cve_in_stream(cve, stream)
        if found is None:
            not_applicable.append(cve)
            try:
                affected_versions = kernel_cves[cve]['affected_versions']
            except KeyError:
                affected_versions = "(no data)"
            log.debug("{}: affected versions {}".format(cve, affected_versions))
        elif found == 'outstanding':
            outstanding[cve] = found
        else:
            patchable[cve] = found
    if args.summary:
        print("Applicable, patches available: {}".format(len(patchable)))
        print("Applicable, outstanding:       {}".format(len(outstanding)))
        print("Not applicable:                {}".format(len(not_applicable)))
        return 0

    if patchable:
        print("Applicable, patches available:")
    for cve in sorted(patchable.keys()):
        print("  {}: fixed in {}".format(cve, patchable[cve]))
        try:
            print("    {}\n".format(kernel_cves[cve]['nvd_text']))
        except KeyError:
            print("    (no description)\n")
    if outstanding:
        print("Applicable, no patch available:")
    for cve in sorted(outstanding.keys()):
        print("  {}: {}".format(cve, outstanding[cve]))
        try:
            print("    {}\n".format(kernel_cves[cve]['nvd_text']))
        except KeyError:
            print("    (no description)\n")
    if not_applicable:
        print("Not applicable:")
    for i in range(0, len(not_applicable), 8):
        subset = not_applicable[i:i+8]
        print("    {} \\".format(' '.join(subset)))

    return 0


if __name__ == "__main__":
    # noinspection PyBroadException
    try:
        ret = main()
        sys.exit(ret)
    except SystemExit:
        pass
    except Exception:
        import traceback

        traceback.print_exc(5)
        sys.exit(1)
