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

import configparser
from datetime import datetime, timedelta
import logging
import json
import os
import re
import sys
import webbrowser

from typing import Any

from wk.cfg.main      import KIT_NAME_FULL
from wk.cfg.setup     import (
  BROWSER_PATHS,
  DISABLED_ENTRIES_WINDOWS_11,
  FAB_TIMEFRAME,
  LIBREOFFICE_XCU_DATA,
  REG_CHROME_UBLOCK_ORIGIN,
  REG_ESET_NOD32_SETTINGS,
  REG_OPEN_SHELL_LOW_POWER_IDLE,
  REG_OPEN_SHELL_SETTINGS,
  REG_WINDOWS_EXPLORER,
  UBLOCK_ORIGIN_URLS,
  )
from wk.exe           import kill_procs, run_program, popen_program
from wk.io            import case_insensitive_path, get_path_obj
from wk.kit.tools     import (
  ARCH,
  extract_archive,
  extract_tool,
  find_kit_dir,
  get_tool_path,
  run_tool,
  )
from wk.log           import format_log_path, update_log_path
from wk.os.win        import (
  OS_VERSION,
  activate_with_bios,
  check_4k_alignment,
  get_installed_antivirus,
  get_installed_ram,
  get_os_activation,
  get_os_name,
  get_raw_disks,
  get_volume_usage,
  is_activated,
  is_secure_boot_enabled,
  reg_read_value,
  reg_set_value,
  reg_write_settings,
  winget_check,
  winget_import,
  winget_upgrade,
  )
from wk.repairs.win   import (
  WIDTH,
  backup_all_browser_profiles,
  backup_registry,
  create_custom_power_plan,
  create_system_restore_point,
  enable_windows_updates,
  export_power_plans,
  reset_power_plans,
  set_system_restore_size,
  )
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__)
CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False)
KNOWN_ENCODINGS = (
  'utf-8',
  'utf-16',
  'utf-32',
  'utf-16-be',
  'utf-16-le',
  'utf-32-be',
  'utf-32-le',
  )
IN_CONEMU = 'ConEmuPID' in os.environ
MENU_PRESETS = ui.Menu()
PROGRAMFILES_32 = os.environ.get(
  'PROGRAMFILES(X86)', os.environ.get(
    'PROGRAMFILES', r'C:\Program Files (x86)',
    ),
  )
PROGRAMFILES_64 = os.environ.get(
  'PROGRAMW6432', os.environ.get(
    'PROGRAMFILES', r'C:\Program Files',
    ),
  )
SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:')
TRY_PRINT = ui.TryAndPrint()
TRY_PRINT.width = WIDTH
TRY_PRINT.verbose = True
for error in ('CalledProcessError', 'FileNotFoundError'):
  TRY_PRINT.add_error(error)


