import os
from pathlib import Path
from typing import Any

from pydantic import Field
from pydantic_settings import SettingsConfigDict

from napari.settings._appearance import AppearanceSettings
from napari.settings._application import ApplicationSettings
from napari.settings._base import (
    _NOT_SET,
    EventedConfigFileSettings,
    _NotSetType,
    _remove_empty_dicts,
)
from napari.settings._experimental import ExperimentalSettings
from napari.settings._fields import Version
from napari.settings._plugins import PluginsSettings
from napari.settings._shortcuts import ShortcutsSettings
from napari.utils._base import _DEFAULT_CONFIG_PATH

_CFG_PATH = os.getenv('NAPARI_CONFIG', _DEFAULT_CONFIG_PATH)

CURRENT_SCHEMA_VERSION = Version(0, 9, 0)


class NapariSettings(EventedConfigFileSettings):
    """Schema for napari settings."""

    # 1. If you want to *change* the default value of a current option, you need to
    #    do a MINOR update in config version, e.g. from 3.0.0 to 3.1.0
    # 2. If you want to *remove* options that are no longer needed in the codebase,
    #    or if you want to *rename* options, then you need to do a MAJOR update in
    #    version, e.g. from 3.0.0 to 4.0.0
    # 3. You don't need to touch this value if you're just adding a new option
    schema_version: Version = Field(
        CURRENT_SCHEMA_VERSION,
        description='Napari settings schema version.',
    )

    application: ApplicationSettings = Field(
        default_factory=ApplicationSettings,
        title='Application',
        description='Main application settings.',
    )
    appearance: AppearanceSettings = Field(
        default_factory=AppearanceSettings,
        title='Appearance',
        description='User interface appearance settings.',
        frozen=True,
    )
    plugins: PluginsSettings = Field(
        default_factory=PluginsSettings,
        title='Plugins',
        description='Plugins settings.',
        frozen=True,
    )
    shortcuts: ShortcutsSettings = Field(
        default_factory=ShortcutsSettings,
        title='Shortcuts',
        description='Shortcut settings.',
        frozen=True,
    )
    experimental: ExperimentalSettings = Field(
        default_factory=ExperimentalSettings,
        title='Experimental',
        description='Experimental settings.',
        frozen=True,
    )

    # private attributes and ClassVars will not appear in the schema
    config_path: Path | None = Field(
        Path(_CFG_PATH) if _CFG_PATH else None, exclude=True
    )

    model_config = SettingsConfigDict(
        env_prefix='napari_',
        nested_model_default_partial_update=True,
        env_nested_delimiter='_',
        env_nested_max_split=1,
        use_enum_values=False,
        extra='ignore',
        populate_by_name=True,
    )

    def _connect_events(self) -> None:
        """Connects the events for the settings to their respective update functions."""
        self.application._connect_events()
        self.experimental._connect_events()

    def __init__(
        self, config_path: Path | _NotSetType | None = _NOT_SET, **values: Any
    ) -> None:
        super().__init__(config_path, **values)
        self._maybe_migrate()

    def _save_dict(self, **kwargs: dict[Any, Any]) -> dict[str, Any]:
        # we always want schema_version written to the settings.yaml
        # TODO: is there a better way to always include schema version?
        return {
            'schema_version': self.schema_version,
            **super()._save_dict(**kwargs),
        }

    def __str__(self) -> str:
        out = 'NapariSettings (defaults excluded)\n' + 34 * '-' + '\n'
        data = self.model_dump(exclude_defaults=True)
        out += self._yaml_dump(_remove_empty_dicts(data))
        return out

    def __repr__(self) -> str:
        return str(self)

    def _maybe_migrate(self) -> None:
        if self.schema_version < CURRENT_SCHEMA_VERSION:
            from napari.settings._migrations import do_migrations

            do_migrations(self)


if __name__ == '__main__':
    import json
    import sys

    if len(sys.argv) > 2:
        dest = Path(sys.argv[2]).expanduser().absolute()
    else:
        dest = Path(__file__).parent / 'napari.schema.json'
    dest.write_text(json.dumps(NapariSettings.model_json_schema()))
