"""WizardKit: Repairs - Windows"""
# vim: sts=2 sw=2 ts=2

import atexit
import logging
import os
import pathlib
import re
import sys
import time

from subprocess       import CalledProcessError, DEVNULL
from typing import Any
from xml.dom.minidom  import parse as xml_parse

from wk.cfg.main      import KIT_NAME_FULL, KIT_NAME_SHORT, WINDOWS_TIME_ZONE
from wk.cfg.repairs   import (
  AUTO_REPAIR_DELAY_IN_SECONDS,
  AUTO_REPAIR_KEY,
  BLEACH_BIT_CLEANERS,
  CUSTOM_POWER_PLAN_DESC,
  CUSTOM_POWER_PLAN_NAME,
  POWER_PLANS,
  POWER_PLAN_SLEEP_TIMEOUTS,
  REG_UAC_DEFAULTS_WIN7,
  REG_UAC_DEFAULTS_WIN10,
  WIDTH,
  )
from wk.exe           import (
  get_procs,
  run_program,
  popen_program,
  wait_for_procs,
  )
from wk.io            import (
  copy_file,
  delete_folder,
  get_path_obj,
  non_clobber_path,
  rename_item,
  )
from wk.kit.tools     import (download_tool, get_tool_path, run_tool)
from wk.log           import (
  format_log_path,
  get_root_logger_path,
  update_log_path,
  )
from wk.os.win        import (
  OS_VERSION,
  get_timezone,
  set_timezone,
  reg_delete_value,
  reg_read_value,
  reg_set_value,
  reg_write_settings,
  disable_service,
  enable_service,
  stop_service,
  )
from wk.std           import (
  GenericError,
  GenericWarning,
  sleep,
  )
from wk.ui            import cli as ui
from wk.ui            import ansi


# STATIC VARIABLES
LOG = logging.getLogger(__name__)
BACKUP_BROWSER_BASE_CMD = (
  get_tool_path('7-Zip', '7z'),
  'a', '-t7z', '-mx=1', '-bso0', '-bse0', '-bsp0',
  )
CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False)
GPUPDATE_SUCCESS_STRINGS = (
  'Computer Policy update has completed successfully.',
  'User Policy update has completed successfully.',
  )
IN_CONEMU = 'ConEmuPID' in os.environ
MENU_PRESETS = ui.Menu()
PROGRAMDATA = os.environ.get('{ALLUSERSPROFILE}', r'C:\ProgramData')
PROGRAMFILES_32 = os.environ.get(
  'PROGRAMFILES(X86)', os.environ.get(
    'PROGRAMFILES', r'C:\Program Files (x86)',
    ),
  )
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:')
WHITELIST = '\n'.join((
  str(CONEMU_EXE),
  fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer.exe',
  fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Desktop.exe',
  fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Note.exe',
  fr'{PROGRAMFILES_32}\TeamViewer\TeamViewer_Service.exe',
  fr'{PROGRAMFILES_32}\TeamViewer\tv_w32.exe',
  fr'{PROGRAMFILES_32}\TeamViewer\tv_x64.exe',
  sys.executable,
  ))
TRY_PRINT = ui.TryAndPrint()
TRY_PRINT.width = WIDTH
TRY_PRINT.verbose = True
for error in ('CalledProcessError', 'FileNotFoundError'):
  TRY_PRINT.add_error(error)


# Auto Repairs
def build_menus(base_menus, title, presets) -> dict[str, ui.Menu]:
  """Build menus, returns dict."""
  menus = {}
  menus['Main'] = ui.Menu(title=f'{title}\n{ansi.color_string("Main Menu", "GREEN")}')

  # Main Menu
  for entry in base_menus['Actions']:
    menus['Main'].add_action(entry.name, entry.details)
  for group in base_menus['Groups']:
    menus['Main'].add_option(group, {'Selected': True})

  # Options
  menus['Options'] = ui.Menu(title=f'{title}\n{ansi.color_string("Options", "GREEN")}')
  for entry in base_menus['Options']:
    menus['Options'].add_option(entry.name, entry.details)
  menus['Options'].add_action('All')
  menus['Options'].add_action('None')
  menus['Options'].add_action('Main Menu', {'Separator': True})
  menus['Options'].add_action('Quit')

  # Run groups
  for group, entries in base_menus['Groups'].items():
    menus[group] = ui.Menu(title=f'{title}\n{ansi.color_string(group, "GREEN")}')
    menus[group].disabled_str = 'Locked'
    for entry in entries:
      menus[group].add_option(entry.name, entry.details)
    menus[group].add_action('All')
    menus[group].add_action('None')
    menus[group].add_action('Select Skipped Entries', {'Separator': True})
    menus[group].add_action('Unlock All Entries')
    menus[group].add_action('Main Menu', {'Separator': True})
    menus[group].add_action('Quit')

  # Initialize main menu display names
  menus['Main'].update()

  # Fix Function references
  for group, menu in menus.items():
    if group not in base_menus['Groups']:
      continue
    for name in menu.options:
      _function = menu.options[name]['Function']
      if isinstance(_function, str):
        menu.options[name]['Function'] = getattr(
          sys.modules[__name__], _function,
          )

  # Update presets
  for group, entries in base_menus['Groups'].items():
    presets['Default'][group] = tuple(
      entry.name for entry in entries if entry.details['Selected']
      )

  # Update presets Menu
  MENU_PRESETS.title = f'{title}\n{ansi.color_string("Load Preset", "GREEN")}'
  MENU_PRESETS.add_option('Default')
  for name in presets:
    MENU_PRESETS.add_option(name)
  MENU_PRESETS.add_option('Custom')
  MENU_PRESETS.add_action('Main Menu')
  MENU_PRESETS.add_action('Quit')
  MENU_PRESETS.update()

  # Done
  return menus


def update_scheduled_task() -> None:
  """Create (or update) scheduled task to start repairs."""
  cmd = [
    'schtasks', '/create', '/f',
    '/sc', 'ONLOGON',
    '/tn', f'{KIT_NAME_FULL}-AutoRepairs',
    '/rl', 'HIGHEST',
    '/tr', fr'C:\Windows\System32\cmd.exe "/C {sys.executable} {sys.argv[0]}"',
    ]
  if IN_CONEMU:
    cmd[-1] = f'{CONEMU_EXE} -run {sys.executable} {sys.argv[0]}'
  run_program(cmd)