# Auto Setup
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})

  # Run groups
  for group, entries in base_menus['Groups'].items():
    menus[group] = ui.Menu(title=f'{title}\n{ansi.color_string(group, "GREEN")}')
    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('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 check_if_auto_repairs_are_needed():
  """Check if Auto Repairs are needed based on last Fab run."""
  try:
    last_run = reg_read_value(
      'HKCU', fr'Software\{KIT_NAME_FULL}', 'FabLastRun',
      )
  except FileNotFoundError:
    # Assuming it isn't needed
    return
  fab_last_run = datetime.strptime(last_run, '%Y-%m-%d')

  # Check if Auto Repairs was run after Fab
  auto_repairs_last_run = None
  try:
    last_run = reg_read_value(
      'HKCU', fr'Software\{KIT_NAME_FULL}', 'AutoRepairsLastRun',
      )
    auto_repairs_last_run = datetime.strptime(last_run, '%Y-%m-%d')
  except FileNotFoundError:
    # Assuming it wasn't
    pass

  # Check date(s) and prompt tech if necessary
  if datetime.now() - fab_last_run < timedelta(days=FAB_TIMEFRAME):
    if auto_repairs_last_run and auto_repairs_last_run >= fab_last_run:
      # Auto Repairs shouldn't be needed
      return
    ui.print_warning('Fab was recently run, Auto Repairs may be needed.')
    selection = ui.choice(
      'Continue with Auto Setup, run Auto Repairs, or Quit?', ['C', 'R', 'Q'],
      )
    if selection == 'Q':
      ui.abort()
    if selection == 'R':
      auto_repairs_script = find_kit_dir('Scripts').joinpath('auto_repairs.py')
      os.execv(sys.executable, ['python', auto_repairs_script])


def check_os_and_set_menu_title(title) -> str:
  """Check OS version and update title for menus, returns str."""
  color = None
  os_name = get_os_name(check=False)
  ui.print_standard(f'Operating System: {os_name}')

  # Check support status and set color
  try:
    get_os_name()
  except GenericWarning:
    # Outdated version
    ui.print_warning('OS version is outdated, updating is recommended.')
    if not ui.ask('Continue anyway?'):
      ui.abort()
    color = 'YELLOW'
  except GenericError:
    # Unsupported version
    ui.print_error('OS version is unsupported, updating is recommended.')
    if not ui.ask('Continue anyway? (NOT RECOMMENDED)'):
      ui.abort()
    color = 'RED'

  # Done
  return f'{title}  ({ansi.color_string(os_name, color)})'


def load_preset(menus, presets, title, enable_menu_exit=True) -> None:
  """Load menu settings from preset and ask selection question(s)."""
  msp = False

  # Menu exit entry
  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

  # Ask selection question(s)
  ui.clear_screen()
  ui.print_standard(f'{title}')
  print('')
  if selection[0] == 'Default':
    # OpenShell
    if OS_VERSION != 11 and ui.ask('Install OpenShell?'):
      menus['Install Software'].options['Open-Shell']['Selected'] = True
      menus['Configure System'].options['Open-Shell']['Selected'] = True

    # LibreOffice
    if ui.ask('Install LibreOffice?'):
      menus['Install Software'].options['LibreOffice']['Selected'] = True

    # Hiberboot & Hibernation
    print('')
    msg = 'Disable Fast Startup and enable Hibernation? (Recommended for SSDs)'
    if ui.ask(msg):
      for option in ('Disable Fast Startup', 'Enable Hibernation'):
        menus['Configure System'].options[option]['Selected'] = True

    # Apply ITS settings?
    msp = ui.ask('Is this an ITS system?')
    if msp:
      option = 'Apply ITS Settings'
      menus['Configure System'].options[option]['Selected'] = True

    # ESET NOD32 AV
    print('')
    if msp or ui.ask('Install ESET NOD32 AV?'):
      option = 'ESET NOD32 AV'
      if msp or ui.ask('          For VIP?'):
        option = f'{option} (MSP)'
      menus['Install Software'].options[option]['Selected'] = True

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

  # Disable entries incompatible with Windows 11
  if OS_VERSION == 11:
    for group_name, entry_name in DISABLED_ENTRIES_WINDOWS_11.items():
      menus[group_name].options[entry_name]['Disabled'] = True
      menus[group_name].options[entry_name]['Selected'] = False


def run_auto_setup(base_menus, presets) -> None:
  """Run Auto Setup."""
  update_log_path(dest_name='Auto Setup', timestamp=True)
  title = f'{KIT_NAME_FULL}: Auto Setup'
  ui.clear_screen()
  ui.set_title(title)
  ui.print_info(title)
  print('')
  ui.print_standard('Initializing...')

  # Check OS and update title for menus
  title = check_os_and_set_menu_title(title)

  # Check if AV scan needs to be run
  check_if_auto_repairs_are_needed()

  # Generate menus
  menus = build_menus(base_menus, title, presets)

  # Get setup preset and ask initial questions
  load_preset(menus, presets, title, enable_menu_exit=False)

  # Show Menu
  show_main_menu(base_menus, menus, presets, title)

  # Start setup
  ui.clear_screen()
  ui.print_standard(title)
  print('')
  ui.print_info('Running setup')

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

  # Done
  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)

    # Not selected
    if not details.get('Selected', False):
      ui.show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH)
      continue

    # Selected
    details['Function']()


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

    # Select all or none
    value = 'All' in selection
    for name in menu.options:
      if not menu.options[name].get('Disabled', False):
        menu.options[name]['Selected'] = 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_backup_registry() -> None:
  """Backup registry."""
  TRY_PRINT.run('Backup Registry...', backup_registry)


def auto_backup_browser_profiles() -> None:
  """Backup browser profiles."""
  backup_all_browser_profiles(use_try_print=True)


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


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


def auto_set_custom_power_plan() -> None:
  """Set custom power plan."""
  TRY_PRINT.run('Set Custom Power Plan...', create_custom_power_plan)


