'''
EDA CLI - from running

> eda [--debug] (enter)

Textual rich terminal app for EDA, support for
 - running commands (target sidebar)
 - viewing work artifacts (jobs sidebar)
 - interative terminal help (help sidebar)
 - CLI autocomplete with fanciness beyond bash autocomplete
'''

# pylint: disable=logging-fstring-interpolation

import copy
import json
import logging
import os
import shlex
import sys

from textual import on, events, work # Import decorators
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Static, Label, Header, Footer, Input, RichLog, Tree
from textual.containers import Vertical, VerticalScroll, Horizontal

from textual_pyfiglet import FigletWidget

from opencos import __version__ as opencos_version
from opencos import util, eda_tool_helper
from opencos.utils import subprocess_helpers, str_helpers
from opencos.eda_extract_targets import get_targets
from opencos.eda_base import get_eda_exec

from opencos.tui.tui_tool_picker import ToolPickerModal, ActionDetailsModal, get_tool_arg
from opencos.tui.tui_text_viewer import TextViewerModal
from opencos.tui.tui_markdown_viewer import CustomHelpViewer, get_opencos_docs_fpath

# Manual debug logging:
_TUI_DEBUG: bool = False
if _TUI_DEBUG:
    logging.basicConfig(filename=".tui_debug.log", level=logging.DEBUG)
logger = logging.getLogger("eda_tui")
logger.debug("Starting MainScreen Mount...")


def get_latest_mtime(directory) -> int:
    '''Walk through all files in the job directory and subdirectories'''
    latest = 0
    for root, _, files in os.walk(directory):
        for f in files:
            mtime = os.path.getmtime(os.path.join(root, f))
            latest = max(latest, mtime)
    return latest


def artifacts_type_to_label(typ: str, leafname: str) -> str:
    '''Returns a str like: Log shortname.txt'''
    _map = {
        'text': 'Log',
        'waveform': 'Waves',
        'shell': 'Shell',
        'json': 'JSON',
        'yaml': 'YAML',
        'dotf': 'DotFile',
        'coverage_database': 'Coverage DB'
    }
    if typ in _map:
        return f'{_map[typ]}: {leafname}'

    return f'{leafname}'


def get_label_eda_errors_json(
        eda_errors_json_filepath: str, job_id: str
) -> str:
    '''Returns an emoji'ifed string for job-tree based on eda_errors.json'''
    if eda_errors_json_filepath and os.path.exists(eda_errors_json_filepath):
        data = {}
        with open(eda_errors_json_filepath, encoding='utf-8', errors='replace') as f:
            data = json.load(f)
        if all(x in data for x in ('errors', 'warnings')):
            if data['errors'] or ('tool_errors' in data and data['tool_errors']):
                return f"📁[red]✘ {job_id}[/]"
            if data['warnings'] or ('tool_warnings' in data and data['tool_warnings']):
                return f'📁[yellow]✔[/] {job_id}'

            # Appears to pass, set in all green
            return f'📁[green]✔ {job_id}[/]'

        # we didn't have 'errors' or 'warnings' in eda_errors.json,
        # perhaps it didn't run?
        return f'📁[green]-[/] {job_id}'

    # We didn't have eda_errors.json at all?
    return f'📁[gray]- {job_id}[/]'