def end_session() -> None:
  """End Auto Repairs session."""
  # Remove logon task
  cmd = [
    'schtasks', '/delete', '/f',
    '/tn', f'{KIT_NAME_FULL}-AutoRepairs',
    ]
  try:
    run_program(cmd)
  except CalledProcessError:
    LOG.error("Failed to remove scheduled task or it doesn't exist.")

  # Disable Autologon
  try:
    use_autologon_in_autorepairs = reg_read_value(
      'HKCU', AUTO_REPAIR_KEY, 'Use Autologon',
      )
  except FileNotFoundError:
    # Assuming it isn't being used
    use_autologon_in_autorepairs = False
  if use_autologon_in_autorepairs and is_autologon_enabled():
    run_tool('Sysinternals', 'Autologon', '-accepteula', download=True)
    reg_set_value(
      'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon',
      'AutoAdminLogon', '0', 'SZ',
      )

  # Delete Auto Repairs keys
  try:
    reg_delete_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted')
  except FileNotFoundError:
    LOG.error('Ending repair session but session not started.')
  try:
    cmd = ['reg', 'delete', fr'HKCU\{AUTO_REPAIR_KEY}', '/f']
    run_program(cmd)
  except CalledProcessError:
    LOG.error('Failed to remove Auto Repairs session settings')


def get_entry_settings(group, name) -> dict[str, Any]:
  """Get menu entry settings from the registry, returns dict."""
  key_path = fr'{AUTO_REPAIR_KEY}\{group}\{name}'
  settings = {}
  for value in ('done', 'failed', 'message', 'selected', 'skipped', 'warning'):
    try:
      settings[value.title()] = reg_read_value('HKCU', key_path, value)
    except FileNotFoundError:
      # Ignore and use current settings
      pass

  # Disable previously run or skipped entries
  if settings.get('Done', False) or settings.get('Skipped', False):
    settings['Disabled'] = True

  # Done
  return settings


def init(menus, presets) -> None:
  """Initialize Auto Repairs."""
  session_started = is_session_started()

  # Check if autologon is needed
  if not session_started and is_autologon_enabled():
    LOG.warning('Skipping Autologon to preserve current settings.')
    menus['Options'].options['Use Autologon']['Disabled'] = True
    menus['Options'].options['Use Autologon']['Selected'] = False
    reg_set_value('HKCU', AUTO_REPAIR_KEY, 'Use Autologon', 0, 'DWORD')
    save_selection_settings(menus)

  # Start new session
  if not session_started:
    load_preset(menus, presets, enable_menu_exit=False)
    return

  # Resume session
  load_settings(menus)
  ui.print_info('Resuming session, press CTRL+c to cancel')
  for _x in range(AUTO_REPAIR_DELAY_IN_SECONDS, 0, -1):
    print(f'  {_x} second{"" if _x==1 else "s"} remaining...  \r', end='')
    sleep(1)
  print('')


def init_run(options) -> None:
  """Initialize Auto Repairs Run."""
  update_scheduled_task()
  if options['Kill Explorer']['Selected']:
    atexit.register(start_explorer)
    TRY_PRINT.run('Killing Explorer...', kill_explorer, msg_good='DONE')
  if options['Use Autologon']['Selected'] and not is_autologon_enabled():
    TRY_PRINT.run(
      'Running Autologon...', run_tool,
      'Sysinternals', 'Autologon', '-accepteula',
      download=True, msg_good='DONE',
      )
    if is_autologon_enabled():
      # Update session settings to remove during end_session()
      reg_set_value('HKCU', AUTO_REPAIR_KEY, 'Use Autologon', 1, 'DWORD')
    else:
      # A password wasn't provided or the account doesn't have one
      options['Use Autologon']['Disabled'] = True
      options['Use Autologon']['Selected'] = False
  if options['Sync Clock']['Selected']:
    TRY_PRINT.run(
      'Syncing Clock...', run_tool, 'Neutron', 'Neutron', msg_good='DONE',
      )
  if options['Run RKill']['Selected']:
    TRY_PRINT.run('Running RKill...', run_rkill, msg_good='DONE')


def init_session(options) -> None:
  """Initialize Auto Repairs session."""
  reg_set_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted', 1, 'DWORD')
  reg_set_value('HKCU', AUTO_REPAIR_KEY, 'LogName', get_root_logger_path().stem, 'SZ')

  # Check timezone
  zone = get_timezone()
  msg = (
    'The timezone is currently set to '
    f'{zone}, switch it to {WINDOWS_TIME_ZONE}?'
    )
  if zone != WINDOWS_TIME_ZONE and ui.ask(msg):
    set_timezone(WINDOWS_TIME_ZONE)

  # One-time tasks
  TRY_PRINT.run(
    'Run AVRemover...', run_tool, 'AVRemover', 'AVRemover',
    download=True, msg_good='DONE',
    )
  if options['Run TDSSKiller (once)']['Selected']:
    TRY_PRINT.run('Running TDSSKiller...', run_tdsskiller, msg_good='DONE')
  print('')
  reboot(30)


def is_autologon_enabled() -> bool:
  """Check if Autologon is enabled, returns bool."""
  auto_admin_logon = False
  try:
    auto_admin_logon = reg_read_value(
      'HKLM', r'Software\Microsoft\Windows NT\CurrentVersion\Winlogon',
      'AutoAdminLogon',
      )
  except FileNotFoundError:
    # Ignore and assume it's disabled
    pass
  else:
    auto_admin_logon = auto_admin_logon != '0'

  # Done
  return auto_admin_logon


def is_session_started() -> bool:
  """Check if session was started, returns bool."""
  session_started = False
  try:
    session_started = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'SessionStarted')
  except FileNotFoundError:
    pass

  # Done
  return session_started


def load_preset(menus, presets, enable_menu_exit=True) -> None:
  """Load menu settings from preset and ask selection question(s)."""
  if not enable_menu_exit:
    MENU_PRESETS.actions['Main Menu'].update({'Disabled':True, 'Hidden':True})

  # Get selection
  selection = MENU_PRESETS.simple_select()

  # Exit early
  if 'Main Menu' in selection:
    return
  if 'Quit' in selection:
    raise SystemExit

  # Load preset
  preset = presets[selection[0]]
  for group, menu in menus.items():
    group_enabled = group in preset
    for name in menu.options:
      value = group_enabled and name in preset[group]
      menu.options[name]['Selected'] = value

  # Re-enable Main Menu action if disabled
  MENU_PRESETS.actions['Main Menu'].update({'Disabled':False, 'Hidden':False})