def auto_enable_bsod_minidumps() -> None:
  """Enable saving minidumps during BSoDs."""
  TRY_PRINT.run('Enable BSoD mini dumps...', enable_bsod_minidumps)


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


def auto_apply_its_settings():
  """Apply ITS settings."""
  TRY_PRINT.run('Apply ITS settings...', apply_its_settings)


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


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


def auto_system_restore_create() -> None:
  """Create System Restore point."""
  TRY_PRINT.run('Create System Restore...', create_system_restore_point)


def auto_windows_updates_enable() -> None:
  """Enable Windows Updates."""
  TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates)


# Auto Setup: Wrapper Functions
def auto_activate_windows() -> None:
  """Attempt to activate Windows using BIOS key."""
  TRY_PRINT.run('Windows Activation...', activate_with_bios)


def auto_config_browsers() -> None:
  """Configure Browsers."""
  prompt = '    Press Enter to continue...'
  TRY_PRINT.run(
    'uBlock Origin...', enable_ublock_origin, msg_good='STARTED',
    )
  TRY_PRINT.run(
    'Set default browser...', set_default_browser, msg_good='STARTED',
    )
  print(prompt, end='', flush=True)
  ui.pause(' ')

  # Move cursor to beginning of the previous line and clear prompt
  print(f'\033[F\r{" "*len(prompt)}\r', end='', flush=True)


def auto_config_explorer() -> None:
  """Configure Windows Explorer and restart the process."""
  TRY_PRINT.run('Windows Explorer...', config_explorer)


def auto_config_open_shell() -> None:
  """Configure Open Shell."""
  TRY_PRINT.run('Open Shell...', config_open_shell)


def auto_disable_chrome_notifications():
  """Disable Chrome notifications.

  NOTE: This can cause Chrome Sync to be re-authenticated."""
  TRY_PRINT.run('Disable Chrome Notifications...', disable_chrome_notifications)


def auto_disable_fast_startup():
  """Disable fast startup (i.e. Hiberboot)."""
  TRY_PRINT.run(
    'Disable Fast Startup...', reg_set_value, 'HKLM',
    r'SYSTEM\CurrentControlSet\Control\Session Manager\Power',
    'HiberbootEnabled', 0, 'DWORD',
    )


def auto_disable_password_expiration() -> None:
  """Disable password expiration for all users."""
  TRY_PRINT.run('Disable password expiration...', disable_password_expiration)


def auto_enable_hibernation():
  """Enable Hibernation."""
  TRY_PRINT.run(
    'Enable Hibernation...', run_program, ['powercfg', '/hibernate', 'on'],
    )


def auto_export_aida64_report() -> None:
  """Export AIDA64 reports."""
  TRY_PRINT.run('AIDA64 Report...', export_aida64_report)


def auto_install_eset_nod32_av():
  """Install ESET NOD32 AV."""
  TRY_PRINT.run('ESET NOD32...', install_eset_nod32_av)


def auto_install_eset_nod32_av_msp():
  """Install ESET NOD32 AV with MSP settings."""
  TRY_PRINT.run('ESET NOD32 (MSP)...', install_eset_nod32_av, msp=True)


def auto_install_firefox() -> None:
  """Install Firefox."""
  TRY_PRINT.run('Firefox...', install_firefox)


def auto_install_libreoffice() -> None:
  """Install LibreOffice.

  NOTE: It is assumed that auto_install_vcredists() will be run
        before this function to satisfy the vcredist=False usage.
  """
  TRY_PRINT.run(
    'LibreOffice...', install_libreoffice,
    use_mso_formats=True, vcredist=False,
    )


def auto_install_open_shell() -> None:
  """Install Open Shell."""
  TRY_PRINT.run('Open Shell...', install_open_shell)


def auto_install_software_bundle() -> None:
  """Install standard software bundle."""
  TRY_PRINT.run('Software Bundle...', winget_import, group_name='default')


def auto_install_software_upgrades() -> None:
  """Upgrade all supported installed software."""
  TRY_PRINT.run('Software Upgrades...', winget_upgrade)


def auto_install_vcredists() -> None:
  """Install latest supported Visual C++ runtimes."""
  TRY_PRINT.run('Visual C++ Runtimes...', winget_import, group_name='vcredists')


def auto_install_winget() -> None:
  """Install winget if needed."""
  TRY_PRINT.run('Winget...', winget_check, raise_exceptions=True)