class SplashScreen(Screen):
    '''Textual class handler for the splash screen with EDA'''

    # This allows the screen itself to receive keyboard focus
    can_focus = True

    def on_mount(self) -> None:
        '''Handler for mounting the splash screen'''
        try:
            # 1. Focus the screen so it can "hear" keys immediately
            self.focus()
        except: # pylint: disable=bare-except
            pass

    def manual_exit(self) -> None:
        '''Handler for manually exiting the splash screen (on key press or click)'''
        self.app.switch_to_main()

    def on_key(self, event) -> None: # pylint: disable=unused-argument
        """Any key press will dismiss the splash screen immediately."""
        self.manual_exit()

    def on_click(self) -> None:
        """Bonus: Clicking the splash screen also advances it."""
        self.manual_exit()

    def _get_tools_rows(self) -> list:
        # From eda_tool_helper:
        rows = eda_tool_helper.pretty_info_handler_tools(
            config=self.app.config, no_print_get_rows=True
        )
        tools_str_rows = []
        for rownum, row in enumerate(str_helpers.pretty_2dlist_columns(
                rows, return_as_2d_list=True, header_row_centered=False)):
            if rownum == 0:
                tools_str_rows.append("".join(row))
            elif row[0].startswith('--'):
                pass # skip these
            else:
                tools_str_rows.append(f'{row[0]}' + ''.join(row[1:]))
        return tools_str_rows

    def _get_config_rows(self) -> list:
        ret = [
            f'Run from: {self.app.config["tui"]["run_from"]}',
            (f'Python version: {sys.version_info.major}.{sys.version_info.minor}.'
             f'{sys.version_info.micro}'),
            f'EDA from: {self.app.config["tui"]["eda_from"]}',
            f'Config from: {self.app.config["config-yml"]}'
        ]
        if x := self.app.config.get("partial-config-yml", None):
            ret.append(f'Partial config from: {x}')
        return ret


    def compose(self) -> ComposeResult:

        tools_str_rows = self._get_tools_rows()
        config_str_rows = self._get_config_rows()

        with Vertical(id="splash-card"):
            yield FigletWidget(
                "eda", font="dos_rebel", justify="center",
                colors=["orange", #FFA500", # orange
                        "white"] # #A0A0A0"] # white/gray
            )
            yield Static(f"eda-ui (part of opencos-eda) v{opencos_version}\n", id="splash-subtitle")
            with VerticalScroll(id="tool-scroll-area"):
                yield Label(tools_str_rows[0], id='splash-tools-header')
                yield Label('\n'.join(tools_str_rows[1:]) + '\n', id='splash-tools')
                yield Label('\n'.join(config_str_rows), id='splash-eda-info')