def load_settings(menus) -> None:
  """Load session settings from the registry."""
  for group, menu in menus.items():
    if group == 'Main':
      continue
    for name in menu.options:
      menu.options[name].update(get_entry_settings(group, ansi.strip_colors(name)))


def run_auto_repairs(base_menus, presets) -> None:
  """Run Auto Repairs."""
  set_log_path()
  title = f'{KIT_NAME_FULL}: Auto Repairs'
  ui.clear_screen()
  ui.set_title(title)
  ui.print_info(title)
  print('')

  # Generate menus
  ui.print_standard('Initializing...')
  menus = build_menus(base_menus, title, presets)

  # Init
  session_started = False
  show_menu_again = False
  try:
    init(menus, presets)
  except KeyboardInterrupt:
    # Assuming session was started and resume countdown was interrupted
    show_menu_again = True
  session_started = is_session_started()

  # Show Menu
  if show_menu_again or not session_started:
    try:
      show_main_menu(base_menus, menus, presets, title)
    except SystemExit:
      if ui.ask('End session?'):
        end_session()
      raise

  # Start or resume repairs
  ui.clear_screen()
  ui.print_standard(title)
  print('')
  save_selection_settings(menus)
  ui.print_info('Initializing...')
  init_run(menus['Options'].options)
  save_selection_settings(menus)
  if not session_started:
    init_session(menus['Options'].options)
  ui.print_info('Running repairs')

  # Run repairs
  for group, menu in menus.items():
    if group in ('Main', 'Options'):
      continue
    try:
      run_group(group, menu)
    except KeyboardInterrupt:
      ui.abort()

  # Done
  end_session()
  ui.print_info('Done')
  ui.pause('Press Enter to exit...')


def run_group(group, menu) -> None:
  """Run entries in group if appropriate."""
  ui.print_info(f'  {group}')
  for name, details in menu.options.items():
    name_str = ansi.strip_colors(name)
    skipped = details.get('Skipped', False)
    done = details.get('Done', False)
    disabled = details.get('Disabled', False)
    selected = details.get('Selected', False)

    # Selection changed
    if (skipped or done) and not disabled and selected:
      save_settings(group, name, done=False, skipped=False)
      details['Function'](group, name)
      continue

    # Previously skipped
    if skipped:
      ui.show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
      continue

    # Previously ran
    if done:
      color = 'GREEN'
      if details.get('Warning', False):
        color = 'YELLOW'
      elif details.get('Failed', False):
        color = 'RED'
      ui.show_data(
        f'{name_str}...',
        details.get('Message', 'Unknown'), color, width=WIDTH,
        )
      continue

    # Not selected
    if not selected:
      ui.show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
      save_settings(group, name, skipped=True)
      continue

    # Selected
    details['Function'](group, name)


def save_selection_settings(menus) -> None:
  """Save selections in the registry."""
  for group, menu in menus.items():
    if group == 'Main':
      continue
    for name, details in menu.options.items():
      save_settings(
        group, name,
        disabled=details.get('Disabled', False),
        selected=details.get('Selected', False),
        )


def save_settings(group, name, result=None, **kwargs) -> None:
  """Save entry settings in the registry."""
  key_path = fr'{AUTO_REPAIR_KEY}\{group}\{ansi.strip_colors(name)}'

  # Get values from TryAndPrint result
  if result:
    kwargs.update({
      'done': True,
      'failed': result['Failed'],
      'message': result['Message'],
      })
    if isinstance(result['Exception'], GenericWarning):
      kwargs['warning'] = True

  # Write values to registry
  for value_name, data in kwargs.items():
    value_name = ansi.strip_colors(value_name)
    if isinstance(data, bool):
      data = 1 if data else 0
    if isinstance(data, int):
      data_type = 'DWORD'
    elif isinstance(data, str):
      data_type = 'SZ'
    else:
      raise TypeError(f'Invalid data: "{data}" ({type(data)})')
    reg_set_value('HKCU', key_path, value_name, data, data_type)


def set_log_path() -> None:
  """Set log name using defaults or the saved registry value."""
  try:
    log_path = reg_read_value('HKCU', AUTO_REPAIR_KEY, 'LogName')
  except FileNotFoundError:
    # Ignore and use default values
    update_log_path(dest_name='Auto Repairs', timestamp=True)
  else:
    update_log_path(
      dest_name=log_path,
      keep_history=False,
      timestamp=False,
      append=True,
      )


def show_main_menu(base_menus, menus, presets, title) -> None:
  """Show main menu and handle actions."""
  while True:
    update_main_menu(menus)
    selection = menus['Main'].simple_select(update=False)
    if selection[0] in base_menus['Groups'] or selection[0] == 'Options':
      show_sub_menu(menus[selection[0]])
    if selection[0] == 'Load Preset':
      load_preset(menus, presets, title)
    elif 'Start' in selection:
      break
    elif 'Quit' in selection:
      raise SystemExit


def show_sub_menu(menu) -> None:
  """Show sub-menu and handle sub-menu actions."""
  while True:
    selection = menu.advanced_select()
    if 'Main Menu' in selection:
      break
    if 'Quit' in selection:
      raise SystemExit

    # Modify entries
    key = 'Selected'
    unlock_all = False
    unlock_skipped = False
    if 'Select Skipped Entries' in selection:
      key = 'Disabled'
      unlock_skipped = True
      value = False
    if 'Unlock All Entries' in selection:
      key = 'Disabled'
      unlock_all = True
      value = False
    else:
      value = 'All' in selection
    for name in menu.options:
      if (unlock_all
          or (unlock_skipped and not menu.options[name].get('Selected', False))
          or not menu.options[name].get('Disabled', False)
          ):
        menu.options[name][key] = value