def auto_open_device_manager() -> None:
  """Open Device Manager."""
  TRY_PRINT.run('Device Manager...', open_device_manager)


def auto_open_hwinfo_sensors() -> None:
  """Open HWiNFO Sensors."""
  TRY_PRINT.run('HWiNFO Sensors...', open_hwinfo_sensors)


def auto_open_microsoft_store_updates() -> None:
  """Opem Microsoft Store Updates."""
  TRY_PRINT.run('Microsoft Store Updates...', open_microsoft_store_updates)


def auto_open_snappy_driver_installer_origin() -> None:
  """Open Snappy Driver Installer Origin."""
  TRY_PRINT.run('Snappy Driver Installer...', open_snappy_driver_installer_origin)


def auto_open_webcam_tests():
  """Open Webcam Tests."""
  TRY_PRINT.run('Webcam Tests...', open_webcam_tests)


def auto_open_windows_activation() -> None:
  """Open Windows Activation."""
  if not is_activated():
    TRY_PRINT.run('Windows Activation...', open_windows_activation)


def auto_open_windows_updates() -> None:
  """Open Windows Updates."""
  TRY_PRINT.run('Windows Updates...', open_windows_updates)


def auto_open_xmplay() -> None:
  """Open XMPlay."""
  TRY_PRINT.run('XMPlay...', open_xmplay)


def auto_show_4k_alignment_check() -> None:
  """Display 4K alignment check."""
  TRY_PRINT.run('4K alignment Check...', check_4k_alignment, show_alert=True)


def auto_show_installed_antivirus() -> None:
  """Display installed antivirus."""
  TRY_PRINT.run('Virus Protection...', get_installed_antivirus)


def auto_show_installed_ram() -> None:
  """Display installed RAM."""
  TRY_PRINT.run('Installed RAM...', get_installed_ram,
  as_list=True, raise_exceptions=True,
  )


def auto_show_os_activation() -> None:
  """Display OS activation status."""
  TRY_PRINT.run('Activation...', get_os_activation, as_list=True)


def auto_show_os_name() -> None:
  """Display OS Name."""
  TRY_PRINT.run('Operating System...', get_os_name, as_list=True)


def auto_show_secure_boot_status() -> None:
  """Display Secure Boot status."""
  TRY_PRINT.run(
    'Secure Boot...', check_secure_boot_status, msg_good='Enabled',
    )


def auto_show_storage_status() -> None:
  """Display storage status."""
  TRY_PRINT.run('Storage Status...', get_storage_status)


def auto_shutup_10():
  """Disable Windows telemetry using O&O ShutUp 10."""
  TRY_PRINT.run('Disable Telemetry...', run_shutup_10)


def auto_windows_temp_fix() -> None:
  """Restore default ACLs for Windows\\Temp."""
  TRY_PRINT.run(r'Windows\Temp fix...', fix_windows_temp)


# Configure Functions
def config_explorer() -> None:
  """Configure Windows Explorer and restart the process."""
  reg_write_settings(REG_WINDOWS_EXPLORER)
  kill_procs('explorer.exe', force=True)
  popen_program(['explorer.exe'])


def config_open_shell() -> None:
  """Configure Open Shell."""
  has_low_power_idle = False

  # Check if the system supports S0 Low Power Idle
  proc = run_program(['powercfg', '/AvailableSleepStates'], check=False)
  for line in proc.stdout.splitlines():
    if 'Low Power Idle' in line:
      # NOTE: This is safe since we break before the listed unsupported states
      has_low_power_idle = True
      break
    if line.startswith('The following sleep states are not available'):
      break

  # Apply registry settings
  reg_write_settings(REG_OPEN_SHELL_SETTINGS)
  if has_low_power_idle:
    reg_write_settings(REG_OPEN_SHELL_LOW_POWER_IDLE)


