from typing import override

from archinstall.default_profiles.profile import GreeterType
from archinstall.lib.applications.application_menu import ApplicationMenu
from archinstall.lib.args import ArchConfig
from archinstall.lib.authentication.authentication_menu import AuthenticationMenu
from archinstall.lib.bootloader.bootloader_menu import BootloaderMenu
from archinstall.lib.configuration import save_config
from archinstall.lib.disk.disk_menu import DiskLayoutConfigurationMenu
from archinstall.lib.general.general_menu import add_number_of_parallel_downloads, select_hostname, select_ntp, select_timezone
from archinstall.lib.general.system_menu import select_kernel, select_swap
from archinstall.lib.hardware import SysInfo
from archinstall.lib.locale.locale_menu import LocaleMenu
from archinstall.lib.menu.abstract_menu import AbstractMenu, SpecialMenuKey
from archinstall.lib.mirror.mirror_handler import MirrorListHandler
from archinstall.lib.mirror.mirror_menu import MirrorMenu
from archinstall.lib.models.application import ApplicationConfiguration, ZramConfiguration
from archinstall.lib.models.authentication import AuthenticationConfiguration
from archinstall.lib.models.bootloader import Bootloader, BootloaderConfiguration
from archinstall.lib.models.device import DiskLayoutConfiguration, DiskLayoutType, FilesystemType, PartitionModification
from archinstall.lib.models.locale import LocaleConfiguration
from archinstall.lib.models.mirrors import MirrorConfiguration
from archinstall.lib.models.network import NetworkConfiguration, NicType
from archinstall.lib.models.packages import Repository
from archinstall.lib.models.profile import ProfileConfiguration
from archinstall.lib.network.network_menu import select_network
from archinstall.lib.output import FormattedOutput
from archinstall.lib.packages.packages import list_available_packages, select_additional_packages
from archinstall.lib.pacman.config import PacmanConfig
from archinstall.lib.translationhandler import Language, tr, translation_handler
from archinstall.tui.ui.menu_item import MenuItem, MenuItemGroup


