from __future__ import annotations

from collections.abc import Awaitable, Callable
from typing import Any, TypeVar, override

from textual import work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Center, Horizontal, Vertical
from textual.events import Key
from textual.screen import Screen
from textual.widgets import Button, DataTable, Input, LoadingIndicator, Static

from archinstall.lib.output import debug
from archinstall.lib.translationhandler import tr
from archinstall.tui.menu_item import MenuItem, MenuItemGroup
from archinstall.tui.ui.result import Result, ResultType

ValueT = TypeVar('ValueT')


class BaseScreen(Screen[Result[ValueT]]):
	BINDINGS = [  # noqa: RUF012
		Binding('escape', 'cancel_operation', 'Cancel', show=True),
		Binding('ctrl+c', 'reset_operation', 'Reset', show=True),
	]

	def __init__(self, allow_skip: bool = False, allow_reset: bool = False):
		super().__init__()
		self._allow_skip = allow_skip
		self._allow_reset = allow_reset

	def action_cancel_operation(self) -> None:
		if self._allow_skip:
			self.dismiss(Result(ResultType.Skip, None))  # type: ignore[unused-awaitable]

	def action_reset_operation(self) -> None:
		if self._allow_reset:
			self.dismiss(Result(ResultType.Reset, None))  # type: ignore[unused-awaitable]

	def _compose_header(self) -> ComposeResult:
		"""Compose the app header if global header text is available."""
		if tui.global_header:
			yield Static(tui.global_header, classes='app-header')


class LoadingScreen(BaseScreen[None]):
	CSS = """
	LoadingScreen {
		align: center middle;
	}

	.dialog {
		align: center middle;
		width: 100%;
		border: none;
		background: transparent;
	}

	.header {
		text-align: center;
		margin-bottom: 1;
	}

	LoadingIndicator {
		align: center middle;
	}
	"""

	def __init__(
		self,
		timer: int,
		header: str | None = None,
	):
		super().__init__()
		self._timer = timer
		self._header = header

	async def run(self) -> Result[None]:
		return await tui.show(self)

	@override
	def compose(self) -> ComposeResult:
		yield from self._compose_header()

		with Center():
			with Vertical(classes='dialog'):
				if self._header:
					yield Static(self._header, classes='header')
				yield Center(LoadingIndicator())  # ensures indicator is centered too

	def on_mount(self) -> None:
		self.set_timer(self._timer, self.action_pop_screen)

	def action_pop_screen(self) -> None:
		self.dismiss()  # type: ignore[unused-awaitable]


class ConfirmationScreen(BaseScreen[ValueT]):
	BINDINGS = [  # noqa: RUF012
		Binding('l', 'focus_right', 'Focus right', show=True),
		Binding('h', 'focus_left', 'Focus left', show=True),
		Binding('right', 'focus_right', 'Focus right', show=True),
		Binding('left', 'focus_left', 'Focus left', show=True),
	]

	CSS = """
	ConfirmationScreen {
		align: center middle;
	}

	.dialog-wrapper {
		align: center middle;
		height: 100%;
		width: 100%;
	}

	.dialog {
		width: 80;
		height: 10;
		border: none;
		background: transparent;
	}

	.dialog-content {
		padding: 1;
		height: 100%;
	}

	.message {
		text-align: center;
		margin-bottom: 1;
	}

	.buttons {
		align: center middle;
		background: transparent;
	}

	Button {
		width: 4;
		height: 3;
		background: transparent;
		margin: 0 1;
	}

	Button.-active {
		background: #1793D1;
		color: white;
		border: none;
		text-style: none;
	}
	"""

	def __init__(
		self,
		group: MenuItemGroup,
		header: str,
		allow_skip: bool = False,
		allow_reset: bool = False,
	):
		super().__init__(allow_skip, allow_reset)
		self._group = group
		self._header = header

	async def run(self) -> Result[ValueT]:
		return await tui.show(self)

	@override
	def compose(self) -> ComposeResult:
		yield from self._compose_header()

		with Center(classes='dialog-wrapper'):
			with Vertical(classes='dialog'):
				with Vertical(classes='dialog-content'):
					yield Static(self._header, classes='message')
					with Horizontal(classes='buttons'):
						for item in self._group.items:
							yield Button(item.text, id=item.key)

	def on_mount(self) -> None:
		self.update_selection()

	def update_selection(self) -> None:
		focused = self._group.focus_item
		buttons = self.query(Button)

		if not focused:
			return

		for button in buttons:
			if button.id == focused.key:
				button.add_class('-active')
				button.focus()
			else:
				button.remove_class('-active')

	def action_focus_right(self) -> None:
		self._group.focus_next()
		self.update_selection()

	def action_focus_left(self) -> None:
		self._group.focus_prev()
		self.update_selection()

	def on_key(self, event: Key) -> None:
		if event.key == 'enter':
			item = self._group.focus_item
			if not item:
				return None
			self.dismiss(Result(ResultType.Selection, item.value))  # type: ignore[unused-awaitable]