def update_main_menu(menus) -> None:
  """Update main menu based on current selections."""
  index = 1
  skip = 'Reboot'
  for name in menus['Main'].options:
    checkmark = ' '
    selected = [
      _v['Selected'] for _k, _v in menus[name].options.items() if _k != skip
      ]
    if all(selected):
      checkmark = '✓'
    elif any(selected):
      checkmark = '-'
    display_name = f' {index}: [{checkmark}] {name}'
    index += 1
    menus['Main'].options[name]['Display Name'] = display_name


# Auto Repairs: Wrapper Functions
def auto_adwcleaner(group, name) -> None:
  """Run AdwCleaner scan.

  save_settings() is called first since AdwCleaner may kill this script.
  """
  save_settings(group, name, done=True, failed=False, message='DONE')
  result = TRY_PRINT.run('AdwCleaner...', run_adwcleaner, msg_good='DONE')

  # Update with actual results (assuming this script wasn't killed)
  save_settings(group, name, result=result)


def auto_backup_browser_profiles(group, name) -> None:
  """Backup browser profiles."""
  backup_all_browser_profiles(use_try_print=True)
  save_settings(group, name, done=True, failed=False, message='DONE')


def auto_backup_power_plans(group, name) -> None:
  """Backup power plans."""
  result = TRY_PRINT.run('Backup Power Plans...', export_power_plans)
  save_settings(group, name, result=result)


def auto_backup_registry(group, name) -> None:
  """Backup registry."""
  result = TRY_PRINT.run('Backup Registry...', backup_registry)
  save_settings(group, name, result=result)


def auto_bleachbit(group, name) -> None:
  """Run BleachBit to clean files."""
  result = TRY_PRINT.run(
    'BleachBit...', run_bleachbit, BLEACH_BIT_CLEANERS, msg_good='DONE',
    )
  save_settings(group, name, result=result)


def auto_chkdsk(group, name) -> None:
  """Run CHKDSK repairs."""
  needs_reboot = False
  result = TRY_PRINT.run(f'CHKDSK ({SYSTEMDRIVE})...', run_chkdsk_online)

  # Run offline CHKDSK if required
  if result['Failed'] and 'Repaired' not in result['Message']:
    needs_reboot = True
    result = TRY_PRINT.run(
      f'Scheduling offline CHKDSK ({SYSTEMDRIVE})...',
      run_chkdsk_offline,
      )
    if not result['Failed']:
      # Successfully set dirty bit to force offline check
      # Set result['Failed'] to True because we failed to repair online
      result['Failed'] = True
      result['Message'] = 'Scheduled offline repairs'

  # Done
  save_settings(group, name, result=result)
  if needs_reboot:
    reboot()


def auto_disable_pending_renames(group, name) -> None:
  """Disable pending renames."""
  result = TRY_PRINT.run(
    'Disabling pending renames...', disable_pending_renames,
    )
  save_settings(group, name, result=result)


def auto_dism(group, name) -> None:
  """Run DISM repairs."""
  needs_reboot = False
  result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism)

  # Try again if necessary
  if result['Failed']:
    TRY_PRINT.run('Enabling Windows Updates...', enable_windows_updates)
    result = TRY_PRINT.run('DISM (RestoreHealth)...', run_dism)
    TRY_PRINT.run('Disabling Windows Updates...', disable_windows_updates)
    needs_reboot = True

  # Save settings
  save_settings(
    group, name, done=True,
    failed=result['Failed'],
    warning=not result['Failed'] and needs_reboot,
    message=result['Message'],
    )

  # Done
  if needs_reboot:
    reboot()


def auto_enable_regback(group, name) -> None:
  """Enable RegBack."""
  result = TRY_PRINT.run(
    'Enable RegBack...', reg_set_value, 'HKLM',
    r'System\CurrentControlSet\Control\Session Manager\Configuration Manager',
    'EnablePeriodicBackup', 1, 'DWORD',
    )
  save_settings(group, name, result=result)


def auto_hitmanpro(group, name) -> None:
  """Run HitmanPro scan."""
  result = TRY_PRINT.run('HitmanPro...', run_hitmanpro, msg_good='DONE')
  save_settings(group, name, result=result)


def auto_kvrt(group, name) -> None:
  """Run KVRT scan."""
  result = TRY_PRINT.run('KVRT...', run_kvrt, msg_good='DONE')
  save_settings(group, name, result=result)


def auto_microsoft_defender(group, name) -> None:
  """Run Microsoft Defender scan."""
  result = TRY_PRINT.run(
    'Microsoft Defender...', run_microsoft_defender, msg_good='DONE',
    )
  save_settings(group, name, result=result)


def auto_reboot(group, name) -> None:
  """Reboot the system."""
  save_settings(group, name, done=True, failed=False, message='DONE')
  print('')
  reboot(30)


def auto_remove_power_plan(group, name) -> None:
  """Remove custom power plan and set to Balanced."""
  result = TRY_PRINT.run(
    'Remove Custom Power Plan...', remove_custom_power_plan,
    )
  save_settings(group, name, result=result)


def auto_repair_registry(group, name) -> None:
  """Delete registry keys with embedded null characters."""
  result = TRY_PRINT.run(
    'Running Registry repairs...', delete_registry_null_keys,
    )
  save_settings(group, name, result=result)


def auto_reset_power_plans(group, name) -> None:
  """Reset power plans."""
  result = TRY_PRINT.run('Reset Power Plans...', reset_power_plans)
  save_settings(group, name, result=result)


def auto_reset_proxy(group, name) -> None:
  """Reset proxy settings."""
  result = TRY_PRINT.run('Clearing proxy settings...', reset_proxy)
  save_settings(group, name, result=result)


def auto_reset_windows_policies(group, name) -> None:
  """Reset Windows policies to defaults."""
  result = TRY_PRINT.run(
    'Resetting Windows policies...', reset_windows_policies,
    )
  save_settings(group, name, result=result)


def auto_restore_uac_defaults(group, name) -> None:
  """Restore UAC default settings."""
  result = TRY_PRINT.run('Restoring UAC defaults...', restore_uac_defaults)
  save_settings(group, name, result=result)


def auto_set_custom_power_plan(group, name) -> None:
  """Set custom power plan."""
  result = TRY_PRINT.run(
    'Set Custom Power Plan...', create_custom_power_plan,
    enable_sleep=False,
    keep_display_on=True,
    )
  save_settings(group, name, result=result)


