# from zyplib.utils.print import print_error
import dataclasses
import logging
import os
import typing
from dataclasses import asdict, dataclass, fields
from typing import Any, Dict, Literal, Optional, Type, Union, get_args, get_origin

from dotenv import dotenv_values, find_dotenv, load_dotenv

ENV_FNAME = 'zyplib.env'
BASE_DIR = os.path.expanduser('~/.zyplib')
GLOBAL_ENV_PATH = os.path.join(BASE_DIR, ENV_FNAME)

__all__ = [
    'Config',
    'config',
    'envs',
    'reset_zyplib_envfile',
    'update_zyplib_envfile',
    'cast_type',
]


def cast_type(value: str, type_def: Type[Any]) -> Any:
    """
    Casts a string value to a specified type, with special handling for
    bool, list, and generic list types (e.g., list[int]).
    """
    origin = get_origin(type_def)

    # 0. Handle Optional[T] and Union types
    if origin is Union:
        # Optional[T] is Union[T, NoneType]
        union_types = get_args(type_def)
        # If NoneType is in union, treat as Optional
        none_type = type(None)
        non_none_types = [t for t in union_types if t is not none_type]

        if len(union_types) == 2 and none_type in union_types:
            # This is Optional[T]
            if value.strip().lower() in ('', 'none', 'null'):
                return None
            # Try to cast to the non-None type
            return cast_type(value, non_none_types[0])
        else:
            logging.warning(
                f'{origin} 类型的值: {value} 较为复杂, 只能尽可能尝试转换为 {union_types}'
            )
            # General Union - try each type
            last_exception = None
            for t in union_types:
                try:
                    return cast_type(value, t)
                except Exception as exc:
                    last_exception = exc
            # All failed
            raise ValueError(
                f"Could not cast value '{value}' to Union {union_types}: {last_exception}"
            )

    # 1. Handle boolean type
    if type_def is bool:
        return value.lower() in ('true', '1', 't', 'yes')

    # 2. Handle list types (e.g., list, list[str], list[int])
    if origin is list:
        # Split the string by commas and strip whitespace
        items = [item.strip() for item in value.split(',') if item.strip()]

        # Check if the list has a specific subtype (e.g., int in list[int])
        list_item_type = get_args(type_def)
        if list_item_type:
            # Recursively call cast for each item in the list
            item_caster = list_item_type[0]
            return [cast_type(item, item_caster) for item in items]
        else:
            # If it's just `list`, treat items as strings
            return items

    if origin is dict:
        import json

        return json.loads(value)

    # 3. Handle simple types (int, float, str, etc.) as a fallback
    try:
        return type_def(value)
    except (ValueError, TypeError):
        # Let the caller handle the exception if casting fails
        raise ValueError(f"Could not cast value '{value}' to type {type_def}")


@dataclass
class Config:
    """Configuration class for zyplib.

    Holds global configuration settings for the library, including cache settings.

    Parameters
    ----------
    - `DISK_CACHE_DIR` : `str`
        - Directory path for disk cache storage, by default './cache'
    - `DISK_CACHE_MAX_SIZE` : `int`
        - Maximum cache size in MB, by default 3 * 1024 MB
    """

    DISK_CACHE_DIR: str = './cache'
    DISK_CACHE_MAX_SIZE: int = 3 * 1024  # MB

    LOAD_DEFAULT_MPL_STYLE: bool = True

    OPENAI_BASE_URL: Optional[str] = None
    OPENAI_MODEL: Optional[str] = None
    OPENAI_KEY: Optional[str] = None

    @classmethod
    def from_env(cls, error: Literal['ignore', 'raise'] = 'raise') -> 'Config':
        """
        Creates a Config instance by loading values from environment variables.
        """
        config_kwargs = {}
        for f in fields(cls):
            env_value = os.getenv(f.name)
            if env_value is not None:
                try:
                    # Use our cast function to get the correctly typed value
                    casted_value = cast_type(env_value, f.type)
                    config_kwargs[f.name] = casted_value
                except ValueError as e:
                    # If casting fails, we simply don't add it to our kwargs,
                    # so the dataclass will use its default value.
                    if error == 'raise':
                        raise ValueError(
                            f'Invalid value for {f.name}: {env_value}'
                        ) from e
                    else:
                        logging.warning(
                            f'Invalid value for {f.name}: {env_value}, using default.'
                        )
                        config_kwargs[f.name] = env_value
                        # If error is 'ignore', we just skip this field
                        continue

        # Create an instance of the class, passing only the values
        # found in the environment. Others will fall back to defaults.
        return cls(**config_kwargs)

    def as_dict(self):
        """Convert configuration to dictionary.

        Returns
        ----------
        - `dict`
            - Dictionary representation of the configuration
        """
        return asdict(self)

    def as_envstr(self):
        """Convert configuration to environment variable string format.

        Returns
        ----------
        - `str`
            - String representation of the configuration in ENV format
        """
        return '\n'.join(
            f'{key}={value if value is not None else ""}'
            for key, value in self.as_dict().items()
        )


def _default_env_text():
    conf = Config()
    return f"""# Zyplib Global Configuration
# Configuration
{conf.as_envstr()}

# Add your custom environment variables here
# MODEL_PATH=/path/to/models
# Example: MY_DICT='{{"key": "value", "number": 123}}'
"""


