#!/usr/bin/env python
# -*- coding: utf-8 -*-


import ctypes
import logging
import os
import subprocess
import sys
import tempfile
import time
import urllib2


try:
    import win32con
    import win32api
    import win32evtlogutil
    PYWIN32_AVAILABLE = True
except ImportError:

    PYWIN32_AVAILABLE = False



DCU_INSTALLER_URL = "https://script-downloads.itarian.com/Dell_Command_Update.EXE"


CONFIG = {
    "dcu_paths": [
        r"C:\Program Files\Dell\CommandUpdate\dcu-cli.exe",
        r"C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe",
    ],
    "log_dir":          r"C:\ProgramData\DellCommandUpdate\Logs",
    "log_file":         "DCU-AutoUpdate.log",
    "max_log_size_mb":  5,
    "event_log_name":   "Application",
    "event_source":     "DellCommandUpdate",
    "alert_event_id":   1001,
    "success_event_id": 1000,
    "suspend_bitlocker": True,
    "bitlocker_drive":   os.environ.get("SystemDrive", "C:"),
    "dcu_config_args":  [
        "/configure",
        "-silent",
        "-autoSuspendBitLocker=disable",
        # NOTE: -reboot is NOT a valid /configure option (only /applyUpdates).
        # Including it makes the whole command fail with code 106.
    ],
    "dcu_apply_args":   [
        "/applyUpdates",
        "-reboot=disable",
        "-silent",
    ],
    "download_timeout_secs":  120,
    "download_chunk_bytes":   8192,
}


EVENTLOG_ERROR_TYPE       = 0x0001
EVENTLOG_WARNING_TYPE     = 0x0002
EVENTLOG_INFORMATION_TYPE = 0x0004


INSTALLER_OK_CODES = (0, 2, 3010)


DCU_SUCCESS_CODE = 0        # completed successfully
DCU_REBOOT_CODES = (1, 5)   # 1 = reboot required, 5 = reboot pending from prior op
DCU_NO_UPDATES   = 500      # scan found nothing applicable - system is up to date

DCU_CODE_DESC = {
    0:    "Success.",
    1:    "Success - reboot required to complete updates.",
    2:    "Unknown application error.",
    3:    "System manufacturer is not Dell (DCU only runs on Dell systems).",
    4:    "CLI not launched with administrative privilege.",
    5:    "Success - reboot pending from a previous operation.",
    6:    "Another instance of Dell Command Update is already running.",
    7:    "System model not supported by the catalog.",
    8:    "No update filters configured.",
    500:  "No applicable updates found - system is up to date.",
    501:  "Error determining available updates during scan.",
    502:  "Scan operation was canceled.",
    503:  "Download error during scan - check network connectivity.",
    1000: "Error retrieving the result of the apply-updates operation.",
    1001: "Apply-updates operation was canceled.",
    1002: "Download error during apply - check network connectivity.",
}


def describe_dcu_code(code):
    return DCU_CODE_DESC.get(code, "Undocumented DCU return code.")


CATALOG_FAIL_MARKERS = (
    "error while downloading",
    "downloading the necessary catalog",
    "connect to your network",
    "network settings",
    "unable to connect",
)


def _looks_like_catalog_failure(output):
    text = (output or "").lower()
    return any(marker in text for marker in CATALOG_FAIL_MARKERS)


def classify_dcu_result(code, output=""):
    """Return one of: 'success', 'reboot', 'noop', 'network_fail', 'fail'."""

    if code == DCU_NO_UPDATES and _looks_like_catalog_failure(output):
        return "network_fail"
    if code == DCU_SUCCESS_CODE:
        return "success"
    if code in DCU_REBOOT_CODES:
        return "reboot"
    if code == DCU_NO_UPDATES:
        return "noop"
    return "fail"


def to_script_exit(outcome):
    """0 = pass (success / reboot / no-updates), 1 = real failure."""
    return 1 if outcome in ("fail", "network_fail") else 0


