#!/usr/bin/env python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2014             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk is free software;  you can redistribute it and/or modify it
# under the  terms of the  GNU General Public License  as published by
# the Free Software Foundation in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

###################################################
# IMPORTANT: THIS FILE EXISTS TWICE
# ANY CHANCES HERE ARE ALSO REQUIRED IN THE COUNTERPART
# check_mk.git ~/share/check_mk/agents/windows/msibuild
# cmk-omd.git  ~/packages/msitools
####################################################

import os, sys, uuid, tempfile, shutil, re

from cmk.utils import msi_patch

# Explicitly check for Python 3 (which is understood by mypy)
if sys.version_info[0] >= 3:
    from pathlib import Path  # pylint: disable=import-error
else:
    from pathlib2 import Path


def verbose(text):
    if opt_verbose:
        sys.stdout.write(text + "\n")


def bail_out(text):
    sys.stderr.write("ERROR: %s\n" % text)
    sys.exit(1)


def _remove_cab(path_to_msibuild, new_msi_filename):
    verbose("Removing product.cab from %s" % new_msi_filename)
    if os.system(  # nosec
        (path_to_msibuild +
         "msibuild %(new_msi_file)s -q \"DELETE FROM _Streams where Name = 'product.cab'\"") %
        {"new_msi_file": new_msi_filename}) != 0:
        bail_out('Fehler!')


def _create_new_cab(working_dir):
    verbose("Generating new product.cab")

    lcab_files = ""
    for lcab_file in ["check_mk_ini", "checkmk.dat", "plugins_cap"]:
        lcab_files += "%(source_dir)s/%(lcab_file)s " % {
            "source_dir": source_dir,
            "lcab_file": lcab_file
        }
    if os.system(  # nosec
            "lcab -n %(lcab_files)s %(work_dir)s/product.cab" % {
                "lcab_files": lcab_files,
                "work_dir": working_dir
            }) != 0:
        bail_out('Fehler!')


def _add_cab(path_to_msibuild, new_msi_filename, working_dir):
    verbose("Add modified product.cab")
    if os.system(  # nosec
        (path_to_msibuild + "msibuild %(new_msi_file)s -a product.cab %(work_dir)s/product.cab") % {
            "work_dir": working_dir,
            "new_msi_file": new_msi_filename
        }) != 0:
        bail_out('Fehler!')


def _update_package_code(msi_filename):
    package_code = ("{%s}" % uuid.uuid1()).upper()
    summary_info = {
        "new_msi_file": new_msi_file,
        "start": package_code[:16],
        "end": package_code[16:]
    }
    default_package_codes = {
        # left as example for possible control
        "vanilla_container.msi": {
            "dftl_start": "{08012468-53DE-4",
            "dftl_end": "2A3-BAA9-FE4C2A129135}"
        },
        # used in production
        "check_mk_agent.msi": {
            "dftl_start": "{BAEBF560-7308-4",
            "dftl_end": "D53-B426-903EA74B1D7E}"
        },
        # may be used in production(just other name)
        "baked_container.msi": {
            "dftl_start": "{BAEBF560-7308-4",
            "dftl_end": "D53-B426-903EA74B1D7E}"
        },
        # used for a testing
        "check_mk_agent_update.msi": {
            "dftl_start": "{BAEBF560-7308-4",
            "dftl_end": "D53-B426-903EA74B1D7E}"
        }
    }
    summary_info.update(default_package_codes[os.path.basename(msi_filename)])
    if os.system(  # nosec
            "sed -i -e 's/%(dftl_start)s/%(start)s/' -e 's/%(dftl_end)s/%(end)s/' %(new_msi_file)s"
            % summary_info) != 0:
        bail_out('Fehler!')


def update_package_code(new_msi_file):
    if not msi_patch.patch_package_code_by_marker(new_msi_file):
        raise Exception("Failed to patch package code")


def _make_msi_copy(msi_file, new_msi_file):
    verbose("Creating copy of original file %s -> %s" % (msi_file, new_msi_file))

    # Make a copy
    if os.system("cp %(msi_file)s %(new_msi_file)s" % {  # nosec
            "msi_file": msi_file,
            "new_msi_file": new_msi_file
    }) != 0:
        bail_out('Fehler!')


try:
    if sys.argv[1] == '-v':
        opt_verbose = True
        del sys.argv[1]
    else:
        opt_verbose = False

    # MSI container to modify
    msi_file = sys.argv[1]
    if msi_file.find("baked_container.msi"):
        verbose("changing baked_container.msi to check_mk_agent.msi")
        msi_file = msi_file.replace("baked_container.msi",
                                    "check_mk_agent.msi")  # The same container for all cases

    # Directory where the sources are contained
    source_dir = sys.argv[2]

    # Revision (from build_version)
    revision = sys.argv[3]

    # TODO: complete overhaul of version generation
    # Official version name, e.g
    # 1.2.5i4p1
    # 2015.04.12
    # 1.2.6-2015.04.12
    new_version_name = sys.argv[4]
    major, minor, build = 1, 0, 0
    try:
        major, minor, build = map(lambda x: x.lstrip("0"),
                                  new_version_name.split("-")[0].split(".")[:3])
        build = build == "" and "0" or build
        if len(major) > 3:
            # Looks like a daily build.. 2015.03.05
            major = major[2:].lstrip("0")
    except:
        pass
    new_product_version = "%s.%s.%s" % (major, minor, build)

    # Remove any traces of i, p, b versions. Windows can't handle them...
    # The revision should be enough to uniquely identify this build
    # The original version name is also still visible in the list of programs
    match = re.search("[a-z]", new_product_version)
    if match:
        new_version_build = new_product_version[:match.start(0)]
        if new_version_build[-1] == ".":
            new_version_build += "0"
        new_version_build += ".%s" % revision
    else:
        new_version_build = "%s.%s" % (new_product_version, revision)

