#!/usr/bin/env python3

import argparse
import logging
import xml.etree.ElementTree as ET
import re
import sys
import json

log = None
ITEM_T_MAP = {
  "0": "float",
  "1": "character",
  "2": "log",
  "3": "unsigned",
  "4": "text",
}
TRIGGER_P_MAP = {
  "0": "not_classified",
  "1": "info",
  "2": "warn",
  "3": "average",
  "4": "high",
  "5": "disaster",
}
EVALTYPE_P_MAP = { 
  "0": "andor",
  "1": "and",
  "2": "or",
  "3": "custom",
}
OPERATOR_P_MAP = { 
  "8": "match",
  "9": "notmatch",
}
GRAPH_T_MAP = {
    "0": "normal",
    "1": "stacked",
    "2": "pie",
    "3": "exploded",
}
GRAPH_A_MAP = {
    "0": "calculated",
    "1": "fixed",
    "2": "item",
}
GRAPH_F_MAP = {
    "1": "min",
    "2": "average",
    "4": "max",
    "7": "all",
    "9": "last",
}
GRAPH_D_MAP = {
    "0": "line",
    "1": "filled",
    "2": "bold",
    "3": "dot",
    "4": "dashed",
    "5": "gradient",
}
GRAPH_IT_MAP = {
    "0": "simple",
    "2": "sum",
}
GRAPH_S_MAP = {
    "0": "left",
    "1": "right",
}

names = {}
names_rev = {}
item_cache = {}
state = {}

def hasCachedTfName(inname):
    return inname in names

def safeTfName(inname):
    if inname in names:
        return names[inname]
    gen = re.sub(r'[^0-9a-z]+', '-', inname, flags=re.IGNORECASE).lower().strip('-')

    if state.get('prefix'):
        gen = state.get('prefix')+'-'+gen

    lookup = gen

    i = 0
    while lookup in names_rev:
        lookup = gen + '-' + str(i)
        i += 1
    names[inname] = lookup
    names_rev[lookup] = inname

    return lookup

def expressionRef(ex):
    def subTmplFun(match):
        cap = match.group(1)
        log.debug("evaluating %s" % cap)

        # check if a ref, local cache of refs
        if hasCachedTfName(cap):
            return '{${zabbix_template.'+safeTfName(cap)+'.host}:'

        # will be used once conditional in place
        return '{'+cap+':'
    
    def subItemFun(match):
        cap = match.group(1)
        log.debug("evaluating %s" % cap)

        # check if a ref, local cache of refs
        if hasCachedTfName(cap):
            log.debug("got match")
            if cap in item_cache:
                log.debug("cache hit %s" % item_cache[cap])
                return ':${'+item_cache[cap]['resource_type']+'.'+safeTfName(cap)+'.key}.'+match.group(2)+'('

        # will be used once conditional in place
        return ':'+cap+'.'

    res = re.sub(r'{([^{:]+):',
    subTmplFun,
    ex)

    res = re.sub(r':(.+?)\.(\w+)\(',
    subItemFun,
    res)

    return res

def extractTemplates(root):
    templates = []

    for tmpl in root.findall("templates/template"):
        log.debug("got tmpl xml: %s" % tmpl)
    
        obj = {
            'template': getattr(tmpl.find('template'), 'text', None),
            'name': getattr(tmpl.find('name'), 'text', None),
            'description': getattr(tmpl.find('description'), 'text', None),
            'groups': [],
            'applications': [],
            'items': [],
            'triggers': [],
        }
        obj["template_safe"] = safeTfName(obj["template"])

        # extract discovery rules
        obj["rules"] = extractLLDRules(tmpl)
        log.debug("got rules %s" % obj['rules'])

        # extract applications
        for item in tmpl.findall("applications/application/name"):
            obj["applications"].append(item.text)

        # extract items
        for item in tmpl.findall("items/item"):
            itemobj = {}
            for child in item:
                if child.text is None:
                    continue
                itemobj[child.tag] = child.text
                log.debug("%s: %s" % (child.tag, child.text)) 
            if 'key' not in itemobj or 'value_type' not in itemobj:
                continue
            itemobj["key_safe"] = safeTfName(itemobj["key"])

            itemobj["applications"] = []
            for a in item.findall('applications/application/name'):
                itemobj["applications"].append(a.text)

            obj['items'].append(itemobj)
            item_cache[itemobj["key"]] = itemobj

        log.debug("got tmpl object %s" % obj)
        templates.append(obj)
    return templates