def reset_zyplib_envfile(dest: Literal['global', 'local'] = 'global'):
    """Reset the configuration to default values.

    Parameters
    ----------
    - `dest` : `str`
        - Destination to reset, either 'global' or 'local'
    """
    from zyplib.utils.print import print_debug

    if dest == 'global':
        # Reset global config file
        with open(GLOBAL_ENV_PATH, 'w', encoding='utf-8') as f:
            f.write(_default_env_text())
        print_debug(f'Global config reset to defaults at {GLOBAL_ENV_PATH}')
    elif dest == 'local':
        if os.path.exists(ENV_FNAME):
            from zyplib.utils.fs import edit_file_lines

            def edit(line: str):
                if line.startswith('#'):
                    return line
                return f'# {line}'

            edit_file_lines(ENV_FNAME, edit)  # comment old content
        with open(ENV_FNAME, 'a', encoding='utf-8') as f:
            f.write(_default_env_text())
        print_debug(f'Local config reset to defaults at {ENV_FNAME}')
    else:
        raise ValueError("Invalid destination. Use 'global' or 'local'.")


def update_zyplib_envfile(dest: Literal['global', 'local'] = 'global'):
    """Update the configuration file with missing fields from Config class.

    Parameters
    ----------
    - `dest` : `str`
        - Destination to update, either 'global' or 'local'

    Returns
    ----------
    - `bool`
        - True if file was updated, False if no changes were needed
    """
    from zyplib.utils.print import print_debug, print_info

    if dest == 'global':
        if not os.path.exists(GLOBAL_ENV_PATH):
            reset_zyplib_envfile(dest='global')
            print_info(f'Created new global config at {GLOBAL_ENV_PATH}')
            return True

        updated = _update_env_file_with_missing_fields(GLOBAL_ENV_PATH)
        if updated:
            print_info(f'Updated global config at {GLOBAL_ENV_PATH}')
        else:
            print_debug('Global config is already up to date')
        return updated

    elif dest == 'local':
        if not os.path.exists(ENV_FNAME):
            reset_zyplib_envfile(dest='local')
            print_info(f'Created new local config at {ENV_FNAME}')
            return True

        updated = _update_env_file_with_missing_fields(ENV_FNAME)
        if updated:
            print_info(f'Updated local config at {ENV_FNAME}')
        else:
            print_debug('Local config is already up to date')
        return updated
    else:
        raise ValueError("Invalid destination. Use 'global' or 'local'.")


def _update_env_file_with_missing_fields(env_file_path: str) -> bool:
    """Check and update environment configuration file with missing fields.

    Parameters
    ----------
    - `env_file_path` : `str`
        - Path to the environment file to update

    Returns
    ----------
    - `bool`
        - True if file was updated, False if no changes were needed
    """
    # Read existing env file
    existing_env = dotenv_values(env_file_path)

    # Get all Config fields and their default values
    config_fields = {}
    for field in fields(Config):
        default_value = field.default
        # Handle default_factory if present
        if field.default_factory is not dataclasses.MISSING:  # type: ignore
            default_value = field.default_factory()
        config_fields[field.name] = default_value

    # Find missing fields
    missing_fields = set(config_fields.keys()) - set(existing_env.keys())

    if not missing_fields:
        return False  # No missing fields

    # Append missing fields to the file
    with open(env_file_path, 'a', encoding='utf-8') as f:
        f.write('\n# Auto-updated configuration fields\n')
        for field_name in sorted(missing_fields):
            default_value = config_fields[field_name]
            # Format None values appropriately
            if default_value is None:
                value_str = ''
            else:
                value_str = str(default_value)
            f.write(f'{field_name}={value_str}\n')

    logging.debug(
        f'Updated {env_file_path} with {len(missing_fields)} missing fields: {missing_fields}'
    )
    return True


def _load_zyplib_envfile(load_to_env: bool = True) -> Dict[str, str]:
    """Load environment variables from dotenv files.

    Loads in order: global env, local env files
    """
    # Create global config directory if it doesn't exist
    if not os.path.exists(BASE_DIR):
        os.makedirs(BASE_DIR)

    # Create default global env file if it doesn't exist
    if not os.path.exists(GLOBAL_ENV_PATH):
        reset_zyplib_envfile(dest='global')

    envs = {}
    # Load global env file
    if os.path.exists(GLOBAL_ENV_PATH):
        # Check and update missing fields before loading
        _update_env_file_with_missing_fields(GLOBAL_ENV_PATH)

        if load_to_env:
            load_dotenv(GLOBAL_ENV_PATH, override=False)
        global_envs = dotenv_values(GLOBAL_ENV_PATH)
        envs = {**global_envs}

    # Load local env files (later files override earlier ones)
    path = find_dotenv(ENV_FNAME)
    if not path:
        path = find_dotenv('.env')  # fallback

    if path:
        logging.debug(f'导入 zyplib.env: {path}')
        if load_to_env:
            load_dotenv(path, override=True)
        local_envs = dotenv_values(path)
        envs = {**envs, **local_envs}

    return envs


# 缓存我们已经加载的对象
_cache = {}


def __getattr__(name: str) -> Any:
    """
    模块级别的惰性加载器 (PEP 562)。
    仅在变量第一次被访问时调用。
    """
    # 检查是否请求 'config'
    if name == 'config':
        # 检查缓存，避免重复加载
        if 'config' not in _cache:
            logging.debug("第一次访问 'config'，开始惰性初始化...")
            get_envs()
            _cache['config'] = Config.from_env()
        return _cache['config']

    # 检查是否请求 'envs'
    if name == 'envs':
        if 'envs' not in _cache:
            logging.debug("第一次访问 'envs'，开始惰性初始化...")
            # 执行加载操作
            loaded_envs = _load_zyplib_envfile(load_to_env=True)
            _cache['envs'] = loaded_envs
        return _cache['envs']

    # 如果请求的不是我们想惰性加载的变量，则抛出标准错误
    raise AttributeError(f"module '{__name__}' has no attribute '{name}'")


if typing.TYPE_CHECKING:
    config: Config
    envs: Dict[str, str]


# 辅助函数，确保 envs 在 config 之前加载
def get_envs():
    return __getattr__('envs')