class MainScreen(Screen):
    '''Main screen (terminal style command line + sidebars)'''


    TITLE = "EDA (opencos-eda)"

    BINDINGS = [
        ("h,?", "toggle_help", "Help"),
        ("j", "toggle_job_sidebar", "Jobs"),
        ("t", "toggle_target_sidebar", "Targets"),
        ("J", "widen_jobs", "Widen Jobs"),
        ("T", "widen_targets", "Widen Targets"),
        ("H", "widen_help", "Widen Help"),
        ("r", "reset_layout", "Reset Layout"),
        ("ctrl+y", "copy_log", "Copy Log")
    ]

    _adjust_width_size: int = 20

    def __init__(self):
        super().__init__()
        self.history = []
        self.history_index = 0
        self.last_help_doc = None

    def _clear_cli(self) -> None:
        '''Clears CLI'''
        self.query_one("#cmd-input").value = ""


    def _apply_cli_history_index(self) -> None:
        '''Replaces CLI with history at index, moves cursor to end'''
        if self.history and self.history_index >= 0 and self.history_index < len(self.history):
            self._apply_cli_line(self.history[self.history_index])


    def _apply_cli_line(self, line: str, focus: bool = False) -> None:
        '''Replaces CLI with line, moves cursor to end'''
        cli = self.query_one('#cmd-input')
        cli.value = line.strip()
        if focus:
            cli.focus()
        cli.cursor_position = len(cli.value)


    def _command_line_to_cli(self, line: str) -> None:
        '''
        Adds command_line (str) to the CLI prompt

        This makes it feel more like a helpful/tutorial CLI, as opposed to running
        magic hidden commands in the background. Forces user to hit enter key on CLI
        to run. This also forces it to add to CLI history.

        If you want to run the command instead:
            self.run_eda_command(command_line.strip())
        '''
        self._apply_cli_line(line, focus=True)


    def on_key(self, event: events.Key) -> None:
        '''
        Handle key presses:
        -- up/down for CLI history
        -- tab
            TODO(drew): Handle smart suggestions from Tab-autocomplete
            Do NOT switch widgets on Tab, which is default Textual behavior
            (this is not implemented)
        '''
        if event.key == "up":
            if self.history and self.history_index > 0:
                self.history_index -= 1
                self._apply_cli_history_index()
        elif event.key == "down":
            if self.history_index < len(self.history) - 1:
                self.history_index += 1
                self._apply_cli_history_index()
            else:
                self.history_index = len(self.history)
                self._clear_cli()

        # TODO(drew): tab disabled, until working.
        ##if event.key == "tab":
        ##    input_widget = self.query_one("#cmd-input", Input)
        ##    # If there's a grayed-out suggestion, Tab fills it in
        ##    if input_widget.suggestion:
        ##        input_widget.value = input_widget.suggestion
        ##        # Prevent focus from leaving the input
        ##        event.prevent_default()


    def _adjust_width(self, selector: str, delta: int) -> None:
        sidebar = self.query_one(selector)
        # Convert the current width to an integer and shift it
        current = sidebar.styles.width.value
        # Clamp it so it doesn't disappear or take over the whole screen
        new_width = max(40, min(current + delta, 100))
        sidebar.styles.width = new_width


    def action_widen_jobs(self) -> None:
        '''Handler for widening the job-tree sidebar'''
        if not isinstance(self.screen, MainScreen):
            return
        self._adjust_width("#job-tree", self._adjust_width_size)
        self._set_right_sidebars_width()


    def action_widen_targets(self) -> None:
        '''Handler for widening the target-tree sidebar'''
        if not isinstance(self.screen, MainScreen):
            return
        self._adjust_width("#target-tree", self._adjust_width_size)


    def action_widen_help(self) -> None:
        '''Handler for widening the help-sidebar'''
        if not isinstance(self.screen, MainScreen):
            return
        self._adjust_width("#help-sidebar", self._adjust_width_size)
        self._set_right_sidebars_width()


    def action_reset_layout(self) -> None:
        '''Handler for reseting sidebar(s) width'''
        if not isinstance(self.screen, MainScreen):
            return
        self.query_one("#job-tree").styles.width = 40
        self.query_one("#target-tree").styles.width = 40
        self.query_one("#help-sidebar").styles.width = 40
        self._set_right_sidebars_width()



    def action_copy_log(self) -> None:
        '''Keybind support for copying the log screen to clipboard'''
        if not isinstance(self.screen, MainScreen):
            return
        log = self.query_one("#output-log", RichLog)
        # Join all lines in the log and send to system clipboard
        content = "\n".join(line.text for line in log.lines)
        self.app.copy_to_clipboard(content)
        self.notify("Log copied to clipboard!")


    def action_toggle_target_sidebar(self) -> None:
        '''Handler for toggling / focus on target-tree sidebar'''
        if not isinstance(self.screen, MainScreen):
            return
        sidebar = self.query_one("#target-tree")
        sidebar.display = not sidebar.display
        # If showing, give it focus so we can use arrow keys
        if sidebar.display:
            sidebar.focus()


    def _load_markdown_help_file(self, filename: str):
        '''Markdown render file on help-sidebar'''
        sidebar = self.query_one('#help-sidebar')
        docs_fpath = get_opencos_docs_fpath(filename)

        if not docs_fpath:
            return

        self.last_help_doc = docs_fpath
        self.call_after_refresh(sidebar.go, str(docs_fpath))


    def _set_right_sidebars_width(self) -> int:
        '''Sets the width for the right-sidebars, returns the width'''
        if not isinstance(self.screen, MainScreen):
            return
        container = self.query_one("#right-sidebars")
        jobs_visible = self.query_one("#job-tree").display
        docs_visible = self.query_one("#help-sidebar").display
        new_width = 0
        if jobs_visible:
            new_width += self.query_one("#job-tree").styles.width.value
        if docs_visible:
            new_width += self.query_one("#help-sidebar").styles.width.value
        container.styles.width = new_width
        return new_width


    def _toggle_right_sidebars(self, sidebar_id: str) -> None:
        """Toggles a specific sidebar and syncs the parent container."""
        if not isinstance(self.screen, MainScreen):
            return
        sidebar = self.query_one(sidebar_id)
        container = self.query_one("#right-sidebars")

        # 1. Flip the specific sidebar's visibility
        sidebar.display = not sidebar.display

        # 2. Check if EITHER sidebar is now visible
        new_width = self._set_right_sidebars_width()

        # 3. Update the parent container
        container.display = bool(new_width)

        # 3b. For the help-sidebar, load default or most recent MD file
        if sidebar_id == '#help-sidebar' and sidebar.display:
            # Load your README or main doc
            if not self.last_help_doc:
                self._load_markdown_help_file('README.md')
            else:
                self._load_markdown_help_file(self.last_help_doc.name)

        # 4. Focus the sidebar if it was just turned on (so keys work)
        if sidebar.display:
            sidebar.focus()

    def action_toggle_job_sidebar(self) -> None:
        '''Handler for toggling / focus on job-tree sidebar'''
        self._toggle_right_sidebars("#job-tree")


    def action_toggle_help(self):
        '''Toggles the right Sidebar for help'''
        self._toggle_right_sidebars("#help-sidebar")


    def compose(self) -> ComposeResult:
        yield Header()
        with Horizontal():
            # Left sidebar, Targets from DEPS.yml (starts hidden or visible)
            yield Tree("Targets", id="target-tree")

            # Main CLI:
            with Vertical(id="terminal-area"):
                yield RichLog(id="output-log", highlight=True, markup=True)
                yield Input(
                    placeholder="-- enter 'eda' command or args, 'help', or Ctrl+Q to Quit --",
                    value="",
                    # TODO(drew): this does not work yet:
                    #suggester=EDASuggester(self.app.original_config),
                    id="cmd-input"
                )

            # Right sidebars
            with Horizontal(id='right-sidebars'):
                # Job Browser (starts hidden or visible)
                yield Tree("EDA Work dirs", id="job-tree")
                # Right sidebar, Help (starts hidden)
                yield CustomHelpViewer(
                    "# Help\nLoading documentation...",
                    id='help-sidebar'
                )

        yield Footer()


    def _populate_target_tree( # pylint: disable=too-many-locals
            self
    ) -> None:
        '''Method for getting information into the target-tree sidebar'''

        tree = self.query_one("#target-tree", Tree)

        tree.clear()
        tree.root.expand()

        nodes_dict = {"": tree.root}

        # Get all targets:
        for root, dirs, files in os.walk(os.path.relpath(os.getcwd())):
            # Skip dirs we don't care about, starting with . or eda.
            dirs[:] = [d for d in dirs if not d.startswith('.') and not d.startswith('eda.')]

            if any(x in files for x in ('DEPS.yml', 'DEPS.yaml', 'DEPS.toml', 'DEPS.json')):

                data = None
                try:
                    # get targets
                    data = get_targets(partial_paths=[], base_path=root, lstrip_path=False,
                                       no_warn=True)
                    data.sort()
                except Exception as e:
                    self.log(f'Unable to get DEPS targets from directory: {root}, {e}')

                if not data:
                    continue

                rel_path = os.path.normpath(root)
                parts = rel_path.split(os.sep)

                current_path = ''
                for part in parts:
                    parent_path = current_path
                    # Build the path string for the dict key (e.g., "a", then "a/b")
                    current_path = os.path.join(current_path, part) if current_path else part
                    if current_path not in nodes_dict:
                        parent_node = nodes_dict[parent_path]
                        nodes_dict[current_path] = parent_node.add(f"📁 {part}", expand=False)

                folder_node = nodes_dict[rel_path]
                for target in data:
                    folder_node.add_leaf(f"🎯 {target}", data={"path": root, "target": target})

                # TODO(drew): might be neat to add .f files here too?


    def _populate_job_tree( # pylint: disable=too-many-locals,too-many-branches
            self
    ) -> None:
        '''Method for getting information into the job-tree sidebar'''

        tree = self.query_one("#job-tree", Tree)

        if not tree.root.is_expanded:
            tree.root.expand()

        # Unfortunately we can't hot check the root nodes' labels
        # because those have pass/fail emojis in them, so instead
        # confirm them by checking for data with 'job-id-work-dir'
        existing_nodes = {}
        for node in tree.root.children:
            if 'job-id-work-dir' in node.data:
                existing_nodes[node.data['job-id-work-dir']] = node

        # Simple scan logic:
        work_dir = self.app.config.get('args', {}).get('work_dir', 'eda.work')

        if not os.path.exists(work_dir):
            return

        # When populating your tree:
        job_dirs = [d for d in os.listdir(work_dir) if os.path.isdir(os.path.join(work_dir, d))]
        # Sort by latest modification time (newest first)
        sorted_jobs = sorted(
            job_dirs, key=lambda d: get_latest_mtime(os.path.join(work_dir, d)), reverse=True
        )


        for job_id in sorted_jobs:

            job_path = os.path.join(work_dir, job_id)
            if not os.path.isdir(job_path):
                continue

            # Check for artifacts.json
            art_path = os.path.join(work_dir, job_id, "artifacts.json")
            _eda_errors_json = None
            if os.path.exists(art_path):

                if os.path.dirname(art_path) == work_dir:
                    continue

                if job_id not in existing_nodes:
                    job_node = tree.root.add(
                        job_id, expand=False, data={'job-id-work-dir': job_id}
                    )
                else:
                    job_node = existing_nodes[job_id]

                existing_leaves = {}
                for child in job_node.children:
                    if child and not child.children:
                        existing_leaves[f'{child.label}'] = child

                data = {}
                with open(art_path, encoding='utf=8', errors='replace') as f:
                    data = json.load(f)

                for fname, table in data.items():
                    fullpath = table.get('name')
                    typ = table.get('type')
                    leafname = os.path.split(fullpath)[-1]
                    table['job_id'] = job_id # make sure this is set in existing_nodes
                    label = artifacts_type_to_label(typ, leafname)
                    if typ == 'json' and fname == 'eda_errors.json':
                        _eda_errors_json = fullpath

                    if label in existing_leaves:
                        _leaf = existing_leaves[label]
                        _leaf.data = table
                        _leaf.set_label(label) # re-set it.
                    else:
                        job_node.add_leaf(label, data=table)


                # Update the job_id node's label with pass/fail and colorization:
                if job_node:
                    job_node.set_label(get_label_eda_errors_json(_eda_errors_json, job_id=job_id))


    def on_mount(self) -> None:
        '''Handler for mounting/starting the main screen'''
        try:
            self.call_after_refresh(self.setup_ui)
        except: # pylint: disable=bare-except
            pass


    def setup_ui(self) -> None:
        '''
        Setup for the main screen UI.
          -- cmd-input
          -- target-tree
          -- job-tree
        '''
        output_log = None
        try:
            output_log = self.query_one("#output-log", RichLog)

            # Set-up opencos.util so its aware of this log now
            util.set_tui_log_widget(output_log)

            # focus on CLI, this currently disabled so you can see bindings.
            # self.query_one("#cmd-input").focus()

            # Handle the sidebar for a target-tree
            self._populate_target_tree()

            # Handle the sidebar for a Job-tree
            self._populate_job_tree()

        except: # pylint: disable=bare-except
            # Don't crash on rendering
            pass


    @on(Input.Submitted, "#cmd-input")
    def handle_command(self, event: Input.Submitted) -> None:
        '''How we handle hitting ENTER key on the CLI style text input'''
        self.log("Message reached handle_submit!") # Check textual console for this

        line = event.value.strip()
        if not line:
            return

        self.history.append(line)
        self.history_index = len(self.history)

        self.query_one(Input).value = ""

        util.info(f'🚀 Lauching eda for: {line=}')

        # Start the engine in a background worker thread
        self.run_eda_command(line)

    @work(thread=True)
    def run_eda_command(self, command_line: str) -> None:
        '''Runs eda in subprocess'''
        cmd = shlex.split(command_line)
        log = self.query_one("#output-log", RichLog)

        if cmd[0] != 'eda':
            cmd = [get_eda_exec()] + cmd

        if '--debug' in self.app.config['tui']['original_args']:
            cmd.append('--debug')

        try:
            # We run this in the worker thread.
            # It creates the files, logs, and artifacts externally.
            util.debug(f'🚀 Lauching subprocess for: {cmd=}')

            if util.args['color']:
                env = os.environ.copy()
                env["FORCE_COLOR"] = "1"          # Forces Rich to use ANSI colors
                env["TERM"] = "xterm-256color"    # Ensures a color-capable profile
            else:
                env = None

            _, _, rc = subprocess_helpers.subprocess_run_background(
                work_dir=os.getcwd(),
                command_list=cmd,
                background=False,
                shell=True,
                env=env
            )

            if rc > 0:
                util.args['errors'] += 1
                util.max_error_code = max(util.max_error_code, rc)

        except util.EdaErrorExit as e:
            # This caught the 'soft exit'—engine stopped, TUI stays alive
            self.app.call_from_thread(log.write, f"\n[bold italic red]-- {e} --[/]\n")

        except Exception as e:
            self.app.call_from_thread(util.error, f"Execution failed: {e}")

        finally:
            # Always refocus the input so the user can type again
            self.app.call_from_thread(self.query_one("#cmd-input").focus)
            # Once finished, refresh the Job Tree to show new artifacts
            self.app.call_from_thread(self._populate_job_tree)


    @on(Tree.NodeSelected, "#job-tree")
    def handle_job_tree_selection(self, event: Tree.NodeSelected) -> None:
        '''Handles mouse clicks on the job-tree artifacts files'''
        node_data = None
        try:
            node_data = event.node.data
        except: # pylint: disable=bare-except
            pass

        if not node_data or not isinstance(node_data, dict) or 'type' not in node_data:
            return

        logger.debug(f"selected: {node_data=}")
        try:
            file_path = node_data.get('name', '')
            if not file_path:
                logger.debug(f'Can not open type for {node_data=} yet, name (file_path) empty')
            elif node_data.get('type') == 'waveform':
                logger.debug(f"Attempting to run_eda_waves on: {node_data=}")
                self.job_tree_waves_selection(file_path)
            elif node_data.get('type') in ('text', 'json', 'yaml', 'dotf', 'tcl', 'shell'):
                self.app.push_screen(TextViewerModal(file_path))
            else:
                # default
                logger.debug(f'Can not open type for {node_data=} yet')
        except Exception as e:
            logger.debug(f'Exception: {e}')


    def job_tree_waves_selection(self, file_path: str) -> None:
        '''Handles how we load a file_path for waves

        Has to use ToolPickerModal to find loaded tools that can open this
        file extension
        '''
        _, ext = os.path.splitext(file_path)

        info = self.app.tool_info
        allowed_waves_tools = info['commands']['waves']
        tools_that_support_this_ext = []

        for tool, _ in allowed_waves_tools.items():
            if tool in self.app.config.get('tools', {}):
                _supported_exts = self.app.config.get('tools', {}).get(tool, {}).get(
                    'waves-file-ext', []
                )
                if ext in _supported_exts:
                    tools_that_support_this_ext.append(tool)

        # If there are no tools_that_support_this_ext, then the
        # ToolPickerModal has a 'DEFAULT (none)' option always.

        # Push modal with your filtered tools
        def on_tool_selected(tool: str) -> None:
            cmd = f"waves {file_path}"
            tool_arg = get_tool_arg(tool)
            if tool_arg:
                cmd += f" {tool_arg}"
            self._command_line_to_cli(cmd)

        self.app.push_screen(
            ToolPickerModal(file_path, tools_that_support_this_ext, add_default=True),
            callback=on_tool_selected
        )


    @on(Tree.NodeSelected, "#target-tree")
    def handle_target_tree_selection(self, event: Tree.NodeSelected) -> None:
        '''
        This handles mouse click events on the Target sidebar tree

        Steps are
          -- get available commands, get full target name
          -- given command selected, get tools that support that command
          -- given command + tools, get additional details (args, --waves)
        '''
        target_data = None
        try:
            target_data = event.node.data
        except: # pylint: disable=bare-except
            pass

        if not target_data or not isinstance(target_data, dict):
            return

        # 1. Get all available commands based on tools/commands we know of:
        info = self.app.tool_info
        available_cmds_dict = info['commands']
        available_cmds = list(available_cmds_dict.keys())
        available_cmds.sort()

        full_target_name = target_data['target']
        if target_data['path']:
            full_target_name = f'{target_data["path"]}/{full_target_name}'
        full_target_name = full_target_name.replace('/./', '/')

        def on_command_selected(cmd: str) -> None:
            if not cmd:
                return

            # 2. Get tools specific to this command/target from config
            available_tools = list(available_cmds_dict.get(cmd, {}).keys())

            def on_tool_selected(tool: str) -> None:
                if not tool:
                    return

                tool_arg = get_tool_arg(tool)

                # 3. Push the Details Modal (do you want waves, additional args?)
                def on_details_confirmed(details) -> None:
                    if not details:
                        return
                    # Assemble the final command with the extra args
                    extra_args = details["args"]
                    waves_flag = ''
                    if details.get('waves', None):
                        waves_flag = "--waves"

                    full_cmd = (
                        f"{cmd} {full_target_name} {tool_arg} {extra_args} {waves_flag}"
                    )

                    # Put in CLI instead:
                    self._command_line_to_cli(full_cmd)

                self.app.push_screen(
                    ActionDetailsModal(f'{cmd} {tool_arg} {full_target_name}', cmd, tool),
                    callback=on_details_confirmed
                )

            self.app.push_screen(
                ToolPickerModal(f"{cmd} {full_target_name}", available_tools, add_default=True),
                callback=on_tool_selected
            )

        self.app.push_screen(
            ToolPickerModal(full_target_name, available_cmds, add_default=False),
            callback=on_command_selected
        )