def auto_sfc(group, name) -> None:
  """Run SFC repairs."""
  result = TRY_PRINT.run('SFC Scan...', run_sfc_scan)
  save_settings(group, name, result=result)


def auto_system_restore_create(group, name) -> None:
  """Create System Restore point."""
  result = TRY_PRINT.run(
    'Create System Restore...', create_system_restore_point,
    )
  save_settings(group, name, result=result)


def auto_system_restore_enable(group, name) -> None:
  """Enable System Restore."""
  cmd = [
    'powershell', '-Command', 'Enable-ComputerRestore',
    '-Drive', SYSTEMDRIVE,
    ]
  result = TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd)
  save_settings(group, name, result=result)


def auto_system_restore_set_size(group, name) -> None:
  """Set System Restore size."""
  result = TRY_PRINT.run('Set System Restore Size...', set_system_restore_size)
  save_settings(group, name, result=result)


def auto_uninstallview(group, name) -> None:
  """Run UninstallView."""
  result = TRY_PRINT.run(
    'UninstallView...', run_uninstallview, msg_good='DONE',
    )
  save_settings(group, name, result=result)


def auto_windows_updates_disable(group, name) -> None:
  """Disable Windows Updates."""
  result = TRY_PRINT.run('Disable Windows Updates...', disable_windows_updates)
  if result['Failed']:
    # Reboot and try again?
    reboot()
  save_settings(group, name, result=result)


def auto_windows_updates_enable(group, name) -> None:
  """Enable Windows Updates."""
  result = TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)
  save_settings(group, name, result=result)


def auto_windows_updates_reset(group, name) -> None:
  """Reset Windows Updates."""
  result = TRY_PRINT.run('Reset Windows Updates...', reset_windows_updates)
  if result['Failed']:
    # Reboot and try again?
    reboot()
  save_settings(group, name, result=result)


# Misc Functions
def set_backup_path(name, date=False) -> pathlib.Path:
  """Set backup path, returns pathlib.Path."""
  return set_local_storage_path('Backups', name, date)


def set_local_storage_path(folder, name, date=False) -> pathlib.Path:
  """Get path for local storage, returns pathlib.Path."""
  local_path = get_path_obj(f'{SYSTEMDRIVE}/{KIT_NAME_SHORT}/{folder}/{name}')
  if date:
    local_path = local_path.joinpath(time.strftime('%Y-%m-%d'))
  return local_path


def set_quarantine_path(name, date=False) -> pathlib.Path:
  """Set quarantine path, returns pathlib.Path."""
  return set_local_storage_path('Quarantine', name, date)


# Tool Functions
def backup_all_browser_profiles(use_try_print=False) -> None:
  """Backup browser profiles for all users."""
  users = get_path_obj(f'{SYSTEMDRIVE}/Users')
  for userprofile in users.iterdir():
    if use_try_print:
      ui.print_info(f'{" "*6}{userprofile.name}')
    backup_browser_profiles(userprofile, use_try_print)


def backup_browser_chromium(backup_path, browser, search_path, use_try_print) -> None:
  """Backup Chromium-based browser profile."""
  for item in search_path.iterdir():
    match = re.match(r'^(Default|Profile).*', item.name, re.IGNORECASE)
    if not match:
      continue
    output_path = backup_path.joinpath(f'{browser}-{item.name}.7z')
    if output_path.exists():
      # Assuming backup was already done
      if use_try_print:
        ui.show_data(
          f'{" "*8}{browser} ({item.name})...', 'Backup already exists.',
          color='YELLOW', width=WIDTH,
          )
      continue

    # Backup data
    cmd = [
      *BACKUP_BROWSER_BASE_CMD,
      output_path, item.joinpath('*'), '-x!*Cache*', '-x!Service Worker',
      ]
    if use_try_print:
      TRY_PRINT.run(
        f'{" "*8}{browser} ({item.name})...',
        run_program, cmd, check=False,
        )
    else:
      run_program(cmd, check=False)


def backup_browser_firefox(backup_path, search_path, use_try_print) -> None:
  """Backup Firefox browser profile."""
  output_path = backup_path.joinpath('Firefox.7z')

  # Bail early
  if output_path.exists():
    # Assuming backup was already done
    if use_try_print:
      ui.show_data(
        f'{" "*8}Firefox (All)...', 'Backup already exists.',
        color='YELLOW', width=WIDTH,
        )
    return

  # Backup data
  cmd = [
    *BACKUP_BROWSER_BASE_CMD, output_path,
    search_path.joinpath('Profiles'), search_path.joinpath('profiles.ini'),
    ]
  if use_try_print:
    TRY_PRINT.run(f'{" "*8}Firefox (All)...', run_program, cmd, check=False)
  else:
    run_program(cmd, check=False)


def backup_browser_profiles(userprofile, use_try_print=False) -> None:
  """Backup browser profiles for userprofile."""
  backup_path = set_backup_path('Browsers', date=True)
  backup_path = backup_path.joinpath(userprofile.name)
  backup_path.mkdir(parents=True, exist_ok=True)

  # Chrome
  search_path = userprofile.joinpath('AppData/Local/Google/Chrome/User Data')
  if search_path.exists():
    backup_browser_chromium(backup_path, 'Chrome', search_path, use_try_print)

  # Edge
  search_path = userprofile.joinpath('AppData/Local/Microsoft/Edge/User Data')
  if search_path.exists():
    backup_browser_chromium(backup_path, 'Edge', search_path, use_try_print)

  # Firefox
  search_path = userprofile.joinpath('AppData/Roaming/Mozilla/Firefox')
  if search_path.joinpath('Profiles').exists():
    backup_browser_firefox(backup_path, search_path, use_try_print)

  # Remove empty folders
  try:
    backup_path.rmdir()
  except OSError:
    # Only looking for empty folders
    pass


def backup_registry() -> None:
  """Backup Registry."""
  backup_path = set_backup_path('Registry', date=True)
  backup_path.parent.mkdir(parents=True, exist_ok=True)

  # Check if backup was already done today
  if backup_path.exists():
    raise GenericWarning('Backup already exists.')

  # Backup registry
  run_tool('ERUNT', 'ERUNT', backup_path, 'sysreg', 'curuser', 'otherusers')


def delete_registry_null_keys() -> None:
  """Delete registry keys with embedded null characters."""
  run_tool('RegDelNull', 'RegDelNull', '-s', '-y', download=True)


