#!/usr/bin/python3

"""This script checks every node at the Berlin Freifunk network for its
current firmware version. If the version does not match the latest stable
release, the script tries to contact its operator.

There are two supported ways of doing this:
    + directly via E-Mail
    + by contact-formular at config.berlin.freifunk.next

The latter way is used, if we detect an http-link in the contact field.
"""

from email.message import EmailMessage
import requests
import argparse
import smtplib
import datetime
import json
import copy
import sys
import re

PASU="/home/martin/router_statistics-fetcher/"

conf_from = 'some@email-address.net'
conf_subj = 'Update für die Freifunk-Firmware'
conf_form_text = 'email_text_form'

date_name = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M_")

def return_decimal(version_string):
    """Returns a decimal number for a stable release. Thus we can compare 
    firmware versions easily. For invalid strings it will return 0.

    Args:
        version_string (str): String in a semantic-versioning format, 
        like 'v1.0.2'. Longer strings which start with the correct versining
        scheme will work too.
    """
    # cut potsdams strings to base-version-format
    if len(version_string) > 8:
        version_string = version_string[:6]

    num = version_string.replace("v", "")
    num = num.replace(".", "")
    try:
        return int(num)
    except:
        return 0


def store_csv(nodes, filepath):
    """Store node-name, fw-version and contact in csv-format.

    Args:
        nodes (list): list of nodes to pe processed. See node-data for structure
        filepath (str): path of the output-file
    """
    if len(nodes) == 0:
        raise Exception("nodes-list was empty!")

    with open(filepath, "w") as f:
        for node in nodes:
            line = node.get("node") + ";" + node.get("firmware") + ";" + node.get("release") + "\n"
            #if node.get("contact") == '':
            #    line += "NONE\n"
            #else:
            #    line += node.get("contact") + "\n"
            f.write(line)


def fetch_data_web(version):
    """Fetches data on nodenames, their fw-version and contact details from OWM. Every node with a more recent firmware that 'version' will be excluded.

    Args:
        version (str): version string in semantic-versioning format (v1.0.2)

    Returns:
        list: list of dictionaries with the relevant node_data
    """
    all_nodes = requests.get("https://hopglass.berlin.freifunk.net/nodes.json")
    if all_nodes.status_code == 200:
        all_nodes = all_nodes.json()
    else:
        sys.stderr.write("Failure on loading nodes.json from server. The response was: " +
                        str(all_nodes.status_code) + "\n")
        exit(1)

    node_list = []
    nodes = all_nodes.get("nodes")

    # fetch json-data and store them locally with date, etc
    node_json = all_nodes
    graph_json = requests.get("https://hopglass.berlin.freifunk.net/graph.json").json()

    with open(PASU+date_name+"nodes.json", "w") as f, open(PASU+date_name+"graphs.json", "w") as g:
        f.write(json.dumps(node_json))
        g.write(json.dumps(graph_json))
        f.close()
        g.close()

    for entry in nodes:
        nodeinfo = entry.get("nodeinfo")

        firmware = nodeinfo.get("software").get("firmware").get("base")
        fw_name = nodeinfo.get("software").get("firmware").get("release")
        
        nodename = nodeinfo.get("hostname")
        #obsf_contact = nodeinfo.get("owner").get("contact")
        # deobsfucate contact, if possible
        #contact = obsf_contact.replace("./-\\.T.", "@", 1)

        node_data = {}
        node_data["node"] = nodename
        node_data["firmware"] = firmware
        node_data["release"] = fw_name
        #node_data["contact"] = contact
        
        node_list.append(node_data)

    return node_list


def fetch_data_csv(filepath, version):
    """fetches data from a csv-input-file. Seperator is a ';'

    Args:
        filepath (str): path of the file
        version (str): version-string. Nodes with a newer firmware-version will be excluded from the returned list

    Returns:
        list: list of dictionaries with the node-data
    """
    node_list = []
    with open(filepath, "r") as f:
        for line in f:
            data = line.split(sep=";")
            node = {}
            node["node"] = data[0]
            node["firmware"] = data[1]
            node["contact"] = data[2]
            if return_decimal(data[1]) >= return_decimal(version):
                continue
            else:
                node_list.append(node)
    return node_list


def print_nodes(nodes):
    """Print nodes to screen in a csv-like fashion.

    Args:
        nodes (list): list of nodes (which are node-directories)
    """
    print("Hostname ; Firmware-Version ; Contact")
    for node in nodes:
        print(node.get("node") + ";" + node.get("firmware") + ";" + node.get("contact"))


