#!/usr/bin/env python3

# This script check if a new version of malcat is available. If it is, it will be unpacked in malcat's install directory 

# Return code:
#   0 - malcat is up-to-date
#   1 - error 
#   2 - a new version is available, was not installed
#   3 - a new version is available, was installed

import io
import json
import os
import platform
import re
import requests
import sys
import traceback
import zipfile

def get_malcat_dir():
    path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
    if not os.path.exists(os.path.join(path, "metadata.txt")):
        raise ValueError("Could not find malcat root dir, metadata.txt not found")
    return path

def get_activation_infos():
    if platform.system() == "Windows":
        path = os.path.join(get_malcat_dir(), "cache.ini")
    else:
        path = os.path.expanduser(os.path.join("~", ".malcat", "cache.ini"))
    if not os.path.exists(path):
        raise ValueError(f"No cache file found at {path}")
    activation_id = None
    product_id = None
    order_id = None
    license = None
    with open(path, "rt", encoding="utf8") as f:
        for line in f.readlines():
            line = line.strip()
            if line.startswith("Code="):
                code = line[5:]
                m = re.match(r"([0-3])-(?:[a-f0-9]{8})?-(?:[a-f0-9]{8})?-(?:[a-f0-9]{8})?-[a-f0-9]{8}-(\d+)-[0-9A-Fa-f]*-(\d+)-[a-f0-9]{8}", code)
                if not m:
                    raise ValueError("Invalid activation code format")
                product_id = int(m.group(1))
                order_id = int(m.group(2))
                activation_id = int(m.group(3))
            elif line.startswith("Serial="):
                license = line.split("=")[1].strip()
    if activation_id is None:
        raise ValueError("Malcat was never activated on this computer: cannot fetch license or order ID")
    elif license is None:
        raise ValueError("Autoupdate does not work for offline activations")
    return product_id, order_id, license, activation_id

def get_metadata():
    path = os.path.join(get_malcat_dir(), "metadata.txt")
    with open(path, "rt", encoding="utf8") as f:
        return json.load(f)

def get_latest_version():
    r = requests.get("http://malcat.fr/version")
    r.raise_for_status()
    return [int(x) for x in r.content.decode("ascii").strip().split(".")]

def get_latest_version_covered_by_a_license(name, product, license, order_id):
    r = requests.post("https://license.malcat.fr/order/latest", json = {
        "product": product,
        "orderid": order_id,
        "license": license,
        "file": name
    })
    r.raise_for_status()
    data = json.loads(r.content.decode("ascii").strip())
    if not data:
        raise ValueError("Cannot find any valid version for your product")
    return [int(x) for x in data.split(".")]

def ver2str(v):
    return ".".join(map(str, v))

if __name__ == '__main__':

    import optparse
    parser = optparse.OptionParser(usage="usage: %prog", description="""check if a new version of malcat is available. If it is, it will be unpacked in malcat's install directory""")
    parser.add_option("-s", "--silent", dest="silent", action='store_true', help="Non-interactive, use this option when running from scripts") 
    parser.add_option("-c", "--check-only", dest="check_only", action='store_true', help="Don't install Malcat, just check for a new version")
    parser.add_option("-g", "--gui", dest="gui_mode", action='store_true', help="Script is run from GUI: will display the 'please close malcat' messages and wait for user input")
    parser.add_option("-o", "--output", dest="output_dir", action='store', default=get_malcat_dir(), help=f"Where the archive should be unpacked, defaults to malcat dir, i.e. {get_malcat_dir()}")
    options, args = parser.parse_args()

    if platform.system() == "Darwin":
        raise ValueError("Autoupdates not supported for MacOS")

    metadata = get_metadata()
    latest_version = get_latest_version()
    product = metadata["product"]
    if product != 0:
        product, order_id, license, activation_id = get_activation_infos()
        if product == 3 and metadata["product"] == 2:
            metadata["product"] = 3
            metadata["name"] = metadata["name"].replace("_pro", "_oem")
        if product != metadata["product"]:
            raise ValueError("Product ID mismatch")
        my_latest_version = get_latest_version_covered_by_a_license(metadata["name"], product, license, order_id)
    else:
        my_latest_version = latest_version

    current_version = [int(x) for x in metadata["version"].split(".")]
    if current_version >= my_latest_version:
        if not options.silent:
            print(f"You have the latest version ({ver2str(current_version)}). ")
            if latest_version > my_latest_version:
                print(f"There is a newer version ({ver2str(latest_version)}) but it is not covered by any of your licenses.")
        sys.exit(0)

    if not options.silent:
        print(f"A new update is available for you ({ver2str(current_version)} --> {ver2str(my_latest_version)})")
        if latest_version > my_latest_version:
            print(f"There is an even newer version ({ver2str(latest_version)}) but it is not covered by any of your licenses.")

    if options.check_only:
        sys.exit(2)

    if not options.silent:
        print(f"Downloading archive for {ver2str(my_latest_version)} ...", end="")
        sys.stdout.flush()

    if product == 0:
        url = f'https://malcat.fr/all/{ver2str(my_latest_version)}/{metadata["name"]}.zip'
        r = requests.get(url, stream=True)
    else:
        r = requests.post("https://license.malcat.fr/order/download", json = {
            "product": product,
            "orderid": order_id,
            "license": license,
            "file": metadata["name"],
            "version": ver2str(my_latest_version)
            }, stream=True)
    r.raise_for_status()

    archive_bytes = io.BytesIO()
    for data in r.iter_content(2 * 1024 * 1024):
        if not options.silent:
            print(".", end="")
            sys.stdout.flush()
        archive_bytes.write(data)

    if not options.silent:
        print(" OK")

    archive_bytes.seek(0)
    zf = zipfile.ZipFile(archive_bytes, "r")

    if options.gui_mode:
        input("Going to install the new version. Make sur that all your malcat instances are closed and press [Enter] to continue")
    if not options.silent:
        print(f"Extracting archive to {os.path.abspath(options.output_dir)} ... ", end="")
        sys.stdout.flush()
    if not os.path.exists(options.output_dir):
        os.makedirs(options.output_dir)
    zf.extractall(path=options.output_dir)
    if not options.silent:
        print("OK")
        print(f"Malcat successfully updated to {ver2str(my_latest_version)}!")

    sys.exit(3)