def _setup_logger():
    log_dir  = CONFIG["log_dir"]
    log_path = os.path.join(log_dir, CONFIG["log_file"])

    if not os.path.exists(log_dir):
        os.makedirs(log_dir)

    if os.path.exists(log_path):
        size_mb = os.path.getsize(log_path) / (1024.0 * 1024.0)
        if size_mb >= CONFIG["max_log_size_mb"]:
            archive = log_path.replace(
                ".log",
                "_{ts}.log".format(ts=time.strftime("%Y%m%d_%H%M%S"))
            )
            os.rename(log_path, archive)

    logger = logging.getLogger("DCU")
    logger.setLevel(logging.DEBUG)

    fmt = logging.Formatter(
        fmt="[%(asctime)s] [%(levelname)-7s] %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    fh = logging.FileHandler(log_path, encoding="utf-8")
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(fmt)
    logger.addHandler(fh)


    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.DEBUG)
    ch.setFormatter(fmt)
    logger.addHandler(ch)

    return logger


LOG = _setup_logger()


def _system_tool(name):

    windir    = os.environ.get("SystemRoot", r"C:\Windows")
    sysnative = os.path.join(windir, "SysNative", name)
    if os.path.exists(sysnative):
        return sysnative
    return os.path.join(windir, "System32", name)


def _ensure_event_source():
    key_path = (
        r"SYSTEM\CurrentControlSet\Services\EventLog\Application\\"
        + CONFIG["event_source"]
    )
    try:
        key = win32api.RegOpenKey(win32con.HKEY_LOCAL_MACHINE, key_path)
        win32api.RegCloseKey(key)
    except win32api.error:
        win32evtlogutil.AddSourceToRegistry(
            appName=CONFIG["event_source"],
            msgDLL=None,
            eventLogType=CONFIG["event_log_name"],
        )
        LOG.info("Registered Windows Event Log source: %s", CONFIG["event_source"])


def write_event(message, event_id, event_type=EVENTLOG_ERROR_TYPE):
    if not PYWIN32_AVAILABLE:
        return
    try:
        _ensure_event_source()
        win32evtlogutil.ReportEvent(
            appName=CONFIG["event_source"],
            eventID=event_id,
            eventType=event_type,
            strings=[message],
        )
        LOG.info("Event Log entry written (ID=%d).", event_id)
    except Exception as exc:
        LOG.warning("Failed to write Event Log entry: %s", exc)


def check_admin():
    try:
        return ctypes.windll.shell32.IsUserAnAdmin() != 0
    except Exception:
        return False


def check_python_version():
    return sys.version_info[:2] == (2, 7)


def run_preflight():
    issues  = []
    notices = []

    ver = "{0}.{1}.{2}".format(*sys.version_info[:3])
    if not check_python_version():
        issues.append("Python 2.7 required; running {0}.".format(ver))
    else:
        notices.append("Python version: {0} - OK".format(ver))

    if not check_admin():
        issues.append(
            "Script must run as Administrator or SYSTEM. "
            "Check the procedure 'Run As' setting in your RMM console."
        )
    else:
        notices.append("Privilege check: Administrator/SYSTEM - OK")

    if PYWIN32_AVAILABLE:
        notices.append("pywin32: available - Event Log alerting enabled.")
    else:
        notices.append("pywin32: not present - Event Log alerting skipped (OK).")

    if not DCU_INSTALLER_URL or "your-server.com" in DCU_INSTALLER_URL:
        issues.append("DCU_INSTALLER_URL is not configured.")
    else:
        notices.append("Installer URL: {0} - OK".format(DCU_INSTALLER_URL))

    for msg in notices:
        LOG.info("[Pre-flight] %s", msg)
    for msg in issues:
        LOG.warning("[Pre-flight] %s", msg)

    return len(issues) == 0, issues