def disable_chrome_notifications() -> None:
  """Disable notifications in Google Chrome."""
  defaults_key = 'default_content_setting_values'
  profiles = []
  try:
    search_path = case_insensitive_path(
      f'{os.environ.get("LOCALAPPDATA")}/Google/Chrome/User Data',
      )
  except FileNotFoundError as err:
    raise GenericWarning('No profiles detected.') from err

  # Close any running instances of Chrome
  kill_procs('chrome.exe', force=True)

  # Build list of profiles
  for item in search_path.iterdir():
    if not item.is_dir():
      continue

    if re.match(r'^(Default|Profile).*', item.name, re.IGNORECASE):
      profiles.append(item)

  # Bail if no profiles were detected
  if not profiles:
    raise GenericWarning('No profiles detected.')

  # Set notifications preference
  for profile in profiles:
    pref_file = profile.joinpath('Preferences')
    if not pref_file.exists():
      continue

    # Update config
    pref_data = json.loads(pref_file.read_text())
    if defaults_key not in pref_data['profile']:
      pref_data['profile'][defaults_key] = {}
    pref_data['profile'][defaults_key]['notifications'] = 2

    # Save file
    pref_file.write_text(json.dumps(pref_data, separators=(',', ':')))


def disable_password_expiration() -> None:
  """Disable password expiration for all users."""
  cmd = ['wmic', 'UserAccount', 'set', 'PasswordExpires=False']
  run_program(cmd)


def enable_bsod_minidumps() -> None:
  """Enable saving minidumps during BSoDs."""
  cmd = ['wmic', 'RECOVEROS', 'set', 'DebugInfoType', '=', '3']
  run_program(cmd)


def enable_ublock_origin() -> None:
  """Enable uBlock Origin in supported browsers."""
  base_paths = [
    PROGRAMFILES_64, PROGRAMFILES_32, os.environ.get('LOCALAPPDATA'),
    ]
  cmds = []

  # Add Google Chrome registry entries
  reg_write_settings(REG_CHROME_UBLOCK_ORIGIN)

  # Build cmds list
  for browser, rel_path in BROWSER_PATHS.items():
    browser_path = None
    for base_path in base_paths:
      try:
        browser_path = case_insensitive_path(f'{base_path}/{rel_path}')
      except FileNotFoundError:
        # Ignore missing browsers
        continue
      else:
        # Found a match, skip checking the rest
        break
    if browser_path:
      cmds.append([browser_path, UBLOCK_ORIGIN_URLS[browser]])

  # Open detected browsers
  for cmd in cmds:
    popen_program(cmd, pipe=True)


def fix_windows_temp() -> None:
  """Restore default permissions for Windows\\Temp."""
  permissions = (
    'Users:(CI)(X,WD,AD)',
    'Administrators:(OI)(CI)(F)',
    )
  for _p in permissions:
    cmd = ['icacls', fr'{SYSTEMDRIVE}\Windows\Temp', '/grant:r', _p, '/T']
    run_program(cmd)


# Install Functions
def install_eset_nod32_av(msp=False):
  """Install ESET NOD32 Antivirus."""
  config_file = f'eset-config{"-msp" if msp else ""}.xml'
  reg_write_settings(REG_ESET_NOD32_SETTINGS)
  run_tool(
    'ESET', 'ESET_NOD32_AV',
    '--silent', '--accepteula',
    '--msi-property', 'PRODUCTTYPE=eav',
    'PRODUCT_LANG=1033', 'PRODUCT_LANG_CODE=en-US',
    f'ADMINCFG="{config_file}"',
    cwd=True, download=True,
    )


def install_firefox() -> None:
  """Install Firefox.

  As far as I can tell if you use the EXE installers then it will use
  the same installation directory as the installed version.  As such a
  32-bit installation could be upgraded to a 64-bit one and still use
  %PROGRAMFILES(X86)%  However, if a 64-bit MSI installer is used then
  it ignores the 32-bit directory and just installs a new copy to
  %PROGRAMFILES% resulting in two copies of Firefox to be in place.
  To address this issue this function will uninstall all copies of
  Firefox under 64-bit Windows if it's detected in %PROGRAMFILES(X86)%
  before installing the latest 64-bit version.

  Firefox 67 changed how profiles are named to avoid reusing the same
  profile between different channels of Firefox (std, beta, ESR, etc).
  However the logic used when upgrading from versions before 67 to
  current isn't ideal.  It can, but doesn't always?, create a new
  profile and set it as default; even if there's an existing profile
  being used.  To address this profiles.ini is read to compare with the
  post-install/upgrade state.  If the default is changed to a new
  profile then it is reverted so the original existing profile instead.
  """
  current_default_profile = None
  encoding = None
  firefox_exe = get_path_obj(
    f'{os.environ["PROGRAMFILES"]}/Mozilla Firefox/firefox.exe',
    )
  profiles_ini = get_path_obj(
    f'{os.environ["APPDATA"]}/Mozilla/Firefox/profiles.ini',
    )
  program_path_32bit_exists = False
  try:
    case_insensitive_path(
      f'{PROGRAMFILES_32}/Mozilla Firefox/firefox.exe',
      )
  except FileNotFoundError:
    # Ignore
    pass
  else:
    program_path_32bit_exists = True
  revert_default = False

  # Save current default profile
  if profiles_ini.exists():
    current_default_profile, encoding = get_firefox_default_profile(
      profiles_ini,
      )

  # Uninstall Firefox if needed
  if ARCH == '64' and program_path_32bit_exists:
    uninstall_firefox()

  # Install Firefox
  run_tool('Firefox', 'Firefox', '/S', download=True)

  # Open Firefox to force profiles.ini update
  popen_program([firefox_exe])
  sleep(5)
  kill_procs('firefox.exe', force=True)

  # Check if default profile changed
  if current_default_profile:
    new_default, encoding = get_firefox_default_profile(profiles_ini)
    revert_default = new_default and new_default != current_default_profile

  # Revert default profile if needed
  if revert_default:
    out = []
    for line in profiles_ini.read_text(encoding=encoding).splitlines():
      if 'Default=Profile' in line:
        out.append(f'Default={current_default_profile}')
      else:
        out.append(line)
    profiles_ini.write_text('\n'.join(out), encoding='utf-8')