def extractLLDRules(root):
    rules = []

    for t in root.findall("discovery_rules/discovery_rule"):
        tobj = {}
        for child in t:
            if child.text is None:
                continue
            if len(child) > 0:
                continue
            tobj[child.tag] = child.text
            log.debug("lld attr %s: %s" % (child.tag, child.text)) 

        tobj["name_safe"] = safeTfName(tobj["name"])

        items = []
        # nested things too
        for i in t.findall('item_prototypes/item_prototype'):
            iobj = {}
            for child in i:
                if child.text is None:
                    continue
                if len(child) > 0:
                    continue
                iobj[child.tag] = child.text
                log.debug("lld item attr %s: %s" % (child.tag, child.text)) 

            iobj["key_safe"] = safeTfName(iobj["key"])
            iobj["prototype"] = True

            iobj["applications"] = []
            for a in i.findall('applications/application/name'):
                iobj["applications"].append(a.text)

            item_cache[iobj["key"]] = iobj
            items.append(iobj)

        tobj['items'] = items

        tobj['graphs'] = extractProtoGraphs(t)
        
        triggers = []
        # nested things too
        for i in t.findall('trigger_prototypes/trigger_prototype'):
            iobj = {}
            for child in i:
                if child.text is None:
                    continue
                if len(child) > 0:
                    continue
                iobj[child.tag] = child.text
                log.debug("lld trigger attr %s: %s" % (child.tag, child.text)) 

            iobj["name_safe"] = safeTfName(iobj["name"])
            triggers.append(iobj)

        tobj['triggers'] = triggers

        rules.append(tobj)
    return rules

def extractTriggers(root):
    triggers = []

    # extract triggers
    for t in root.findall("triggers/trigger"):
        tobj = {}
        for child in t:
            if child.text is None:
                continue
            tobj[child.tag] = child.text
            log.debug("%s: %s" % (child.tag, child.text)) 
        tobj['name_safe'] = safeTfName(tobj['name'])
        triggers.append(tobj)

    log.debug("got triggers %s" % triggers)
    return triggers

def extractGraphCommon(t):
    tobj = {"items": []}
    for child in t:
        if child.text is None:
            continue
        tobj[child.tag] = child.text
        log.debug("%s: %s" % (child.tag, child.text)) 
    tobj['name_safe'] = safeTfName(tobj['name'])

    for i in t.findall("graph_items/graph_item"):
        iobj = {}
        for child in i:
            if child.text is None:
                continue
            iobj[child.tag] = child.text
            log.debug("%s: %s" % (child.tag, child.text)) 
        iobj['item_host'] = i.find('item/host').text
        iobj['item_key'] = i.find('item/key').text
        tobj["items"].append(iobj)
    return tobj

def extractGraphs(root):
    graphs = []

    # extract triggers
    for t in root.findall("graphs/graph"):
        graphs.append(extractGraphCommon(t))

    log.debug("got graphs %s" % graphs)
    return graphs 

def extractProtoGraphs(root):
    graphs = []

    # extract triggers
    for t in root.findall("graph_prototypes/graph_prototype"):
        graphs.append(extractGraphCommon(t))

    log.debug("got graph prototypes %s" % graphs)
    return graphs 