def download_file(url, dest_path):
    LOG.info("Downloading: %s", url)
    LOG.info("Destination: %s", dest_path)

    try:
        response = urllib2.urlopen(url, timeout=CONFIG["download_timeout_secs"])
    except urllib2.URLError as exc:
        LOG.error("Download failed - could not reach URL: %s", exc)
        return False
    except Exception as exc:
        LOG.error("Download failed - unexpected error: %s", exc)
        return False

    content_length = response.info().getheader("Content-Length")
    if content_length:
        LOG.info("File size: %.1f MB", int(content_length) / (1024.0 * 1024.0))
    else:
        LOG.info("File size: unknown (no Content-Length header)")

    try:
        bytes_written = 0
        chunk_size    = CONFIG["download_chunk_bytes"]
        with open(dest_path, "wb") as f:
            while True:
                chunk = response.read(chunk_size)
                if not chunk:
                    break
                f.write(chunk)
                bytes_written += len(chunk)
        LOG.info("Download complete - %.2f MB written.",
                 bytes_written / (1024.0 * 1024.0))
        return True
    except IOError as exc:
        LOG.error("Failed to write installer to disk: %s", exc)
        return False


def install_dcu(installer_path):

    install_log = os.path.join(CONFIG["log_dir"], "dcu-install.log")
    LOG.info("Running DCU installer silently: %s", installer_path)


    try:
        proc = subprocess.Popen(
            [installer_path, "/s", "/l=" + install_log],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            shell=False,
        )
        stdout, stderr = proc.communicate()
        rc = proc.returncode
    except OSError as exc:
        LOG.error("Failed to launch DCU installer: %s", exc)
        return None

    if stdout and stdout.strip():
        LOG.info("[Installer] STDOUT: %s", stdout.strip())
    if stderr and stderr.strip():
        LOG.warning("[Installer] STDERR: %s", stderr.strip())

    LOG.info("[Installer] Exit code: %d", rc)
    if rc in INSTALLER_OK_CODES:
        LOG.info("Installer reported success (code %d).", rc)
    else:
        LOG.warning(
            "Installer returned code %d (see %s). Will verify dcu-cli.exe "
            "before deciding success.", rc, install_log
        )
    return rc


def cleanup_installer(path):
    try:
        if os.path.exists(path):
            os.remove(path)
            LOG.info("Temporary installer removed: %s", path)
    except OSError as exc:
        LOG.warning("Could not remove temporary installer: %s", exc)


def get_dcu_path():
    for path in CONFIG["dcu_paths"]:
        if os.path.isfile(path):
            return path
    return None


def ensure_dcu_installed():
    existing = get_dcu_path()
    if existing:
        LOG.info("DCU already installed - skipping download: %s", existing)
        return existing

    LOG.info("DCU not found - downloading installer from hosted URL...")
    installer_path = os.path.join(
        tempfile.gettempdir(), "Dell-Command-Update-Setup.exe"
    )

    if not download_file(DCU_INSTALLER_URL, installer_path):
        cleanup_installer(installer_path)
        return None

    rc = install_dcu(installer_path)
    cleanup_installer(installer_path)


    dcu_exe = get_dcu_path()
    if dcu_exe:
        LOG.info("DCU installation verified (installer code %s): %s", rc, dcu_exe)
        return dcu_exe

    LOG.error(
        "dcu-cli.exe not found after install (installer code %s). Searched: %s",
        rc, " | ".join(CONFIG["dcu_paths"])
    )
    return None


def invoke_dcu(dcu_exe, args, step_name):
    cmd = [dcu_exe] + args
    LOG.info("[%s] Running: %s", step_name, " ".join(cmd))
    try:
        proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
        )
        stdout, stderr = proc.communicate()
        exit_code = proc.returncode
    except OSError as exc:
        LOG.error("[%s] Failed to launch dcu-cli.exe: %s", step_name, exc)
        return 1000, ""  # treat launch failure as a real failure code

    output = (stdout or "") + (stderr or "")

    if stdout and stdout.strip():
        LOG.info("[%s] STDOUT: %s", step_name, stdout.strip())
    if stderr and stderr.strip():
        LOG.warning("[%s] STDERR: %s", step_name, stderr.strip())

    LOG.info("[%s] Exit code: %d (%s)", step_name, exit_code,
             describe_dcu_code(exit_code))
    return exit_code, output