class NotifyScreen(ConfirmationScreen[ValueT]):
	def __init__(self, header: str):
		group = MenuItemGroup([MenuItem(tr('Ok'))])
		super().__init__(group, header)


class InputScreen(BaseScreen[str]):
	CSS = """
	InputScreen {
	}

	.dialog-wrapper {
		align: center middle;
		height: 100%;
		width: 100%;
	}

	.input-dialog {
		width: 60;
		height: 10;
		border: none;
		background: transparent;
	}

	.input-content {
		padding: 1;
		height: 100%;
	}

	.input-header {
		text-align: center;
		margin: 0 0;
		color: white;
		text-style: bold;
		background: transparent;
	}

	.input-prompt {
		text-align: center;
		margin: 0 0 1 0;
		background: transparent;
	}

	Input {
		margin: 1 2;
		border: solid $accent;
		background: transparent;
		height: 3;
	}

	Input .input--cursor {
		color: white;
	}

	Input:focus {
		border: solid $primary;
	}
	"""

	def __init__(
		self,
		header: str,
		placeholder: str | None = None,
		password: bool = False,
		default_value: str | None = None,
		allow_reset: bool = False,
		allow_skip: bool = False,
	):
		super().__init__(allow_skip, allow_reset)
		self._header = header
		self._placeholder = placeholder or ''
		self._password = password
		self._default_value = default_value or ''
		self._allow_reset = allow_reset
		self._allow_skip = allow_skip

	async def run(self) -> Result[str]:
		return await tui.show(self)

	@override
	def compose(self) -> ComposeResult:
		yield from self._compose_header()

		with Center(classes='dialog-wrapper'):
			with Vertical(classes='input-dialog'):
				with Vertical(classes='input-content'):
					yield Static(self._header, classes='input-header')
					yield Input(
						placeholder=self._placeholder,
						password=self._password,
						value=self._default_value,
						id='main_input',
					)

	def on_mount(self) -> None:
		input_field = self.query_one('#main_input', Input)
		input_field.focus()

	def on_key(self, event: Key) -> None:
		if event.key == 'enter':
			input_field = self.query_one('#main_input', Input)
			value = input_field.value
			self.dismiss(Result(ResultType.Selection, value))  # type: ignore[unused-awaitable]