def renderTemplate(t, args):
    if t.get("name") is None:
      t["name"] = t["template"]
    if t.get("description") is None:
      t["description"] = "Imported Template"
    t["hostgroup"] = safeTfName("t")
    print("""
resource "zabbix_template" "{template_safe}" {{
  host = "{template}"
  name = "{name}"

  description = "{description}"
  groups = [ data.zabbix_hostgroup.{hostgroup}.id ]
}}
""".format_map(t))

    # applications
    for i in t["applications"]:
        print("resource \"zabbix_application\" \"{}\" {{".format(safeTfName(i)))
        print("  name = \"{}\"".format(i))
        print('  hostid = zabbix_template.{}.id'.format(t["template_safe"]))
        print("}")

    # now for all items
    for i in t["items"]:
      renderItem(t, i, args)

    # for all discovery rules
    for r in t["rules"]:
        renderLLDRule(t, r, args)

def renderLLDRule(t, i, args):
    lines = []
    common_lines = [
        '  hostid = zabbix_template.{}.id'.format(t["template_safe"]),
        '  name = "{}"'.format(i["name"]),
        '  key = "{}"'.format(i["key"]),
    ]
    ty = i.get("type", "0")
    if ty == "1" or ty == "4" or ty == "6": # snmp
        t['resource_type'] = 'zabbix_lld_snmp'
        lines.append('resource "{}" "{}" {{'.format(t['resource_type'], i['name_safe']))
        lines.extend(common_lines)
        lines.append('  snmp_oid = "{}"'.format(i["snmp_oid"]))
        lines.append('  snmp_version = "{}"'.format(args.snmp))
        lines.append('}')
    else:
        log.error("unsupported discovery type %s" % i)
        sys.exit(1)

    
    print("\n".join(lines))

    # process its items too
    for item in i['items']:
        renderLLDItem(t, i, item, args)
    
    # process its triggers too
    for trigger in i['triggers']:
        renderLLDTrigger(t, i, trigger, args)
    
    # process its graphs
    for graph in i['graphs']:
        renderGraph(graph, 'zabbix_proto_graph')

def renderLLDItem(t, lld, i, args):
    log.info("got lld item %s" % i)
    lines = []
    applications = [ "zabbix_application."+ safeTfName(m)+".id" for m in i["applications"]]
    common_lines = [
       '  hostid = zabbix_template.{}.id'.format(t["template_safe"]),
       '  ruleid = {}.{}.id'.format(t["resource_type"], lld["name_safe"]),
       '  name = "{}"'.format(i["name"]),
       '  key = "{}"'.format(i["key"]),
       '  valuetype = "{}"'.format(ITEM_T_MAP[i["value_type"]]),
    ]
    if len(applications) > 0:
       common_lines.append('  applications = [{}]'.format(", ".join(applications)))

    ty = i.get("type", "0")
    if ty == "0" or ty == "7": # agent
        i['resource_type'] = 'zabbix_proto_item_agent'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        if ty == "7":
            lines.append('  active = true')
        lines.append('}')
    elif ty == "1" or ty == "4" or ty == "6": # snmp
        i['resource_type'] = 'zabbix_proto_item_snmp'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  snmp_oid = "{}"'.format(i["snmp_oid"]))
        lines.append('  snmp_version = "{}"'.format(args.snmp))
        lines.append('}')
    elif ty == "2": # trapper
        i['resource_type'] = 'zabbix_proto_item_trapper'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "3": # simple
        i['resource_type'] = 'zabbix_proto_item_simple'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "5": # internal
        i['resource_type'] = 'zabbix_proto_item_internal'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "8": # aggregate
        i['resource_type'] = 'zabbix_proto_item_aggregate'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "10": # external
        i['resource_type'] = 'zabbix_proto_item_external'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "15": # calculated
        i['resource_type'] = 'zabbix_proto_item_calculated'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  formula = "{}"'.format(i['params'].replace('"','\\"')))
        lines.append('}')
    elif ty == "17": # snmp trap
        i['resource_type'] = 'zabbix_proto_item_snmptrap'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "18": # dependent
        i['resource_type'] = 'zabbix_proto_item_dependent'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  master_itemid = "{}"'.format(i['master_item']))
        lines.append('}')
    elif ty == "19": # http 
        i['resource_type'] = 'zabbix_proto_item_http'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        # need to add extraction here
        lines.append('}')
    else:
        log.warn("unsupported item type: %s" % i)
        return

    print("\n".join(lines))