except:
    bail_out("Usage: %s MSIFILE.msi SOURCEDIR BUILDNUMBER VERSION" % sys.argv[0])

try:
    if "OMD_ROOT" in os.environ:
        path_prefix = ""
        tmp_dir = os.environ["OMD_ROOT"] + "/tmp"
    else:
        path_prefix = "./"
        tmp_dir = "."

    new_msi_file = "check_mk_agent.msi"
    work_dir = tempfile.mkdtemp(prefix=tmp_dir + "/msi-update.")

    # When this script is run in the build environment then we need to specify
    # paths to the msitools. When running in an OMD site, these tools are in
    # our path

    # Export required idt files into work dir
    for entry in ["File", "Property", "Component"]:
        verbose("Export table %s from file %s" % (entry, msi_file))
        command = (path_prefix +
                   "msiinfo export %(msi_file)s %(property)s > %(work_dir)s/%(property)s.idt") % {
                       "work_dir": work_dir,
                       "msi_file": msi_file,
                       "property": entry
                   }
        if os.system(command) != 0:  # nosec
            bail_out('Failed to unpack msi table %s from %s' % (entry, msi_file))

    verbose("Modify extracted files..")

    # ==============================================
    # Modify File.idt

    # Convert Input Files to Internal-MSI Presentation
    shutil.copy(source_dir + "/check_mk.ini", source_dir + "/check_mk_ini")
    shutil.copy(source_dir + "/plugins.cap", source_dir + "/plugins_cap")
    # checkmk.dat is the same, no need to copy

    lines_file_idt = file(work_dir + "/File.idt").readlines()
    file_idt_new = file(work_dir + "/File.idt.new", "w")
    file_idt_new.write("".join(lines_file_idt[:3]))

    cabinet_files = []
    for line in lines_file_idt[3:]:
        tokens = line.split("\t")
        filename = tokens[0]
        cabinet_files.append((tokens[-1], filename))
        for f in ["check_mk_ini", "checkmk.dat", "plugins_cap"]:  # sorted(cabinet_files):
            if f == filename:
                file_stats = os.stat(source_dir + "/" + filename)
                new_size = file_stats.st_size
                tokens[3] = str(new_size)
                break
        # The version of this file is different from the msi installer version !
        tokens[4] = tokens[4] and new_version_build or ""
        file_idt_new.write("\t".join(tokens))
    file_idt_new.close()
    # ==============================================

    # ==============================================
    # Modify Component.idt
    lines_component_idt = file(work_dir + "/Component.idt").readlines()
    component_idt_new = file(work_dir + "/Component.idt.new", "w")
    component_idt_new.write("".join(lines_component_idt[:3]))

    for line in lines_component_idt[3:]:
        tokens = line.split("\t")
        if tokens[0] in ["check_mk_ini_", "checkmk.dat", "plugins_cap_"]:
            tokens[1] = ("{%s}" % uuid.uuid1()).upper()
        component_idt_new.write("\t".join(tokens))

    component_idt_new.close()

    # ==============================================

    # ==============================================
    # Modify Property.idt
    product_code = ("{%s}\r\n" % uuid.uuid1()).upper()
    lines_property_idt = file(work_dir + "/Property.idt").readlines()
    property_idt_new = file(work_dir + "/Property.idt.new", "w")
    property_idt_new.write("".join(lines_property_idt[:3]))

    for line in lines_property_idt[3:]:
        tokens = line.split("\t")
        if tokens[0] == "ProductName":
            tokens[1] = "Check MK Agent 1.6\r\n"
        # The upgrade code defines the product family. Do not change it!
        #    elif tokens[0] == "UpgradeCode":
        #        tokens[1] = upgrade_code
        elif tokens[0] == "ProductCode":
            tokens[1] = product_code
        elif tokens[0] == "ProductVersion":
            tokens[1] = "%s\r\n" % ".".join(new_version_build.split(".")[:4])
        property_idt_new.write("\t".join(tokens))
    property_idt_new.close()
    # ==============================================

    _make_msi_copy(msi_file, new_msi_file)

    # Rename modified tables
    for entry in ["Property", "File", "Component"]:
        os.rename(work_dir + "/%s.idt.new" % entry, work_dir + "/%s.idt" % entry)

    for entry in ["Property", "File", "Component"]:
        if os.system(  # nosec
            (path_prefix + "msibuild %(new_msi_file)s -i %(work_dir)s/%(file)s.idt") % {
                "work_dir": work_dir,
                "new_msi_file": new_msi_file,
                "file": entry
            }) != 0:
            bail_out('Fehler!')

    # Update summary info with new uuid (HACK! - the msibuild tool is not able to do this on all systems)
    # In this step we replace the package code with a new uuid. This uuid is important, because it is
    # the unqiue identifier for this package. Inside the package the uuid is split into two halfs.
    # Each of it is updated with the corresponding new package code. Since msi-update uses the
    # vanilla_container.msi as well as the baked_container.msi we need to handle both..
    update_package_code(new_msi_file)

    # _update_package_code(msi_file)

    # Remove original product.cab from stream
    _remove_cab(path_prefix, new_msi_file)

    # Prepare product.cab file
    _create_new_cab(work_dir)

    # Add modified product.cab
    _add_cab(path_prefix, new_msi_file, work_dir)

    shutil.rmtree(work_dir)
    verbose("Successfully created file " + new_msi_file)
except Exception, e:
    if work_dir and os.path.exists(work_dir):
        shutil.rmtree(work_dir)
    bail_out("Error on creating msi file: %s" % str(e))
