#!/usr/bin/env python3

# Author: Alex Murray <alex.murray@canonical.com>
# Copyright (C) 2021 Canonical Ltd.
#
# This script is distributed under the terms and conditions of the GNU General
# Public License, Version 2 or later. See http://www.gnu.org/copyleft/gpl.html
# for details.

#
# This script uses the output of check-syntax to determine what needs to
# be fixed. Typical usage is:
#
# ./scripts/check-syntax 2>&1 | ./scripts/check-syntax-fixup
#

import argparse
import os
import sys
import cve_lib


def insert_into_file(filename: str, linenum: int, line: str, dryrun=False, verbose=False):
    """Insert line into filename at linenum."""
    if not dryrun:
        # file may not already exist
        contents = []
        try:
            with open(filename, "r") as f:
                contents = f.readlines()
        except FileNotFoundError:
            pass
        if verbose:
            print("%s: %d: inserting '%s'" % (os.path.relpath(filename), linenum, line.strip()))
        # linenum is 1 based but arrays are 0-based
        contents.insert(linenum - 1, line)
        with open(cve, "w") as f:
            f.write("".join(contents))
    else:
        print("%s: %d: would insert '%s'" % (os.path.relpath(filename), linenum, line.strip()))


def delete_from_file(filename: str, linenum: int, dryrun=False, verbose=False):
    """Delete line at linenum from filename."""
    if not dryrun:
        # file may not already exist
        contents = []
        try:
            with open(filename, "r") as f:
                contents = f.readlines()
        except FileNotFoundError:
            pass
        if verbose:
            print("%s: %d: deleting... " % (os.path.relpath(filename), linenum))
        # linenum is 1 based but arrays are 0-based
        del contents[linenum - 1]
        with open(cve, "w") as f:
            f.write("".join(contents))
    else:
        print("%s: %d: would delete this line" % (os.path.relpath(filename), linenum))


parser = argparse.ArgumentParser("Automatically fixup issues flagged by check-syntax")
parser.add_argument(
    "-n",
    "--dry-run",
    action="store_true",
    default=False,
    help="Don't perform any actual modifications just print what would be done.",
)
parser.add_argument(
    "-v",
    "--verbose",
    action="store_true",
    default=False,
    help="Print output for each operation performed.",
)
parser.add_argument(
    "infile",
    nargs="?",
    help="File to read input from. Defaults to stdin.",
    type=argparse.FileType("r"),
    default=sys.stdin,
)
args = parser.parse_args()
if args.dry_run:
    print("DRY RUN - LIKE THE MATRIX, THIS IS JUST A SIMULATION.")


modified = []

for line in args.infile:
    # skip warnings
    if line.startswith("WARNING:"):
        continue
    # parse out file name, line number, and message
    parts = line.split(":")
    if len(parts) != 3:
        continue
    # strip whitespace from all parts
    parts = map(lambda s: s.strip(), parts)
    cve, linenum, msg = parts
    linenum = int(linenum)

    # don't modify a file more than once otherwise the line numbers get out of whack
    if cve in modified:
        # print unhandled lines
        print(line, file=sys.stderr)
        continue

    if "missing release" in msg or 'DOES exist' in msg:
        # e.g. golang missing release 'gke/gke-1.19'
        # e.g. package 'libextractor' DOES exist in 'trusty/esm'
        parts = msg.split(" ")
        if 'DOES exist' in msg:
            pkg = parts[1].replace("'", "")
            rel = parts[-1].replace("'", "")
        else:
            pkg = parts[0]
            rel = parts[-1].replace("'", "")

        # get status from the parent release if there is one
        status = "needs-triage"
        try:
            _, _, _, details = cve_lib.get_subproject_details(rel)
            parent = details["parent"]
            # this may be either an alias or a full name but cve_lib only
            # uses aliases
            parent = cve_lib.release_alias(parent)
            data = cve_lib.load_cve(cve)
            status = data["pkgs"][pkg][parent][0]
            note = data["pkgs"][pkg][parent][1]
            if len(note) > 0:
                status = status + " (" + note + ")"
            # if parent reached EOL then we are likely the new alive
            # release so ignore their status in that case
            if "reached end-of-life" in status or "out of standard support" in status:
                status = 'needs-triage'
        except (KeyError, ValueError, TypeError):
            pass

        if rel == cve_lib.devel_release:
            rel = "devel"

        fixup = "{rel}_{pkg}: {status}\n".format(rel=rel, pkg=pkg, status=status)

        # remove this hard-coded hack one-day...
        if rel in cve_lib.external_releases or \
           (rel == "trusty/esm" and "DOES exist" in msg):
            # ignore boilerplate files for external_releases
            if "boilerplate" in cve:
                # print unhandled lines
                print(line, file=sys.stderr)
                continue
            cve = os.path.join(
                cve_lib.get_external_subproject_cve_dir(rel), os.path.basename(cve)
            )
            # linenum is only relevant to the original cve file
            linenum = 1

        # Remove the 'DNE' line before adding the new one
        if 'DOES exist' in msg:
            delete_from_file(cve, linenum, args.dry_run, args.verbose)

        insert_into_file(cve, linenum, fixup, args.dry_run, args.verbose)
        modified.append(cve)

    elif "unknown package" in msg or "not in" in msg or "unknown release" in msg:
        # delete this line since
        delete_from_file(cve, linenum, args.dry_run, args.verbose)
        modified.append(cve)
    else:
        # print unhandled lines
        print(line, file=sys.stderr)