def log_kvrt_results(log_path, report_path) -> None:
  """Parse KVRT report and log results in plain text."""
  log_text = ''
  report_file = None

  # Get latest KVRT report
  for item in reversed(sorted(report_path.iterdir())):
    if item.name.startswith('report'):
      report_file = item
      break
  if not report_file:
    log_path.write_text('Failed to find KVRT report.', encoding='utf-8')
    return

  # Parse report
  dom_document = xml_parse(str(report_file))
  block_elements = dom_document.getElementsByTagName('Block0')
  if not block_elements:
    log_path.write_text('Failed to parse KVRT report.', encoding='utf-8')
    return
  attributes = block_elements[0].attributes
  events = block_elements[0].getElementsByTagName('*')

  # Log summary
  for key, value in attributes.items():
    log_text += f'{key+":":<14} {value}\n'

  # Log quarantined items
  quarantined_items = []
  for event in events:
    if not event.getAttribute('Action') == 'Quarantined':
      continue
    quarantined_items.append(event.getAttribute('Object'))
  if quarantined_items:
    log_text += '\nQuarantined Items:\n'
  log_text += '\n'.join(quarantined_items)

  # Done
  log_path.write_text(log_text, encoding='utf-8')


def run_adwcleaner() -> None:
  """Run AdwCleaner."""
  settings_path = get_tool_path('AdwCleaner', 'AdwCleaner', check=False)
  settings_path = settings_path.with_name('settings')
  out_path = get_path_obj(f'{SYSTEMDRIVE}/AdwCleaner/settings')
  out_path.parent.mkdir(exist_ok=True)
  copy_file(settings_path, out_path, overwrite=True)
  run_tool('AdwCleaner', 'AdwCleaner', download=True)


def run_bleachbit(cleaners, preview=True) -> None:
  """Run BleachBit to either clean or preview files."""
  cmd_args = (
    '--preview' if preview else '--clean',
    *cleaners,
    )
  log_path = format_log_path(log_name='BleachBit', timestamp=True, tool=True)
  log_path.parent.mkdir(parents=True, exist_ok=True)
  proc = run_tool('BleachBit', 'bleachbit_console', *cmd_args)

  # Save logs
  log_path.write_text(
    proc.stdout, encoding='utf-8', # type: ignore[reportGeneralTypeIssues]
    )
  log_path.with_suffix('.err').write_text(
    proc.stderr, encoding='utf-8', # type: ignore[reportGeneralTypeIssues]
    )


def run_hitmanpro() -> None:
  """Run HitmanPro scan."""
  log_path = format_log_path(log_name='HitmanPro', timestamp=True, tool=True)
  log_path = log_path.with_suffix('.xml')
  log_path.parent.mkdir(parents=True, exist_ok=True)
  cmd_args = ['/scanonly', f'/log={log_path}']
  run_tool('HitmanPro', 'HitmanPro', *cmd_args, download=True)


def run_kvrt() -> None:
  """Run KVRT scan."""
  log_path = format_log_path(log_name='KVRT', timestamp=True, tool=True)
  log_path.parent.mkdir(parents=True, exist_ok=True)
  quarantine_path = set_quarantine_path('KVRT')
  quarantine_path.mkdir(parents=True, exist_ok=True)
  report_path = quarantine_path.joinpath('Reports')
  report_path.mkdir(parents=True, exist_ok=True)
  scan_list = log_path.with_suffix('.lst')
  scan_list.write_text(
    f'{PROGRAMDATA}\n{SYSTEMDRIVE}\\Users', encoding='utf-8',
    )
  cmd_args = (
    '-accepteula',
    '-d', str(quarantine_path),
    '-dontencrypt', '-fixednames',
    '-processlevel', '1',
    '-customlist', str(scan_list),
    '-silent', '-adinsilent',
    )

  # Run in new pane
  if IN_CONEMU:
    download_tool('KVRT', 'KVRT')
    kvrt_path = get_tool_path('KVRT', 'KVRT')
    tmp_file = fr'{os.environ.get("TMP")}\run_kvrt.cmd'
    with open(tmp_file, 'w', encoding='utf-8') as _f:
      _f.write('@echo off\n')
      _f.write(f'"{kvrt_path}" {" ".join(cmd_args)}\n')
    cmd = ('cmd', '/c', tmp_file, '-new_console:nb', '-new_console:s33V')
    run_program(cmd, check=False)
    sleep(1)
    wait_for_procs('KVRT.exe')
    log_kvrt_results(log_path, report_path)
    return

  # Run in background
  run_tool('KVRT', 'KVRT', *cmd_args, download=True)
  log_kvrt_results(log_path, report_path)


def run_microsoft_defender(full=True) -> None:
  """Run Microsoft Defender scan."""
  reg_key = r'Software\Microsoft\Windows Defender'

  def _get_defender_path() -> str:
    install_path = reg_read_value('HKLM', reg_key, 'InstallLocation')
    return fr'{install_path}\MpCmdRun.exe'

  log_path = format_log_path(
    log_name='Microsoft Defender', timestamp=True, tool=True,
    )
  log_path.parent.mkdir(parents=True, exist_ok=True)

  # Get MS Defender status
  ## NOTE: disabled may be set to an int instead of bool
  ##       This is fine because we're just checking if it's enabled.
  disabled = bool(reg_read_value('HKLM', reg_key, 'DisableAntiSpyware'))
  disabled = disabled or reg_read_value('HKLM', reg_key, 'DisableAntiVirus')
  try:
    passive_mode = reg_read_value('HKLM', reg_key, 'PassiveMode') == 2
  except FileNotFoundError:
    # The registry key may not exist so this is fine
    passive_mode = False
  if disabled and not passive_mode:
    raise GenericError('Defender is disabled.')

  # Update signatures
  defender_path = _get_defender_path()
  cmd = (defender_path, '-SignatureUpdate')
  proc = run_program(cmd, check=False)
  sleep(2)
  if proc.returncode > 0:
    LOG.warning('Failed to update Defender signatures')

  # Update defender path in case it changed after the update
  defender_path = _get_defender_path()

  # Run scan
  cmd = (defender_path, '-Scan', '-ScanType', '2' if full else '1')
  proc = run_program(cmd, check=False)
  log_path.write_text(proc.stdout, encoding='utf-8')
  if proc.returncode > 0:
    raise GenericError('Failed to run scan or clean items.')