def renderLLDTrigger(tmpl, lld, t, args):
    log.info("got trigger %s" % t)

    try:
        expression = expressionRef(t["expression"])
        if t.get('recovery_mode') == "1":
            recovery_expression = expressionRef(t["recovery_expression"])
    except KeyError:
        log.error("cant render trigger %s as no valid item found" % t['name'])
        return

    lines = []

    lines.append('resource "zabbix_proto_trigger" "{}" {{'.format(t['name_safe']))
    lines.append('  name = "{}"'.format(t["name"]))
    lines.append('  expression = "{}"'.format(expression))
    if 'description' in t:
        lines.append('  comments = "{}"'.format('\\n'.join(t["description"].splitlines())))
    lines.append('  priority = "{}"'.format(TRIGGER_P_MAP[t["priority"]]))
    if t.get('recovery_mode') == "1":
        lines.append('  recovery_expression = "{}"'.format(recovery_expression))
    lines.append('}')

    print("\n".join(lines))

def renderItem(t, i, args):
    lines = []
    applications = [ "zabbix_application."+ safeTfName(m)+".id" for m in i["applications"]]
    common_lines = [
       '  hostid = zabbix_template.{}.id'.format(t["template_safe"]),
       '  name = "{}"'.format(i["name"]),
       '  key = "{}"'.format(i["key"]),
       '  valuetype = "{}"'.format(ITEM_T_MAP[i["value_type"]]),
    ]
    if len(applications) > 0:
       common_lines.append('  applications = [{}]'.format(", ".join(applications)))

    ty = i.get("type", "0")
    if ty == "0" or ty == "7": # agent
        i['resource_type'] = 'zabbix_item_agent'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        if ty == "7":
            lines.append('  active = true')
        lines.append('}')
    elif ty == "1" or ty == "4" or ty == "6": # snmp
        i['resource_type'] = 'zabbix_item_snmp'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  snmp_oid = "{}"'.format(i["snmp_oid"]))
        lines.append('  snmp_version = "{}"'.format(args.snmp))
        lines.append('}')
    elif ty == "2": # trapper
        i['resource_type'] = 'zabbix_item_trapper'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "3": # simple
        i['resource_type'] = 'zabbix_item_simple'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "5": # internal
        i['resource_type'] = 'zabbix_item_internal'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "8": # aggregate
        i['resource_type'] = 'zabbix_item_aggregate'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "10": # external
        i['resource_type'] = 'zabbix_item_external'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "15": # calculated 
        i['resource_type'] = 'zabbix_item_calculated'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  formula = "{}"'.format(i['params'].replace('"','\\"')))
        lines.append('}')
    elif ty == "17": # snmp trap
        i['resource_type'] = 'zabbix_item_snmptrap'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('}')
    elif ty == "18": # dependent
        i['resource_type'] = 'zabbix_item_dependent'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        lines.append('  master_itemid = "{}"'.format(i['master_item']))
        lines.append('}')
    elif ty == "19": # http 
        i['resource_type'] = 'zabbix_item_http'
        lines.append('resource "{}" "{}" {{'.format(i['resource_type'], i['key_safe']))
        lines.extend(common_lines)
        # need to add extraction here
        lines.append('}')
    else:
        log.warn("unsupported item type: %s" % i)
        return

    print("\n".join(lines))

def renderTrigger(t):
    lines = []

    try:
        expression = expressionRef(t["expression"])
        if t.get('recovery_mode') == "1":
            recovery_expression = expressionRef(t["recovery_expression"])
    except KeyError:
        log.error("cant render trigger %s as no valid item found" % t['name'])
        return

    # need to fix expression to ref both template and items for dependencies
    lines.append('resource "zabbix_trigger" "{}" {{'.format(t['name_safe']))
    lines.append('  name = "{}"'.format(t["name"]))
    lines.append('  expression = "{}"'.format(expression))
    if 'description' in t:
        lines.append('  comments = "{}"'.format('\\n'.join(t["description"].splitlines())))
    lines.append('  priority = "{}"'.format(TRIGGER_P_MAP[t["priority"]]))
    if t.get('recovery_mode') == "1":
        lines.append('  recovery_expression = "{}"'.format(recovery_expression))
    lines.append('}')

    print("\n".join(lines))