class TableSelectionScreen(BaseScreen[ValueT]):
	BINDINGS = [  # noqa: RUF012
		Binding('j', 'cursor_down', 'Down', show=True),
		Binding('k', 'cursor_up', 'Up', show=True),
	]

	CSS = """
	TableSelectionScreen {
		align: center middle;
		background: transparent;
	}

	DataTable {
		height: auto;
		width: auto;
		border: none;
		background: transparent;
	}

	DataTable .datatable--header {
		background: transparent;
		border: solid;
	}

	.content-container {
		width: auto;
		min-height: 10;
		min-width: 40;
		align: center middle;
		background: transparent;
	}

	.header {
		text-align: center;
		margin-bottom: 1;
	}

	LoadingIndicator {
		height: auto;
		background: transparent;
	}
	"""

	def __init__(
		self,
		header: str | None = None,
		data: list[ValueT] | None = None,
		data_callback: Callable[[], Awaitable[list[ValueT]]] | None = None,
		allow_reset: bool = False,
		allow_skip: bool = False,
		loading_header: str | None = None,
	):
		super().__init__(allow_skip, allow_reset)
		self._header = header
		self._data = data
		self._data_callback = data_callback
		self._loading_header = loading_header

		if self._data is None and self._data_callback is None:
			raise ValueError('Either data or data_callback must be provided')

	async def run(self) -> Result[ValueT]:
		return await tui.show(self)

	def action_cursor_down(self) -> None:
		table = self.query_one(DataTable)
		if table.cursor_row is not None:
			next_row = min(table.cursor_row + 1, len(table.rows) - 1)
			table.move_cursor(row=next_row, column=table.cursor_column or 0)

	def action_cursor_up(self) -> None:
		table = self.query_one(DataTable)
		if table.cursor_row is not None:
			prev_row = max(table.cursor_row - 1, 0)
			table.move_cursor(row=prev_row, column=table.cursor_column or 0)

	@override
	def compose(self) -> ComposeResult:
		yield from self._compose_header()

		with Center():
			with Vertical(classes='content-container'):
				if self._header:
					yield Static(self._header, classes='header', id='header')

				if self._loading_header:
					yield Static(self._loading_header, classes='header', id='loading-header')

				yield LoadingIndicator(id='loader')
				yield DataTable(id='data_table')

	def on_mount(self) -> None:
		self._display_header(True)
		data_table = self.query_one(DataTable)
		data_table.cell_padding = 2

		if self._data:
			self._put_data_to_table(data_table, self._data)
		else:
			self._load_data(data_table)

	@work
	async def _load_data(self, table: DataTable[ValueT]) -> None:
		assert self._data_callback is not None
		data = await self._data_callback()
		self._put_data_to_table(table, data)

	def _display_header(self, is_loading: bool) -> None:
		try:
			loading_header = self.query_one('#loading-header', Static)
			header = self.query_one('#header', Static)
			loading_header.display = is_loading
			header.display = not is_loading
		except Exception:
			pass

	def _put_data_to_table(self, table: DataTable[ValueT], data: list[ValueT]) -> None:
		if not data:
			self.dismiss(Result(ResultType.Selection, None))  # type: ignore[unused-awaitable]
			return

		cols = list(data[0].table_data().keys())  # type: ignore[attr-defined]
		table.add_columns(*cols)

		for d in data:
			row_values = list(d.table_data().values())  # type: ignore[attr-defined]
			table.add_row(*row_values, key=d)  # type: ignore[arg-type]

		table.cursor_type = 'row'
		table.display = True

		loader = self.query_one('#loader')
		loader.display = False
		self._display_header(False)
		table.focus()

	def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
		data: ValueT = event.row_key.value  # type: ignore[assignment]
		self.dismiss(Result(ResultType.Selection, data))  # type: ignore[unused-awaitable]


class TApp(App[Any]):
	CSS = """
	.app-header {
		dock: top;
		height: auto;
		width: 100%;
		content-align: center middle;
		background: $primary;
		color: white;
		text-style: bold;
	}
	"""

	def __init__(self) -> None:
		super().__init__(ansi_color=True)
		self._main = None
		self._global_header: str | None = None

	@property
	def global_header(self) -> str | None:
		return self._global_header

	@global_header.setter
	def global_header(self, value: str | None) -> None:
		self._global_header = value

	def set_main(self, main: Any) -> None:
		self._main = main

	def on_mount(self) -> None:
		self._run_worker()

	@work
	async def _run_worker(self) -> None:
		try:
			if self._main is not None:
				await self._main.run()  # type: ignore[unreachable]
		except Exception as err:
			debug(f'Error while running main app: {err}')
			raise err from err

	@work
	async def _show_async(self, screen: Screen[Result[ValueT]]) -> Result[ValueT]:
		return await self.push_screen_wait(screen)

	async def show(self, screen: Screen[Result[ValueT]]) -> Result[ValueT]:
		return await self._show_async(screen).wait()


tui = TApp()