def run_rkill() -> None:
  """Run RKill scan."""
  log_path = format_log_path(log_name='RKill', timestamp=True, tool=True)
  log_path.parent.mkdir(parents=True, exist_ok=True)
  whitelist_path = log_path.with_suffix('.wl')
  whitelist_path.write_text(WHITELIST, encoding='utf-8')
  cmd_args = (
    '-l', log_path,
    '-w', whitelist_path,
    '-s',
    )
  run_tool('RKill', 'RKill', *cmd_args, download=True)


def run_tdsskiller() -> None:
  """Run TDSSKiller scan."""
  log_path = format_log_path(log_name='TDSSKiller', timestamp=True, tool=True)
  log_path.parent.mkdir(parents=True, exist_ok=True)
  quarantine_path = set_quarantine_path('TDSSKiller')
  quarantine_path.mkdir(parents=True, exist_ok=True)
  cmd_args = (
    '-accepteula',
    '-accepteulaksn',
    '-l', log_path,
    '-qpath', quarantine_path,
    '-qsus',
    '-dcexact',
    '-silent',
    )
  run_tool('TDSSKiller', 'TDSSKiller', *cmd_args, download=True)


def run_uninstallview() -> None:
  """Run UninstallView."""
  run_tool('UninstallView', 'UninstallView')


# OS Built-in Functions
def create_custom_power_plan(enable_sleep=True, keep_display_on=False) -> None:
  """Create new power plan and set as active."""
  custom_guid = POWER_PLANS['Custom']
  sleep_timeouts = POWER_PLAN_SLEEP_TIMEOUTS['High Performance']
  if enable_sleep:
    sleep_timeouts = POWER_PLAN_SLEEP_TIMEOUTS['Balanced']

  # Duplicate High Performance plan
  cmd = [
    'powercfg', '-DuplicateScheme',
    POWER_PLANS['High Performance'], custom_guid,
    ]
  proc = run_program(cmd, check=False)
  if proc.returncode and 'GUID already exists' not in proc.stderr:
    LOG.error("Failed to create custom power plan.\n  Details: %s", proc)

  # Change the name
  cmd = [
    'powercfg', '-ChangeName', custom_guid,
    CUSTOM_POWER_PLAN_NAME, CUSTOM_POWER_PLAN_DESC,
    ]
  run_program(cmd)

  # Set as active plan
  cmd = ['powercfg', '-SetActive', custom_guid]
  run_program(cmd)

  # Keep the display on
  if keep_display_on:
    for setting in ('monitor-timeout-ac', 'monitor-timeout-dc'):
      cmd = ['powercfg', '-Change', setting, '0']
      run_program(cmd)

  # Set CPU min state
  for arg in ('-SetacValueIndex', '-SetdcValueIndex'):
    cmd = [
      'powercfg', arg, custom_guid, 'SUB_PROCESSOR', 'PROCTHROTTLEMIN', '5',
      ]
    run_program(cmd)

  # Set sleep values
  for arg, value in zip(
      ('-SetacValueIndex', '-SetdcValueIndex'), sleep_timeouts):
    cmd = [
      'powercfg', arg, custom_guid, 'SUB_SLEEP', 'STANDBYIDLE', value,
      ]
    run_program(cmd)


def create_system_restore_point() -> None:
  """Create System Restore point."""
  cmd = [
    'powershell', '-Command', 'Checkpoint-Computer',
    '-Description', f'{KIT_NAME_FULL}-AutoRepairs',
    ]
  too_recent = (
    'WARNING: A new system restore point cannot be created'
    'because one has already been created within the past'
    )
  proc = run_program(cmd)
  if too_recent in proc.stdout:
    raise GenericWarning('Skipped, a restore point was created too recently')


def disable_pending_renames() -> None:
  """Disable pending renames."""
  reg_set_value(
    'HKLM', r'SYSTEM\CurrentControlSet\Control\Session Manager',
    'PendingFileRenameOperations', [], 'MULTI_SZ',
    )


def disable_windows_updates() -> None:
  """Disable and stop Windows Updates."""
  disable_service('wuauserv')
  stop_service('wuauserv')


def enable_windows_updates() -> None:
  """Enable Windows Updates."""
  enable_service('wuauserv', 'demand')


def export_power_plans() -> None:
  """Export existing power plans."""
  backup_path = set_backup_path('Power Plans', date=True)

  # Bail early
  if backup_path.exists():
    raise GenericWarning('Backup already exists.')

  # Get powercfg data
  backup_path.mkdir(parents=True, exist_ok=True)
  cmd = ['powercfg', '/L']
  proc = run_program(cmd)
  plans = {}

  # Get plans
  for line in proc.stdout.splitlines():
    line = line.strip()
    match = re.match(r'^Power Scheme GUID: (.{36})\s+\((.*)\)\s*(\*?)', line)
    if match:
      name = match.group(2)
      if match.group(3):
        name += ' (Default)'
      plans[name] = match.group(1)

  # Backup plans to disk
  for name, guid in plans.items():
    out_path = backup_path.joinpath(f'{name}.pow')
    cmd = ['powercfg', '-export', out_path, guid]
    run_program(cmd)


def kill_explorer() -> None:
  """Kill all Explorer processes."""
  cmd = ['taskkill', '/im', 'explorer.exe', '/f']
  run_program(cmd, check=False)


def reboot(timeout=10) -> None:
  """Reboot the system."""
  atexit.unregister(start_explorer)
  ui.print_warning(f'Rebooting the system in {timeout} seconds...')
  sleep(timeout)
  cmd = ['shutdown', '-r', '-t', '0']
  run_program(cmd, check=False)
  raise SystemExit


def remove_custom_power_plan(high_performance=False) -> None:
  """Remove custom power plan and set to a built-in plan.

  If high_performance is True then set to High Performance and set
  min CPU state to 5% otherwise set to Balanced with default settings.
  """
  power_guid = POWER_PLANS['Balanced']
  if high_performance:
    power_guid = POWER_PLANS['High Performance']

  # Set active plan
  cmd = ['powercfg', '-SetActive', power_guid]
  run_program(cmd)

  # Delete custom power plan (if present)
  cmd = ['powercfg', '-Delete', POWER_PLANS['Custom']]
  run_program(cmd, check=False)

  # Set CPU min state (if needed)
  if high_performance:
    for arg in ('-SetacValueIndex', '-SetdcValueIndex'):
      cmd = [
        'powercfg', arg, power_guid, 'SUB_PROCESSOR', 'PROCTHROTTLEMIN', '5',
        ]
      run_program(cmd)