def install_libreoffice(
    register_mso_types=True, use_mso_formats=False, vcredist=True):
  """Install LibreOffice."""
  installer = find_kit_dir('Installers').joinpath(f'LibreOffice{ARCH}.msi')
  xcu_dir = get_path_obj(f'{os.environ.get("APPDATA")}/LibreOffice/4/user')
  xcu_file = xcu_dir.joinpath('registrymodifications.xcu')

  # Set default save formats to MSO types
  if use_mso_formats and not xcu_file.exists():
    xcu_dir.mkdir(parents=True, exist_ok=True)
    with open(xcu_file, 'w', encoding='utf-8', newline='\n') as _f:
      _f.write(LIBREOFFICE_XCU_DATA)

  # Build cmd
  cmd = [
    'msiexec', '/passive', '/norestart',
    '/i', installer,
    'REBOOTYESNO=No',
    f'VC_REDIST={1 if vcredist else 0}',
    ]
  if register_mso_types:
    cmd.append('REGISTER_ALL_MSO_TYPES=1')
  else:
    cmd.append('REGISTER_NO_MSO_TYPES=1')

  # Install LibreOffice
  run_program(cmd)


def install_open_shell() -> None:
  """Install Open Shell (just the Start Menu)."""
  skin_zip = get_tool_path('OpenShell', 'Fluent-Metro', suffix='zip')

  # Bail early
  if OS_VERSION != 10:
    raise GenericWarning('Unsupported OS')

  # Install OpenShell
  run_tool('OpenShell', 'OpenShell', '/qn', 'ADDLOCAL=StartMenu')

  # Install Skin
  extract_archive(skin_zip, f'{PROGRAMFILES_64}/Open-Shell/Skins', '-aoa')

  # Add scheduled task to handle OS upgrades
  cmd = ['schtasks', '/query', '/tn', 'Open-Shell OS upgrade check']
  proc = run_program(cmd, check=False)
  if proc.returncode == 0:
    # Task already exists, bail and leave current task unmodified
    return
  cmd = [
    'schtasks', '/create',
    '/ru', r'NT AUTHORITY\SYSTEM',
    '/sc', 'ONSTART',
    '/tn', 'Open-Shell OS upgrade check',
    '/tr', r'"%PROGRAMFILES%\Open-Shell\StartMenu.exe" -upgrade -silent',
    ]
  run_program(cmd)


def uninstall_firefox() -> None:
  """Uninstall all copies of Firefox."""
  json_file = format_log_path(log_name='Installed Programs', timestamp=True)
  json_file = json_file.with_name(f'{json_file.stem}.json')
  uninstall_data = None

  # Get uninstall_data from UninstallView
  extract_tool('UninstallView')
  cmd = [get_tool_path('UninstallView', 'UninstallView'), '/sjson', json_file]
  run_program(cmd)
  with open(json_file, 'rb') as _f:
    uninstall_data = json.load(_f)

  # Uninstall Firefox if found
  for item in uninstall_data:
    if item['Display Name'].lower().startswith('mozilla firefox'):
      uninstaller = item['Uninstall String'].replace('"', '')
      run_program([uninstaller, '/S'])


