# filebundler/FileBundlerApp.py
import asyncio
import logging
import logfire
import streamlit as st

from pathlib import Path
from typing import Dict, List, Optional, Tuple

# from filebundler.features import tasks
from filebundler.features.sort_files import sort_files
from filebundler.features.ignore_patterns import should_include_path

from filebundler.models.FileItem import FileItem
from filebundler.models.AppProtocol import AppProtocol

from filebundler.managers.BundleManager import BundleManager
from filebundler.managers.SelectionsManager import SelectionsManager
from filebundler.managers.ProjectSettingsManager import ProjectSettingsManager

from filebundler.ui.notification import show_temp_notification


logfire.configure(send_to_logfire='if-token-present', console=False)
logger = logging.getLogger(__name__)


# class ProjectManager
class FileBundlerApp(AppProtocol):
    def __init__(self, project_path: Path):
        self.project_path = project_path.resolve()
        self.psm = ProjectSettingsManager(self.project_path)

        # Validate project path after settings are loaded
        self.path_validation_result = self.psm.validate_project_path()

        # Handle path validation issues - this will be checked in UI
        if not self.path_validation_result.is_valid:
            logger.warning(f"Path validation issues detected: {self.path_validation_result.issues}")

        self.file_items: Dict[Path, FileItem] = {}
        self._highest_token_item: Optional[FileItem] = None

        # Load the directory structure
        self.root_item = FileItem(
            path=self.project_path,
            project_path=self.project_path,
            children=[],
            parent=None,
            selected=False,
        )
        self.file_items[self.project_path] = self.root_item
        self.load_directory_recursive(
            self.project_path,
            self.root_item,
        )

        self.bundles = BundleManager(app=self)
        self.selections = SelectionsManager(app=self)

        # this is an optional feature that should be triggered by CLI if the user so chooses
        # tasks.copy_templates(filebundler_dir=self.psm.filebundler_dir)

        logger.info(
            f"FileBundlerApp initialized with project path: {self.project_path}"
        )

    def update_project_path_if_needed(self, new_path: Path) -> bool:
        """
        Update the project path if it has changed and refresh the app.

        Args:
            new_path: The new project path to use

        Returns:
            True if update was successful, False otherwise
        """
        try:
            success = self.psm.update_project_path(new_path)
            if success:
                # Refresh the validation result
                self.path_validation_result = self.psm.validate_project_path()
                logger.info(f"Project path updated to: {new_path}")
            return success
        except Exception as e:
            logger.error(f"Error updating project path: {e}")
            return False

    def refresh(self):
        """
        Refresh the project by reloading the directory structure and selections.
        """
        self.__init__(self.project_path)
        st.rerun()

    @property
    def highest_token_item(self):
        return self._highest_token_item

    @property
    def nr_of_files(self):
        return len([fi for fi in self.file_items.values() if not fi.is_dir])

    def clear_all_selections(self):
        self.selections.clear_all_selections()
        self.bundles.current_bundle = None

    async def _load_directory_recursive_async(self, dir_path: Path, parent_item: FileItem) -> bool:
        """
        Async version of directory loading with parallel subdirectory processing.

        Args:
            dir_path: Directory to scan
            parent_item: Parent FileItem to attach children to

        Returns:
            bool: True if directory has visible content, False otherwise
        """
        try:
            with logfire.span(
                "loading directory {dir_path}",
                dir_path=dir_path.relative_to(self.project_path),
                _level="debug",
            ):
                # Iterate directory
                filtered_filepaths: List[Path] = []
                for filepath in dir_path.iterdir():
                    rel_path = filepath.relative_to(self.project_path).as_posix()
                    if should_include_path(
                        rel_path, self.psm.project_settings.include_patterns
                    ):
                        filtered_filepaths.append(filepath)

                # Apply max_files limit with warning
                if len(filtered_filepaths) > self.psm.project_settings.max_files:
                    st.warning(
                        f"Directory contains {len(filtered_filepaths)} files, exceeding limit of {self.psm.project_settings.max_files}. "
                        f"Truncating to {self.psm.project_settings.max_files} files."
                    )
                    sorted_filepaths = sort_files(
                        filtered_filepaths, self.psm.project_settings
                    )[: self.psm.project_settings.max_files]
                else:
                    sorted_filepaths = sort_files(
                        filtered_filepaths, self.psm.project_settings
                    )

                has_visible_content = False
                subdirectory_tasks: List[Tuple[Path, FileItem]] = []

                # Process files and collect subdirectory tasks
                for filepath in sorted_filepaths:
                    try:
                        file_item = FileItem(
                            path=filepath,
                            project_path=self.project_path,
                            parent=parent_item,
                            children=[],
                            selected=False,
                        )

                        if filepath.is_dir():
                            # Schedule subdirectory processing for parallel execution
                            subdirectory_tasks.append((filepath, file_item))
                        else:
                            # Process files immediately
                            self.file_items[filepath] = file_item
                            parent_item.children.append(file_item)
                            has_visible_content = True

                            # Track highest token item during loading
                            if not self._highest_token_item or file_item.tokens > self._highest_token_item.tokens:
                                self._highest_token_item = file_item

                    except (PermissionError, OSError):
                        show_temp_notification(
                            f"Error accessing {filepath.relative_to(self.project_path)}",
                            type="error",
                        )
                        continue

                # Process all subdirectories in parallel
                if subdirectory_tasks:
                    subdirectory_results = await asyncio.gather(
                        *[
                            self._load_directory_recursive_async(subdir_path, subdir_item)
                            for subdir_path, subdir_item in subdirectory_tasks
                        ],
                        return_exceptions=True
                    )

                    # Add subdirectories that have content
                    for (subdir_path, subdir_item), result in zip(subdirectory_tasks, subdirectory_results):
                        if isinstance(result, Exception):
                            show_temp_notification(
                                f"Error processing {subdir_path.relative_to(self.project_path)}: {result}",
                                type="error",
                            )
                            continue

                        if result:
                            self.file_items[subdir_path] = subdir_item
                            parent_item.children.append(subdir_item)
                            has_visible_content = True

                # Clean up empty directories
                if not has_visible_content and dir_path in self.file_items:
                    del self.file_items[dir_path]

                return has_visible_content

        except Exception as e:
            st.error(f"Error loading directory {dir_path}: {str(e)}")
            return False

    def load_directory_recursive(self, dir_path: Path, parent_item: FileItem):
        """
        Synchronous wrapper for async directory loading.

        Args:
            dir_path: Directory to scan
            parent_item: Parent FileItem to attach children to

        Returns:
            bool: True if directory has visible content, False otherwise
        """
        return asyncio.run(self._load_directory_recursive_async(dir_path, parent_item))

    def paths_to_file_items(self, paths: List[Path]):
        file_items: List[FileItem] = []
        for file_path in paths:
            file_item = self.file_items.get(file_path.resolve())
            if file_item:
                file_items.append(file_item)
            else:
                logger.warning(
                    f"the following file doesn't exist in the app: {file_path.resolve()}"
                )
                #             show_temp_notification(
                #                 f"The following file doesn't exist in the app: {file_path.resolve()}. "
                #                 f"""Reasons why you may be seeing this notification:
                # - because an LLM response included a file that was not in the app
                # - because the file was removed and the app has not been refreshed
                # - because a bundle that loaded contained a file that was deleted""",
                #                 type="error",
                #             )
                continue
        return file_items
