"""
This module provides functionality for loading Large Language Model (LLM) configurations and creating remote model instances.

The module handles loading LLM configurations from config files or directories, and creates remote model instances
with appropriate parameters. It supports both pre-configured models and dynamically specified configurations.

Main Functions:
    - load_llm_model_from_config: Load LLM model with specified configuration from config file or parameters
    - load_llm_model: Convenience function to load LLM model from various input types

Type Aliases:
    - LLMModelTyping: Union type for model input (str or LLMModel instance)
"""

from typing import Optional, Union

from .base import LLMModel
from .remote import RemoteLLMModel


def load_llm_model_from_config(
        config_file_or_dir: Optional[str] = None,
        base_url: Optional[str] = None,
        api_token: Optional[str] = None,
        model_name: Optional[str] = None,
        **params,
) -> RemoteLLMModel:
    """
    Load a Large Language Model with specified configuration.

    This function attempts to load LLM configuration from a config file or directory,
    and creates a remote model instance. It supports both pre-configured models from
    config files and dynamically specified configurations.

    :param config_file_or_dir: Path to the configuration file or directory. If None, defaults to current directory.
    :type config_file_or_dir: Optional[str]
    :param base_url: Base URL for the LLM API endpoint. If provided, overrides config file settings.
    :type base_url: Optional[str]
    :param api_token: API token for authentication. Required when base_url is provided without config file.
    :type api_token: Optional[str]
    :param model_name: Name of the model to load. Required when base_url is provided without config file.
    :type model_name: Optional[str]
    :param params: Additional parameters to pass to the model.

    :return: An initialized LLM remote model instance.
    :rtype: RemoteLLMModel

    :raises FileNotFoundError: When config file is not found (handled internally).
    :raises KeyError: When specified model is not found in config (handled internally).
    :raises ValueError: When api_token is not specified but required, or when model_name is empty but required.
    :raises RuntimeError: When no model parameters are specified and no local configuration is available.

    Example::
        >>> # Load model from config file
        >>> model = load_llm_model_from_config(config_file_or_dir='./config')
        
        >>> # Load model with explicit parameters
        >>> model = load_llm_model_from_config(
        ...     base_url='https://api.example.com',
        ...     api_token='your-token',
        ...     model_name='gpt-4'
        ... )
        
        >>> # Load model from config with overrides
        >>> model = load_llm_model_from_config(
        ...     config_file_or_dir='./config',
        ...     model_name='gpt-4',
        ...     base_url='https://custom-api.example.com'
        ... )
    """
    from ..manage import LLMConfig
    params: dict

    try:
        llm_config = LLMConfig.open(config_file_or_dir or '.')
    except FileNotFoundError:
        llm_config = None

    if llm_config:
        try:
            llm_params = llm_config.get_model_params(model_name=model_name, **params)
        except KeyError:
            llm_params = None
    else:
        llm_params = None

    if llm_params is not None:
        # known model is found or generated from the config file
        if base_url:
            llm_params['base_url'] = base_url
        if api_token:
            llm_params['api_token'] = api_token
        llm_params.update(**params)

    elif base_url:
        # newly generated llm config
        llm_params = {'base_url': base_url}
        if api_token is None:
            raise ValueError(f'API token must be specified, but {api_token!r} found.')
        llm_params['api_token'] = api_token
        if not model_name:
            raise ValueError(f'Model name must be non-empty, but {model_name!r} found.')
        llm_params['model_name'] = model_name
        llm_params.update(**params)

    else:
        raise RuntimeError('No model parameters specified and no local configuration for falling back.')

    return RemoteLLMModel(**llm_params)


#: Type alias for model input, which can be either a string (model name) or an LLMModel instance.
LLMModelTyping = Union[str, LLMModel]


def load_llm_model(model: Optional[LLMModelTyping] = None) -> LLMModel:
    """
    Load a Large Language Model from various input types.

    This is a convenience function that handles different types of model specifications.
    It can load a model by name from configuration, use an existing model instance,
    or load the default model from configuration.

    :param model: The model specification. Can be:
        - A string representing the model name to load from configuration
        - An LLMModel instance to use directly
        - None to load the default model from configuration
    :type model: Optional[LLMModelTyping]

    :return: An initialized LLM model instance.
    :rtype: LLMModel

    :raises TypeError: When model is not a string, LLMModel instance, or None.
    :raises ValueError: When model name is invalid or not found in configuration.
    :raises RuntimeError: When no model parameters are specified and no local configuration is available.

    Example::
        >>> # Load model by name from configuration
        >>> model = load_llm_model('gpt-4')
        
        >>> # Use an existing model instance
        >>> existing_model = RemoteLLMModel(base_url='...', api_token='...', model_name='gpt-4')
        >>> model = load_llm_model(existing_model)
        
        >>> # Load default model from configuration
        >>> model = load_llm_model()
    """
    model = model or None
    if isinstance(model, str):
        return load_llm_model_from_config(model_name=model)
    elif isinstance(model, LLMModel):
        return model
    elif model is None:
        return load_llm_model_from_config()
    else:
        raise TypeError(
            f'Model must be a string, LLMModel instance, or None, but got {type(model).__name__}: {model!r}')
