#!/usr/bin/python3

from __future__ import print_function

import argparse
import os
import os.path
from collections import namedtuple
import re

import ssg.build_yaml
import ssg.utils
import ssg.environment
import ssg.id_translate
import ssg.build_renumber
import ssg.products


Paths_ = namedtuple("Paths_", ["xccdf", "oval", "ocil", "build_ovals_dir"])


def parse_args():
    parser = argparse.ArgumentParser(
        description="Converts SCAP Security Guide YAML benchmark data "
        "(benchmark, rules, groups) to XCCDF Shorthand Format"
    )
    parser.add_argument(
        "--build-config-yaml", required=True,
        help="YAML file with information about the build configuration. "
        "e.g.: ~/scap-security-guide/build/build_config.yml"
    )
    parser.add_argument(
        "--product-yaml", required=True,
        help="YAML file with information about the product we are building. "
        "e.g.: ~/scap-security-guide/rhel9/product.yml"
    )
    parser.add_argument(
        "--xccdf", required=True,
        help="Output XCCDF file. "
        "e.g.:  ~/scap-security-guide/build/rhel9/ssg-rhel9-xccdf.xml"
    )
    parser.add_argument(
        "--ocil", required=True,
        help="Output OCIL file. "
        "e.g.:  ~/scap-security-guide/build/rhel9/ssg-rhel9-ocil.xml"
    )
    parser.add_argument(
        "--oval", required=True,
        help="Output OVAL file. "
        "e.g.:  ~/scap-security-guide/build/rhel9/ssg-rhel9-oval.xml"
    )
    parser.add_argument(
        "--build-ovals-dir",
        dest="build_ovals_dir",
        help="Directory to store OVAL document for each rule.",
    )
    parser.add_argument(
        "--resolved-base",
        help="To which directory to put processed rule/group/value YAMLs."
    )
    parser.add_argument(
        "--thin-ds-components-dir",
        help="Directory to store XCCDF, OVAL, OCIL, for thin data stream. (off: to disable)"
        "e.g.: ~/scap-security-guide/build/rhel9/thin_ds_component/"
        "Fake profiles are used to create thin DS. Components are generated for each profile.",
    )
    return parser.parse_args()


def link_oval(xccdftree, checks, output_file_name, build_ovals_dir):
    translator = ssg.id_translate.IDTranslator("ssg")
    oval_linker = ssg.build_renumber.OVALFileLinker(
        translator, xccdftree, checks, output_file_name
    )
    oval_linker.build_ovals_dir = build_ovals_dir
    oval_linker.link()
    oval_linker.save_linked_tree()
    oval_linker.link_xccdf()
    return oval_linker


def link_ocil(xccdftree, checks, output_file_name, ocil):
    translator = ssg.id_translate.IDTranslator("ssg")
    ocil_linker = ssg.build_renumber.OCILFileLinker(
        translator, xccdftree, checks, output_file_name
    )
    ocil_linker.link(ocil)
    ocil_linker.save_linked_tree()
    ocil_linker.link_xccdf()


def store_xccdf_per_profile(loader, oval_linker, variables_ids, thin_ds_components_dir):
    for id_, xccdftree in loader.get_benchmark_xml_by_profile(variables_ids):
        xccdf_file_name = os.path.join(thin_ds_components_dir, "xccdf_{}.xml".format(id_))
        oval_file_name = os.path.join(thin_ds_components_dir, "oval_{}.xml".format(id_))

        checks = xccdftree.findall(".//{%s}check" % ssg.constants.XCCDF12_NS)
        oval_linker.linked_fname = oval_file_name
        oval_linker.linked_fname_basename = os.path.basename(oval_file_name)
        oval_linker.checks_related_to_us = oval_linker.get_related_checks(checks)

        oval_linker.link_xccdf()

        ssg.xml.ElementTree.ElementTree(xccdftree).write(xccdf_file_name, encoding="utf-8")


def get_linked_xccdf(loader, xccdftree, args):
    checks = xccdftree.findall(".//{%s}check" % ssg.constants.XCCDF12_NS)

    oval_linker = link_oval(xccdftree, checks, args.oval, args.build_ovals_dir)

    ocil = loader.export_ocil_to_xml()
    link_ocil(xccdftree, checks, args.ocil, ocil)
    return oval_linker, xccdftree


def get_variables_from_go_templating(rule, var_ids):
    go_templating_pattern = re.compile(r"{{(.*?)}}")
    go_templating_var_pattern = re.compile(r"\.([a-zA-Z0-9_]+)")
    for ele in rule.itertext():
        for match in go_templating_pattern.finditer(ele):
            for var in go_templating_var_pattern.finditer(match.group(1)):
                var_ids.add(var.group(1))


def get_rules_with_variables(xccdftree):
    rules = xccdftree.findall(".//{%s}Rule" % ssg.constants.XCCDF12_NS)
    out_var_ids = {}
    for rule in rules:
        var_ids = set()
        check_export_els = rule.findall(".//{%s}check-export" % ssg.constants.XCCDF12_NS)
        for check_export_el in check_export_els:
            var_ids.add(
                check_export_el.get("value-id").replace("xccdf_org.ssgproject.content_value_", "")
            )
        sub_els = rule.findall(".//{%s}sub" % ssg.constants.XCCDF12_NS)
        for sub_el in sub_els:
            var_ids.add(
                sub_el.get("idref").replace("xccdf_org.ssgproject.content_value_", "")
            )
        get_variables_from_go_templating(rule, var_ids)
        out_var_ids[
            rule.get("id").replace("xccdf_org.ssgproject.content_rule_", "")
        ] = var_ids
    return out_var_ids


def main():
    args = parse_args()

    env_yaml = ssg.environment.open_environment(
        args.build_config_yaml, args.product_yaml)
    product_yaml = ssg.products.Product(args.product_yaml)
    base_dir = product_yaml["product_dir"]
    benchmark_root = ssg.utils.required_key(env_yaml, "benchmark_root")

    # we have to "absolutize" the paths the right way, relative to the
    # product_yaml path
    if not os.path.isabs(benchmark_root):
        benchmark_root = os.path.join(base_dir, benchmark_root)

    loader = ssg.build_yaml.LinearLoader(
        env_yaml, args.resolved_base)
    loader.load_compiled_content()
    loader.load_benchmark(benchmark_root)
    loader.add_fixes_to_rules()

    _, xccdftree = get_linked_xccdf(loader, loader.get_benchmark_xml(), args)
    var_ids = get_rules_with_variables(xccdftree)

    oval_linker, xccdftree = get_linked_xccdf(
        loader, loader.export_benchmark_to_xml(var_ids), args
    )

    ssg.xml.ElementTree.ElementTree(xccdftree).write(
        args.xccdf, xml_declaration=True, encoding="utf-8")

    if args.thin_ds_components_dir is not None and args.thin_ds_components_dir != "off":
        if not os.path.exists(args.thin_ds_components_dir):
            os.makedirs(args.thin_ds_components_dir)
        store_xccdf_per_profile(loader, oval_linker, var_ids, args.thin_ds_components_dir)
        oval_linker.build_ovals_dir = args.thin_ds_components_dir
        oval_linker.save_oval_document_for_each_xccdf_rule("oval_")


if __name__ == "__main__":
    main()