def renderGraph(t, resource_type):
    lines = []

    lines.append('resource "{}" "{}" {{'.format(resource_type, t['name_safe']))
    lines.append('  name = "{}"'.format(t["name"]))
    lines.append('  height = "{}"'.format(t["height"]))
    lines.append('  width = "{}"'.format(t["width"]))
    lines.append('  type = "{}"'.format(GRAPH_T_MAP[t["type"]]))
    # type
    lines.append('  percent_left = "{}"'.format(t["percent_left"]))
    lines.append('  percent_right = "{}"'.format(t["percent_right"]))
    lines.append('  do3d = {}'.format("true" if t["show_3d"] == "1" else "false"))
    lines.append('  legend = {}'.format("true" if t["show_legend"] == "1" else "false"))
    lines.append('  work_period = {}'.format("true" if t["show_work_period"] == "1" else "false"))
    
    lines.append('  ymax = "{}"'.format(t["yaxismax"]))
    lines.append('  ymax_itemid = "{}"'.format(t["ymax_item_1"]))
    lines.append('  ymax_type = "{}"'.format(GRAPH_A_MAP[t["ymax_type_1"]]))
    lines.append('  ymin = "{}"'.format(t["yaxismin"]))
    lines.append('  ymin_itemid = "{}"'.format(t["ymin_item_1"]))
    lines.append('  ymin_type = "{}"'.format(GRAPH_A_MAP[t["ymin_type_1"]]))

    for i in t["items"]:
        lines.append('')
        lines.append('  item {')
        lines.append('    color = "{}"'.format(i["color"]))
        lines.append('    sortorder = "{}"'.format(i["sortorder"]))

        itm = item_cache.get(i["item_key"])
        if itm:
            lines.append('    itemid = {}.{}.id'.format(itm["resource_type"], itm["key_safe"]))
        else:
            log.error("cant render graph, no item ref, leaving blank, user needs to fill")
            lines.append('    # INPUT NEEDED')
            lines.append('    itemid = ""')

        lines.append('    function = "{}"'.format(GRAPH_F_MAP[i["calc_fnc"]]))
        lines.append('    drawtype = "{}"'.format(GRAPH_D_MAP[i["drawtype"]]))
        lines.append('    type = "{}"'.format(GRAPH_IT_MAP[i["type"]]))
        lines.append('    yaxis_side = "{}"'.format(GRAPH_S_MAP[i["yaxisside"]]))
        lines.append('  }')
    lines.append('}')

    print("\n".join(lines))

def main():
    logging.basicConfig(level=logging.INFO)
    global log
    log = logging.getLogger(__name__)

    parser = argparse.ArgumentParser()
    parser.add_argument('-D', '--debug', action='store_true', default=False, help='debug logging')
    parser.add_argument('-i', '--input', required=True, help='input xml file')
    parser.add_argument('-s', '--snmp', required=True, help='snmp version')
    parser.add_argument('-p', '--prefix', help='tf resource name prefix')

    args = parser.parse_args()

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
        log.debug("debug logs enabled")
    if args.prefix:
        state["prefix"] = args.prefix


    tree = ET.parse(args.input)
    root = tree.getroot()
    log.debug("got xml: %s" % root)

    templates = extractTemplates(root)
    triggers = extractTriggers(root)
    graphs = extractGraphs(root)

    print("""
data "zabbix_hostgroup" "{}" {{
    name = "Templates"
}}
    """.format(safeTfName("t")))

    for t in templates:
      renderTemplate(t, args)

    for t in triggers:
      renderTrigger(t)
    
    for t in graphs:
      renderGraph(t, 'zabbix_graph')


if __name__ == "__main__":
    main()