def _run_manage_bde(args):
    cmd = [_system_tool("manage-bde.exe")] + args
    try:
        proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
        )
        stdout, stderr = proc.communicate()
        return proc.returncode, stdout, stderr
    except OSError as exc:
        return -1, "", str(exc)


def is_bitlocker_active(drive):
    rc, stdout, _ = _run_manage_bde(["-status", drive])
    if rc != 0:
        return False
    return "Protection On" in stdout


def suspend_bitlocker(drive):
    if not CONFIG["suspend_bitlocker"]:
        LOG.info("BitLocker suspension disabled in config - skipping.")
        return False
    if not is_bitlocker_active(drive):
        LOG.info("BitLocker not active on %s - no suspension needed.", drive)
        return False
    rc, _, stderr = _run_manage_bde(
        ["-protectors", "-disable", drive, "-RebootCount", "1"]
    )
    if rc == 0:
        LOG.info("BitLocker suspended on %s (auto-resumes next boot).", drive)
        return True
    LOG.warning("Failed to suspend BitLocker on %s (rc=%d): %s", drive, rc, stderr)
    return False


def resume_bitlocker(drive, was_suspended):
    if not was_suspended:
        return
    rc, _, stderr = _run_manage_bde(["-protectors", "-enable", drive])
    if rc == 0:
        LOG.info("BitLocker protection resumed on %s.", drive)
    else:
        LOG.warning("Failed to resume BitLocker on %s (rc=%d): %s",
                    drive, rc, stderr)


def _log_footer(script_exit):
    result = "PASS" if script_exit == 0 else "FAIL"
    LOG.info("=" * 72)
    LOG.info("Session complete. Exit code: %d (%s)", script_exit, result)
    LOG.info("=" * 72)
    try:
        sys.stdout.flush()
    except Exception:
        pass