class GlobalMenu(AbstractMenu[None]):
	def __init__(
		self,
		arch_config: ArchConfig,
		mirror_list_handler: MirrorListHandler | None = None,
		skip_boot: bool = False,
		title: str | None = None,
	) -> None:
		self._arch_config = arch_config
		self._mirror_list_handler = mirror_list_handler
		self._skip_boot = skip_boot
		self._uefi = SysInfo.has_uefi()
		menu_options = self._get_menu_options()

		self._item_group = MenuItemGroup(
			menu_options,
			sort_items=False,
			checkmarks=True,
		)

		super().__init__(self._item_group, config=arch_config, title=title)

	def _get_menu_options(self) -> list[MenuItem]:
		menu_options = [
			MenuItem(
				text=tr('Archinstall language'),
				action=self._select_archinstall_language,
				preview_action=self._prev_archinstall_language,
				key='archinstall_language',
			),
			MenuItem(
				text=tr('Locales'),
				value=LocaleConfiguration.default(),
				action=self._locale_selection,
				preview_action=self._prev_locale,
				key='locale_config',
			),
			MenuItem(
				text=tr('Mirrors and repositories'),
				action=self._mirror_configuration,
				preview_action=self._prev_mirror_config,
				key='mirror_config',
			),
			MenuItem(
				text=tr('Disk configuration'),
				action=self._select_disk_config,
				preview_action=self._prev_disk_config,
				mandatory=True,
				key='disk_config',
			),
			MenuItem(
				text=tr('Swap'),
				value=ZramConfiguration(enabled=True),
				action=select_swap,
				preview_action=self._prev_swap,
				key='swap',
			),
			MenuItem(
				text=tr('Bootloader'),
				value=BootloaderConfiguration.get_default(self._uefi, self._skip_boot),
				action=self._select_bootloader_config,
				preview_action=self._prev_bootloader_config,
				key='bootloader_config',
			),
			MenuItem(
				text=tr('Kernels'),
				value=['linux'],
				action=select_kernel,
				preview_action=self._prev_kernel,
				mandatory=True,
				key='kernels',
			),
			MenuItem(
				text=tr('Hostname'),
				value='archlinux',
				action=select_hostname,
				preview_action=self._prev_hostname,
				key='hostname',
			),
			MenuItem(
				text=tr('Authentication'),
				action=self._select_authentication,
				preview_action=self._prev_authentication,
				key='auth_config',
			),
			MenuItem(
				text=tr('Profile'),
				action=self._select_profile,
				preview_action=self._prev_profile,
				key='profile_config',
			),
			MenuItem(
				text=tr('Applications'),
				action=self._select_applications,
				value=[],
				preview_action=self._prev_applications,
				key='app_config',
			),
			MenuItem(
				text=tr('Network configuration'),
				action=select_network,
				value={},
				preview_action=self._prev_network_config,
				key='network_config',
			),
			MenuItem(
				text=tr('Parallel Downloads'),
				action=add_number_of_parallel_downloads,
				value=1,
				preview_action=self._prev_parallel_dw,
				key='parallel_downloads',
			),
			MenuItem(
				text=tr('Additional packages'),
				action=self._select_additional_packages,
				value=[],
				preview_action=self._prev_additional_pkgs,
				key='packages',
			),
			MenuItem(
				text=tr('Timezone'),
				action=select_timezone,
				value='UTC',
				preview_action=self._prev_tz,
				key='timezone',
			),
			MenuItem(
				text=tr('Automatic time sync (NTP)'),
				action=select_ntp,
				value=True,
				preview_action=self._prev_ntp,
				key='ntp',
			),
			MenuItem(
				text='',
				read_only=True,
			),
			MenuItem(
				text=tr('Save configuration'),
				action=lambda x: self._safe_config(),
				key=SpecialMenuKey.SAVE.value,
			),
			MenuItem(
				text=tr('Install'),
				preview_action=self._prev_install_invalid_config,
				key=SpecialMenuKey.INSTALL.value,
			),
			MenuItem(
				text=tr('Abort'),
				key=SpecialMenuKey.ABORT.value,
			),
		]

		return menu_options

	async def _safe_config(self) -> None:
		# data: dict[str, Any] = {}
		# for item in self._item_group.items:
		# if item.key is not None:
		# data[item.key] = item.value

		self.sync_all_to_config()
		await save_config(self._arch_config)

	def _missing_configs(self) -> list[str]:
		item: MenuItem = self._item_group.find_by_key('auth_config')
		auth_config: AuthenticationConfiguration | None = item.value

		def check(s: str) -> bool:
			item = self._item_group.find_by_key(s)
			return item.has_value()

		missing = set()

		if (auth_config is None or auth_config.root_enc_password is None) and not (auth_config and auth_config.has_superuser()):
			missing.add(
				tr('Either root-password or at least 1 user with sudo privileges must be specified'),
			)

		# These greeters only show users with UID >= 1000 and have no manual login by default
		if not (auth_config and auth_config.has_regular_user()):
			profile_item: MenuItem = self._item_group.find_by_key('profile_config')
			profile_config: ProfileConfiguration | None = profile_item.value

			if profile_config and profile_config.profile and profile_config.profile.is_desktop_profile():
				problematic_greeters = {GreeterType.Sddm}
				if any(p.default_greeter_type in problematic_greeters for p in profile_config.profile.current_selection):
					missing.add(
						tr('The selected desktop profile requires a regular user to log in via the greeter'),
					)

		for item in self._item_group.items:
			if item.mandatory:
				assert item.key is not None
				if not check(item.key):
					missing.add(item.text)

		return list(missing)

	@override
	def is_config_valid(self) -> bool:
		"""
		Checks the validity of the current configuration.
		"""
		if len(self._missing_configs()) != 0:
			return False
		return self._validate_bootloader() is None

	async def _select_archinstall_language(self, preset: Language) -> Language:
		from archinstall.lib.general.general_menu import select_archinstall_language

		language = await select_archinstall_language(translation_handler.translated_languages, preset)
		translation_handler.activate(language)

		self._update_lang_text()

		return language

	def _prev_archinstall_language(self, item: MenuItem) -> str | None:
		if not item.value:
			return None

		lang: Language = item.value
		return f'{tr("Language")}: {lang.display_name}'

	async def _select_applications(self, preset: ApplicationConfiguration | None) -> ApplicationConfiguration | None:
		app_config = await ApplicationMenu(preset).show()
		return app_config

	async def _select_authentication(self, preset: AuthenticationConfiguration | None) -> AuthenticationConfiguration | None:
		auth_config = await AuthenticationMenu(preset).show()
		return auth_config

	def _update_lang_text(self) -> None:
		"""
		The options for the global menu are generated with a static text;
		each entry of the menu needs to be updated with the new translation
		"""
		new_options = self._get_menu_options()

		for o in new_options:
			if o.key is not None:
				self._item_group.find_by_key(o.key).text = o.text

	async def _locale_selection(self, preset: LocaleConfiguration) -> LocaleConfiguration | None:
		locale_config = await LocaleMenu(preset).show()
		return locale_config

	def _prev_locale(self, item: MenuItem) -> str | None:
		if not item.value:
			return None

		config: LocaleConfiguration = item.value
		return config.preview()

	def _prev_network_config(self, item: MenuItem) -> str | None:
		if item.value:
			network_config: NetworkConfiguration = item.value
			if network_config.type == NicType.MANUAL:
				output = FormattedOutput.as_table(network_config.nics)
			else:
				output = f'{tr("Network configuration")}:\n{network_config.type.display_msg()}'

			return output
		return None

	def _prev_additional_pkgs(self, item: MenuItem) -> str | None:
		if item.value:
			output = '\n'.join(sorted(item.value))
			return output
		return None

	def _prev_authentication(self, item: MenuItem) -> str | None:
		if item.value:
			auth_config: AuthenticationConfiguration = item.value
			output = ''

			if auth_config.root_enc_password:
				output += f'{tr("Root password")}: {auth_config.root_enc_password.hidden()}\n'

			if auth_config.users:
				output += FormattedOutput.as_table(auth_config.users) + '\n'

			if auth_config.u2f_config:
				u2f_config = auth_config.u2f_config
				login_method = u2f_config.u2f_login_method.display_value()
				output = tr('U2F login method: ') + login_method

				output += '\n'
				output += tr('Passwordless sudo: ') + (tr('Enabled') if u2f_config.passwordless_sudo else tr('Disabled'))

			return output

		return None

	def _prev_applications(self, item: MenuItem) -> str | None:
		if item.value:
			app_config: ApplicationConfiguration = item.value
			output = ''

			if app_config.bluetooth_config:
				output += f'{tr("Bluetooth")}: '
				output += tr('Enabled') if app_config.bluetooth_config.enabled else tr('Disabled')
				output += '\n'

			if app_config.audio_config:
				audio_config = app_config.audio_config
				output += f'{tr("Audio")}: {audio_config.audio.value}'
				output += '\n'

			if app_config.print_service_config:
				output += f'{tr("Print service")}: '
				output += tr('Enabled') if app_config.print_service_config.enabled else tr('Disabled')
				output += '\n'

			if app_config.power_management_config:
				power_management_config = app_config.power_management_config
				output += f'{tr("Power management")}: {power_management_config.power_management.value}'
				output += '\n'

			if app_config.firewall_config:
				firewall_config = app_config.firewall_config
				output += f'{tr("Firewall")}: {firewall_config.firewall.value}'
				output += '\n'

			return output

		return None

	def _prev_tz(self, item: MenuItem) -> str | None:
		if item.value:
			return f'{tr("Timezone")}: {item.value}'
		return None

	def _prev_ntp(self, item: MenuItem) -> str | None:
		if item.value is not None:
			output = f'{tr("NTP")}: '
			output += tr('Enabled') if item.value else tr('Disabled')
			return output
		return None

	def _prev_disk_config(self, item: MenuItem) -> str | None:
		disk_layout_conf: DiskLayoutConfiguration | None = item.value

		if disk_layout_conf:
			output = tr('Configuration type: {}').format(disk_layout_conf.config_type.display_msg()) + '\n'

			if disk_layout_conf.config_type == DiskLayoutType.Pre_mount:
				output += tr('Mountpoint') + ': ' + str(disk_layout_conf.mountpoint)

			if disk_layout_conf.lvm_config:
				output += '{}: {}'.format(tr('LVM configuration type'), disk_layout_conf.lvm_config.config_type.display_msg()) + '\n'

			if disk_layout_conf.disk_encryption:
				output += tr('Disk encryption') + ': ' + disk_layout_conf.disk_encryption.encryption_type.type_to_text() + '\n'

			if disk_layout_conf.btrfs_options:
				btrfs_options = disk_layout_conf.btrfs_options
				if btrfs_options.snapshot_config:
					output += tr('Btrfs snapshot type: {}').format(btrfs_options.snapshot_config.snapshot_type.value) + '\n'

			return output

		return None

	def _prev_swap(self, item: MenuItem) -> str | None:
		if item.value is not None:
			output = f'{tr("Swap on zram")}: '
			output += tr('Enabled') if item.value.enabled else tr('Disabled')
			if item.value.enabled:
				output += f'\n{tr("Compression algorithm")}: {item.value.algorithm.value}'
			return output
		return None

	def _prev_hostname(self, item: MenuItem) -> str | None:
		if item.value is not None:
			return f'{tr("Hostname")}: {item.value}'
		return None

	def _prev_parallel_dw(self, item: MenuItem) -> str | None:
		if item.value is not None:
			return f'{tr("Parallel Downloads")}: {item.value}'
		return None

	def _prev_kernel(self, item: MenuItem) -> str | None:
		if item.value:
			kernel = ', '.join(item.value)
			return f'{tr("Kernel")}: {kernel}'
		return None

	def _prev_bootloader_config(self, item: MenuItem) -> str | None:
		bootloader_config: BootloaderConfiguration | None = item.value
		if bootloader_config:
			return bootloader_config.preview(self._uefi)
		return None

	def _validate_bootloader(self) -> str | None:
		"""
		Checks the selected bootloader is valid for the selected filesystem
		type of the boot partition.

		Returns [`None`] if the bootloader is valid, otherwise returns a
		string with the error message.
		"""
		bootloader_config: BootloaderConfiguration | None = None
		root_partition: PartitionModification | None = None
		boot_partition: PartitionModification | None = None
		efi_partition: PartitionModification | None = None

		bootloader_config = self._item_group.find_by_key('bootloader_config').value

		if not bootloader_config or bootloader_config.bootloader == Bootloader.NO_BOOTLOADER:
			return None

		bootloader = bootloader_config.bootloader

		if disk_config := self._item_group.find_by_key('disk_config').value:
			for layout in disk_config.device_modifications:
				if root_partition := layout.get_root_partition():
					break
			for layout in disk_config.device_modifications:
				if boot_partition := layout.get_boot_partition():
					break
			if self._uefi:
				for layout in disk_config.device_modifications:
					if efi_partition := layout.get_efi_partition():
						break
		else:
			return 'No disk layout selected'

		if root_partition is None:
			return 'Root partition not found'

		if boot_partition is None:
			return 'Boot partition not found'

		if self._uefi:
			if efi_partition is None:
				return 'EFI system partition (ESP) not found'

			if efi_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
				return 'ESP must be formatted as a FAT filesystem'

		if bootloader == Bootloader.Limine:
			if boot_partition.fs_type not in [FilesystemType.Fat12, FilesystemType.Fat16, FilesystemType.Fat32]:
				return 'Limine does not support booting with a non-FAT boot partition'

		elif bootloader == Bootloader.Refind:
			if not self._uefi:
				return 'rEFInd can only be used on UEFI systems'

		return None

	def _prev_install_invalid_config(self, item: MenuItem) -> str | None:
		if missing := self._missing_configs():
			text = tr('Missing configurations:\n')
			for m in missing:
				text += f'- {m}\n'
			return text[:-1]  # remove last new line

		if error := self._validate_bootloader():
			return tr(f'Invalid configuration: {error}')

		return None

	def _prev_profile(self, item: MenuItem) -> str | None:
		profile_config: ProfileConfiguration | None = item.value

		if profile_config and profile_config.profile:
			output = tr('Profiles') + ': '
			if profile_names := profile_config.profile.current_selection_names():
				output += ', '.join(profile_names) + '\n'
			else:
				output += profile_config.profile.name + '\n'

			if profile_config.gfx_driver:
				output += tr('Graphics driver') + ': ' + profile_config.gfx_driver.value + '\n'

			if profile_config.greeter:
				output += tr('Greeter') + ': ' + profile_config.greeter.value + '\n'

			return output

		return None

	async def _select_disk_config(
		self,
		preset: DiskLayoutConfiguration | None = None,
	) -> DiskLayoutConfiguration | None:
		disk_config = await DiskLayoutConfigurationMenu(preset).show()
		return disk_config

	async def _select_bootloader_config(
		self,
		preset: BootloaderConfiguration | None = None,
	) -> BootloaderConfiguration | None:
		if preset is None:
			preset = BootloaderConfiguration.get_default(self._uefi, self._skip_boot)

		bootloader_config = await BootloaderMenu(preset, self._uefi, self._skip_boot).show()

		return bootloader_config

	async def _select_profile(self, current_profile: ProfileConfiguration | None) -> ProfileConfiguration | None:
		from archinstall.lib.profile.profile_menu import ProfileMenu

		profile_config = await ProfileMenu(preset=current_profile).show()
		return profile_config

	async def _select_additional_packages(self, preset: list[str]) -> list[str]:
		config: MirrorConfiguration | None = self._item_group.find_by_key('mirror_config').value

		repositories: set[Repository] = set()
		if config:
			repositories = set(config.optional_repositories)

		packages = await select_additional_packages(
			preset,
			repositories=repositories,
		)

		return packages

	async def _mirror_configuration(self, preset: MirrorConfiguration | None = None) -> MirrorConfiguration | None:
		if self._mirror_list_handler is None:
			self._mirror_list_handler = MirrorListHandler()

		mirror_configuration = await MirrorMenu(self._mirror_list_handler, preset=preset).run()

		if mirror_configuration and mirror_configuration.optional_repositories:
			# reset the package list cache in case the repository selection has changed
			list_available_packages.cache_clear()

			# enable the repositories in the config
			pacman_config = PacmanConfig(None)
			pacman_config.enable(mirror_configuration.optional_repositories)
			pacman_config.apply()

		return mirror_configuration

	def _prev_mirror_config(self, item: MenuItem) -> str | None:
		if not item.value:
			return None

		mirror_config: MirrorConfiguration = item.value

		output = ''
		if mirror_config.mirror_regions:
			title = tr('Selected mirror regions')
			divider = '-' * len(title)
			regions = mirror_config.region_names
			output += f'{title}\n{divider}\n{regions}\n\n'

		if mirror_config.custom_servers:
			title = tr('Custom servers')
			divider = '-' * len(title)
			servers = mirror_config.custom_server_urls
			output += f'{title}\n{divider}\n{servers}\n\n'

		if mirror_config.optional_repositories:
			title = tr('Optional repositories')
			divider = '-' * len(title)
			repos = ', '.join(r.value for r in mirror_config.optional_repositories)
			output += f'{title}\n{divider}\n{repos}\n\n'

		if mirror_config.custom_repositories:
			title = tr('Custom repositories')
			table = FormattedOutput.as_table(mirror_config.custom_repositories)
			output += f'{title}:\n\n{table}'

		return output.strip()