class EDAApp(App):
    '''Overall App (splash screen + Main screen) for eda-ui'''

    BINDINGS = [
        ("ctrl+p", "command_palette", "Command Palette"),
        ("s,a", "show_splash", "Splash/About"),
    ]

    CSS_PATH = "tui_eda.tcss"


    def __init__(self, config):
        super().__init__()
        self.config = config
        self.original_config = copy.deepcopy(config) # do this once so we have a config w/ no tools

        self.tool_info = eda_tool_helper.get_handler_info_with_versions(
            config=self.app.config, include_commands=True, sort=True,
            all_details=True
        )

    def on_mount(self) -> None:
        '''
        Handles mounting the app. Starts splash screen, Splash screen will
        call self.app.switch_to_main()
        '''
        self.push_screen(SplashScreen())


    def switch_to_main(self) -> None:
        """Call this from the SplashScreen timer/key handler."""
        if isinstance(self.screen, MainScreen):
            return
        if not self.is_running:
            return
        self.switch_screen(MainScreen())


    def action_show_splash(self) -> None:
        """Pushes the splash screen back onto the top of the stack."""
        if isinstance(self.screen, SplashScreen):
            return
        if not self.is_running:
            return
        self.switch_screen(SplashScreen())


    async def on_unmount(self) -> None:
        '''
        Handles unmount (exit) from the main app. Makes sure we clean up

        globals that were set in util, and kills all subprocess_helpers PIDs that we
        know about
        '''
        # Before the app fully dies, tell util to stop trying to use it
        util.set_tui_context(None)
        util.set_tui_log_widget(None)

        # Run common opencos cleanup:
        subprocess_helpers.cleanup_all()