def main():
    sep      = "=" * 72
    hostname = os.environ.get("COMPUTERNAME", "unknown")
    username = os.environ.get("USERNAME",     "SYSTEM")
    domain   = os.environ.get("USERDOMAIN",   hostname)
    log_path = os.path.join(CONFIG["log_dir"], CONFIG["log_file"])

    LOG.info(sep)
    LOG.info("Dell Command Update - Automated Update Session started.")
    LOG.info("Host: %s  |  Account: %s\\%s", hostname, domain, username)
    LOG.info("Log : %s", log_path)
    LOG.info(sep)

    # Step 0: Pre-flight
    LOG.info("Step 0/5 - Running pre-flight checks...")
    passed, issues = run_preflight()
    if not passed:
        msg = "Pre-flight checks FAILED on {0}. Issues: {1}".format(
            hostname, " | ".join(issues))
        LOG.error(msg)
        write_event(msg, CONFIG["alert_event_id"], EVENTLOG_ERROR_TYPE)
        LOG.info("STATUS: FAILED - pre-flight")
        _log_footer(1)
        return 1
    LOG.info("Pre-flight checks passed.")

    # Step 1: Ensure DCU is installed
    LOG.info("Step 1/5 - Checking Dell Command Update installation...")
    dcu_exe = ensure_dcu_installed()
    if dcu_exe is None:
        msg = ("Dell Command Update could not be installed on {0}. URL: {1}"
               .format(hostname, DCU_INSTALLER_URL))
        LOG.error(msg)
        write_event(msg, CONFIG["alert_event_id"], EVENTLOG_ERROR_TYPE)
        LOG.info("STATUS: FAILED - install")
        _log_footer(1)
        return 1

    # Step 2: Configure DCU (non-fatal)
    LOG.info("Step 2/5 - Configuring DCU (silent, no auto-reboot)...")
    cfg_exit, _ = invoke_dcu(dcu_exe, CONFIG["dcu_config_args"], "Configure")
    if cfg_exit == 0:
        LOG.info("DCU configured successfully.")
    else:
        LOG.warning("DCU /configure returned %d (%s) - proceeding anyway.",
                    cfg_exit, describe_dcu_code(cfg_exit))

    # Step 3: Suspend BitLocker
    LOG.info("Step 3/5 - Suspending BitLocker on %s...", CONFIG["bitlocker_drive"])
    bl_suspended = suspend_bitlocker(CONFIG["bitlocker_drive"])

    # Step 4: Apply updates
    LOG.info("Step 4/5 - Applying updates (no forced reboot)...")
    dcu_exit, dcu_out = invoke_dcu(dcu_exe, CONFIG["dcu_apply_args"], "ApplyUpdates")

    # Step 5: Resume BitLocker
    LOG.info("Step 5/5 - Resuming BitLocker...")
    resume_bitlocker(CONFIG["bitlocker_drive"], bl_suspended)

    # Report
    outcome     = classify_dcu_result(dcu_exit, dcu_out)
    script_exit = to_script_exit(outcome)
    desc        = describe_dcu_code(dcu_exit)

    if outcome == "success":
        LOG.info("Updates applied successfully on %s.", hostname)
        write_event("DCU PASSED on {0}. Updates applied (code 0).".format(hostname),
                    CONFIG["success_event_id"], EVENTLOG_INFORMATION_TYPE)
        LOG.info("STATUS: SUCCESS")
    elif outcome == "reboot":
        LOG.info("Updates applied on %s - REBOOT REQUIRED (code %d).",
                 hostname, dcu_exit)
        write_event("DCU PASSED on {0}. Reboot required (code {1}).".format(
                    hostname, dcu_exit),
                    CONFIG["success_event_id"], EVENTLOG_INFORMATION_TYPE)
        LOG.info("Schedule the reboot via your patch tooling or a separate procedure.")
        LOG.info("STATUS: SUCCESS-REBOOT-REQUIRED")
    elif outcome == "noop":
        LOG.info("No applicable updates on %s - system is up to date (code 500).",
                 hostname)
        write_event("DCU NOOP on {0}. System up to date.".format(hostname),
                    CONFIG["success_event_id"], EVENTLOG_INFORMATION_TYPE)
        LOG.info("STATUS: NOOP - no applicable updates")
    elif outcome == "network_fail":
        alert = ("DCU could not reach Dell's catalog servers on {0} "
                 "(code 500 with a download/network error - NOT up to date). "
                 "Check the endpoint's outbound HTTPS access to Dell update "
                 "servers (downloads.dell.com / dl.dell.com), proxy, and "
                 "firewall. Log: {1}".format(hostname, log_path))
        LOG.error("ALERT: %s", alert)
        write_event(alert, CONFIG["alert_event_id"], EVENTLOG_ERROR_TYPE)
        LOG.info("STATUS: FAILED - catalog/network error (could not reach Dell)")
    else:
        alert = ("DCU FAILED on {0}. Code {1} ({2}). Log: {3}".format(
                 hostname, dcu_exit, desc, log_path))
        LOG.error("ALERT: %s", alert)
        write_event(alert, CONFIG["alert_event_id"], EVENTLOG_ERROR_TYPE)
        LOG.info("STATUS: FAILED - dcu code %d (%s)", dcu_exit, desc)

    _log_footer(script_exit)
    return script_exit


def _entrypoint():
    try:
        return main()
    except Exception as exc:
        host  = os.environ.get("COMPUTERNAME", "unknown")
        fatal = "Unhandled exception on {0}: {1}".format(host, exc)
        LOG.error(fatal)
        write_event(fatal, CONFIG["alert_event_id"], EVENTLOG_ERROR_TYPE)
        LOG.info("STATUS: ERROR - %s", exc)
        _log_footer(1)
        return 1

_entrypoint()