# Misc Functions
def apply_its_settings():
  """Apply ITS settings."""
  create_custom_power_plan(enable_sleep=False)


def check_secure_boot_status() -> None:
  """Check Secure Boot status."""
  is_secure_boot_enabled(raise_exceptions=True, show_alert=True)


def get_firefox_default_profile(profiles_ini) -> Any:
  """Get Firefox default profile, returns(pathlib.Path, encoding) or None."""
  # TODO: Refactor to remove dependancy on Any
  default_profile = None
  encoding = None
  parser = None

  # Bail early
  if not profiles_ini.exists():
    return (default_profile, encoding)

  # Determine encoding (if possible)
  for enc in KNOWN_ENCODINGS:
    try:
      profiles_ini.read_text(encoding=enc)
    except UnicodeError:
      # Ignore and keep trying
      pass
    else:
      encoding = enc
      break
  if not encoding:
    raise UnicodeError('Failed to determine encoding of profiles.ini')

  # Parse INI
  parser = configparser.ConfigParser()
  parser.read(profiles_ini, encoding=encoding)
  for section in parser.sections():
    if section.lower().startswith('install'):
      default_profile = parser[section].get('default')
      break
    value = parser[section].get('default')
    if value and value == '1':
      default_profile = parser[section].get('path')

  # Done
  return (default_profile, encoding)


def get_storage_status() -> list[str]:
  """Get storage status for fixed disks, returns list."""
  report = get_volume_usage(use_colors=True)
  for disk in get_raw_disks():
    report.append(ansi.color_string(f'Uninitialized Disk: {disk}', 'RED'))

  # Done
  return report


def set_default_browser() -> None:
  """Open Windows Settings to the default apps section."""
  cmd = ['start', '', 'ms-settings:defaultapps']
  popen_program(cmd, shell=True)


# Tool Functions
def export_aida64_report() -> None:
  """Export AIDA64 report."""
  report_path = format_log_path(
    log_name='AIDA64 System Report',
    tool=True, timestamp=True,
    )
  report_path = report_path.with_suffix('.html')
  report_path.parent.mkdir(parents=True, exist_ok=True)

  # Run AIDA64 and check result
  proc = run_tool(
    'AIDA64', 'aida64',
    '/R', report_path,
    '/CUSTOM', 'basic.rpf',
    '/HTML', '/SILENT', '/SAFEST',
    cwd=True,
    )
  if proc.returncode:
    raise GenericError('Error(s) encountered exporting report.')


def open_device_manager() -> None:
  """Open Device Manager."""
  popen_program(['mmc', 'devmgmt.msc'])


def open_hwinfo_sensors() -> None:
  """Open HWiNFO sensors."""
  hwinfo_path = get_tool_path('HWiNFO', 'HWiNFO')
  base_config = hwinfo_path.with_name('general.ini')

  # Write new config to disk
  with open(hwinfo_path.with_suffix('.ini'), 'w', encoding='utf-8') as _f:
    _f.write(
      f'{base_config.read_text(encoding="utf-8")}\n'
      'SensorsOnly=1\nSummaryOnly=0\n'
      )

  # Open HWiNFO
  run_tool('HWiNFO', 'HWiNFO', popen=True)


def open_microsoft_store_updates() -> None:
  """Open Microsoft Store to the updates page."""
  popen_program(['explorer', 'ms-windows-store:updates'])


def open_snappy_driver_installer_origin() -> None:
  """Open Snappy Driver Installer Origin."""
  run_tool('SDIO', 'SDIO', cwd=True, pipe=True, popen=True)


def open_webcam_tests():
  """Open Webcam Tests."""
  webbrowser.open('http://webcamtests.com/')


def open_windows_activation() -> None:
  """Open Windows Activation."""
  popen_program(['slui'])


def open_windows_updates() -> None:
  """Open Windows Updates."""
  popen_program(['control', '/name', 'Microsoft.WindowsUpdate'])


def open_xmplay() -> None:
  """Open XMPlay."""
  sleep(2)
  run_tool('XMPlay', 'XMPlay', 'music.7z', cwd=True, popen=True)


def run_shutup_10():
  """Disable Windows telemetry using O&O ShutUp 10."""
  run_tool('ShutUp10', 'OOSU10', '1201.cfg', '/quiet', cwd=True)


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