def sort_nodes_contact_type(nodes, email_cont, webform_cont, unknown_cont):
    """Sorts the nodes to a nodelist, based on their contact-data.

    Args:
        nodes (list): list of node-dictionaries
        email_cont (list): list of node-dicts, which contain a valid email-address
        webform_cont (list): node-dicts whith a link to config.berlin.freifunk.net-conact-form
        unknown_cont (list): list of nodes, which did not match one of the other categories.
    """
    for node in nodes:
        contact = node.get("contact")
        # sort node in one list
        if re.search("\@", contact):
            email_cont.append(node)
        elif re.search("(http|https)", contact):
            webform_cont.append(node)
        else:
            unknown_cont.append(node)


def sort_nodes_recipient(nodes):
    """Sort a list of nodes with email-recipients for their recipients

    Args:
        nodes (list): list of nodes with email-contact-data

    Returns:
        dict: email-address as keys, list of node-dicts as value
    """
    recipients = {}
    for node in nodes:
        mail = node.get("contact")
        # email is not in dict
        if mail not in recipients:
            recipients[mail] = []
            recipients[mail].append(node)
        # email-address is already in dict
        else:
            recipients[mail].append(node)

    return recipients


def mail_format_nodes(nodes):
    string = "{0:<39} {1:<10}\n".format("Knoten", "Version")
    string += "="*50 + "\n"
    for node in nodes:
        name = "{0:<35}".format(node.get("node"))
        string += "* " + name + "\t" + node.get("firmware") + "\n"
    return string


def send_mails(messages):
    # establish smtp-connection
    """s = smtplib.SMTP('smtp.web.de', port=587)
    s.starttls()
    s.login(username, password)
    s.send_message(msg)
    s.close()"""


def compose_emails(contact_sorted_nodes):
    """gets a dict of node-dicts, sorted for their operators-contact.

    Args:
        contact_sorted_nodes (dict): dictionary with node-dicts. The key is the contact. The value is a list of node-dicts.

    Returns:
        list: list of readily compose message-objects
    """
    # load template
    form = open(conf_form_text).read()
    messages = []
    #print(contact_sorted_nodes)
    
    for contact in contact_sorted_nodes:
        #print(contact_sorted_nodes.get(contact))
        msg = EmailMessage()
        msg["From"] = conf_from
        msg["To"] = contact
        msg["Subject"] = conf_subj

        text = copy.deepcopy(form)
        text = text.replace(r"{Version}", args.version, 1)
        text = text.replace(r"{Knoten}", mail_format_nodes(contact_sorted_nodes.get(contact)), 1)
        msg.set_content(text)

        messages.append(msg)
    return messages


if __name__ == "__main__":

    parser = argparse.ArgumentParser(
        description="Script for sending firmware-upgrade-reminders to operators of Freifnk-Berlin-nodes.")
    #parser.add_argument(
    #    "version", help="provide the latest release. format: v1.0.2")
    parser.add_argument("-a", "--all", action="store_true",
                        help="send mail to all deprecated nodes")
    parser.add_argument("-d", "--exclude-devs", action="store_true",
                        help="exclude nodes with development-images")
    parser.add_argument("-i", "--input", help="Give a csv-file instead of fetching data from OWM")
    parser.add_argument(
        "-o", "--output", help="dump all nodes info into a csv-file")
    parser.add_argument("-s", "--send", action="store_true", help="send notifications to the operators")
    args = parser.parse_args()

    # check version string for right format
    #if re.search("v\d\.\d\.\d$", args.version):
    current_firmware_version = "v9.0.0"
    #else:
    #    print("Version-number does not use the right format!\nPlease give a valid string like: v1.0.2\n")
    #    exit(1)


    if args.input:
        all_nodes = fetch_data_csv(args.input, args.version)
        print("read nodes from " + args.input + "...")
    else:
        all_nodes = fetch_data_web(current_firmware_version)
        print("fetch nodes from web...")
    #print(all_nodes)

    # sort nodes for contact_type
    email = all_nodes
    webform = []
    unknown = []
    #sort_nodes_contact_type(all_nodes, email, webform, unknown)

    # status
    print("fetched " + str(len(email)) + " nodes with e-mail contact.")
    print("\t" + str(len(webform)) + " nodes with contact via webform.")
    print("\t" + str(len(unknown)) + " nodes with none or non-analysable contact data.")

    # store csv of all data
    if True:
        liste = email + webform + unknown
        store_csv(liste, PASU+date_name+"datenauszug.csv")
        print("csv-data stored at ...")

    if args.send:
        # includes several nodes of same contact into one email
        contact_sorted_nodes = sort_nodes_recipient(email)
        mails = compose_emails(contact_sorted_nodes)
        for mail in mails:
            print(mail)