def reset_power_plans() -> None:
  """Reset power plans to their default settings."""
  cmd = ['powercfg', '-RestoreDefaultSchemes']
  run_program(cmd)


def reset_proxy() -> None:
  """Reset WinHTTP proxy settings."""
  cmd = ['netsh', 'winhttp', 'reset', 'proxy']
  proc = run_program(cmd, check=False)

  # Check result
  if 'Direct access (no proxy server)' not in proc.stdout:
    raise GenericError('Failed to reset proxy settings.')


def reset_windows_policies() -> None:
  """Reset Windows policies to defaults."""
  cmd = ['gpupdate', '/force']
  proc = run_program(cmd, check=False)

  # Check result
  if not all(_s in proc.stdout for _s in GPUPDATE_SUCCESS_STRINGS):
    raise GenericError('Failed to reset one or more policies.')


def reset_windows_updates() -> None:
  """Reset Windows Updates."""
  system_root = os.environ.get('SYSTEMROOT', 'C:/Windows')
  src_path = f'{system_root}/SoftwareDistribution'
  dest_path = non_clobber_path(f'{src_path}.old')
  try:
    rename_item(src_path, dest_path)
    delete_folder(dest_path, force=True)
  except FileNotFoundError:
    # Ignore
    pass


def restore_uac_defaults() -> None:
  """Restore UAC default settings."""
  settings = REG_UAC_DEFAULTS_WIN10
  if OS_VERSION in (7, 8, 8.1):
    settings = REG_UAC_DEFAULTS_WIN7

  reg_write_settings(settings)


def run_chkdsk_offline() -> None:
  """Set filesystem 'dirty bit' to force a CHKDSK during startup."""
  cmd = ['fsutil', 'dirty', 'set', SYSTEMDRIVE]
  proc = run_program(cmd, check=False)

  # Check result
  if proc.returncode > 0:
    raise GenericError('Failed to set dirty bit.')


def run_chkdsk_online() -> None:
  """Run CHKDSK.

  NOTE: If run on Windows 8+ online repairs are attempted.
  """
  cmd = ['CHKDSK', SYSTEMDRIVE]
  if OS_VERSION >= 8:
    cmd.extend(['/scan', '/perf'])
  if IN_CONEMU:
    cmd.extend(['-new_console:nb', '-new_console:s33V'])
  retried = False

  # Run scan
  run_program(cmd, check=False)
  try:
    proc = get_procs('chkdsk.exe')[0]
    return_code = proc.wait()
  except IndexError:
    # Failed to get CHKDSK process, set return_code to force a retry
    return_code = 255
  if return_code > 1:
    # Try again
    retried = True
    run_program(cmd, check=False)
    try:
      proc = get_procs('chkdsk.exe')[0]
      return_code = proc.wait()
    except IndexError:
      # Failed to get CHKDSK process
      return_code = -1

  # Check result
  if return_code == -1:
    raise GenericError('Failed to find CHKDSK process.')
  if (return_code == 0 and retried) or return_code == 1:
    raise GenericWarning('Repaired (or manually aborted)')
  if return_code > 1:
    raise GenericError('Issue(s) detected')


def run_dism(repair=True) -> None:
  """Run DISM to either scan or repair component store health."""
  conemu_args = ['-new_console:nb', '-new_console:s33V'] if IN_CONEMU else []

  # Bail early
  if OS_VERSION < 8:
    raise GenericWarning('Unsupported OS')

  # Run (repair) scan
  log_path = format_log_path(
    log_name=f'DISM_{"Restore" if repair else "Scan"}Health',
    timestamp=True, tool=True,
    )
  log_path.parent.mkdir(parents=True, exist_ok=True)
  cmd = [
    'DISM', '/Online', '/Cleanup-Image',
    '/RestoreHealth' if repair else '/ScanHealth',
    f'/LogPath:{log_path}',
    *conemu_args,
    ]
  run_program(cmd, check=False, pipe=False)
  wait_for_procs('dism.exe')

  # Run check health
  log_path = format_log_path(
    log_name='DISM_CheckHealth.log', timestamp=True, tool=True,
    )
  cmd = [
    'DISM', '/Online', '/Cleanup-Image',
    '/CheckHealth',
    f'/LogPath:{log_path}',
    ]
  proc = run_program(cmd, check=False)

  # Check for errors
  if 'no component store corruption detected' not in proc.stdout.lower():
    raise GenericError('Issue(s) detected')


def run_sfc_scan() -> None:
  """Run SFC and save results."""
  cmd = ['sfc', '/scannow']
  log_path = format_log_path(log_name='SFC', timestamp=True, tool=True)
  err_path = log_path.with_suffix('.err')

  # Run SFC
  proc = run_program(cmd, check=False, encoding='utf-16le')

  # Save output
  os.makedirs(log_path.parent, exist_ok=True)
  with open(log_path, 'a', encoding='utf-8') as _f:
    _f.write(proc.stdout)
  with open(err_path, 'a', encoding='utf-8') as _f:
    _f.write(proc.stderr)

  # Check result
  if 'did not find any integrity violations' in proc.stdout:
    pass
  elif 'successfully repaired' in proc.stdout:
    raise GenericWarning('Repaired')
  elif 'found corrupt files' in proc.stdout:
    raise GenericError('Corruption detected')
  else:
    raise OSError


def set_system_restore_size(size=8) -> None:
  """Set System Restore size."""
  cmd = [
    'vssadmin', 'Resize', 'ShadowStorage',
    f'/On={SYSTEMDRIVE}', f'/For={SYSTEMDRIVE}', f'/MaxSize={size}%',
    ]
  run_program(cmd, pipe=False, stderr=DEVNULL, stdout=DEVNULL)


def start_explorer() -> None:
  """Start Explorer."""
  popen_program(['explorer.exe'])


if __name__ == '__main__':
  print("This file is not meant to be called directly.")
