import asyncio
import importlib
import sys
from pathlib import Path
from types import ModuleType
from typing import Any


class UvRunManager(object):
    script: str
    verbose: bool

    def __init__(self, script: str, verbose: bool = False) -> None:
        self.script = script
        self.verbose = verbose

    def run(self) -> None:
        cwd = str(Path.cwd())

        if cwd not in sys.path:
            sys.path.append(cwd)

        if self.verbose:
            print(f'Running script: {self.script}')

        module_name, func_path = self._parse_script()

        try:
            if self.verbose:
                print(f'Importing module: {module_name}')

            # Import the module
            module = importlib.import_module(module_name)
            final_callable = self._resolve_callable(module, func_path)
            self._execute_callable(final_callable)

        except ImportError as e:
            if self.verbose:
                print(f'Error importing module: {e}')
            raise ImportError(f'Could not import module: {module_name}') from e

        except AttributeError as e:
            if self.verbose:
                print(f'Error getting attribute: {e}')
            raise AttributeError(f'Could not find callable in the script path: {e}') from e

        except Exception as e:
            if self.verbose:
                print(f'Error running script: {e}')
            raise e

    def _parse_script(self) -> tuple[str, str]:
        # Parse the script format: module:callable_path
        try:
            module_name, func_path = self.script.split(':', 1)
            return module_name, func_path
        except ValueError as e:
            raise ValueError(f"Invalid script format. Expected 'module:callable_path', got: {self.script}") from e

    def _resolve_callable(self, module: ModuleType, func_path: str) -> Any:
        parts = func_path.split('.')
        final_callable = None

        if len(parts) == 1:
            # Direct callable
            final_callable = getattr(module, parts[0])
            if self.verbose:
                print(f'Calling direct callable: {parts[0]}')

        elif len(parts) >= 2:
            # Get object from parts[:-1], then call method parts[-1]
            obj_getter_name = '.'.join(parts[:-1])
            method_name = parts[-1]

            # Get the obj_getter (could be nested, but in examples it's not)
            obj_getter = getattr(module, obj_getter_name)

            if self.verbose:
                print(f'Calling obj getter: {obj_getter_name}')

            # Call obj_getter, handling async
            if asyncio.iscoroutinefunction(obj_getter):
                obj = asyncio.run(obj_getter())
            else:
                obj = obj_getter()

            final_callable = getattr(obj, method_name)
            if self.verbose:
                print(f'Calling method: {method_name} on object')

        if final_callable is None:
            raise ValueError(f'Invalid func_path format: {func_path}')

        return final_callable

    def _execute_callable(self, final_callable: Any) -> None:
        # Check if it's a coroutine (async)
        if asyncio.iscoroutinefunction(final_callable):
            # Run async
            if self.verbose:
                print('Running async...')
            asyncio.run(final_callable())
        else:
            # Run sync
            if self.verbose:
                print('Running sync...')
            final_callable()
