from __future__ import annotations

from collections.abc import Iterator, Mapping, MutableMapping, Sequence
import contextlib
import logging
import os
import sys
from typing import Any, Literal, Self

from prettyqt import core
from prettyqt.utils import bidict, datatypes


logger = logging.getLogger(__name__)

FormatStr = Literal["user", "system"]

FORMAT: bidict[FormatStr, core.QSettings.Format] = bidict(
    native=core.QSettings.Format.NativeFormat, ini=core.QSettings.Format.IniFormat
)

ScopeStr = Literal["user", "system"]

SCOPE: bidict[ScopeStr, core.QSettings.Scope] = bidict(
    user=core.QSettings.Scope.UserScope, system=core.QSettings.Scope.SystemScope
)


class Settings_(  # noqa: N801
    core.ObjectMixin, core.QSettings, MutableMapping, metaclass=datatypes.QABCMeta
):
    """Persistent platform-independent application settings."""

    # Setting class with original behavior, compatible with original QSettings
    def __init__(self, *args, settings_id: str | None = None):
        super().__init__(*args)
        self.settings_id = settings_id

    def __repr__(self):
        return f"{type(self).__name__}({dict(self)})"

    def __contains__(self, key: str) -> bool:
        return self.contains(key)

    def __enter__(self):
        if self.settings_id:
            self.beginGroup(self.settings_id)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self.settings_id:
            self.endGroup()

    def __getitem__(self, index: str):
        if index not in self.allKeys():
            raise KeyError(index)
        return self.get_value(index)

    def __setitem__(self, name: str, value):
        return self.set_value(name, value)

    def __delitem__(self, key: str):
        if not self.contains(key):
            raise KeyError(key)
        return self.remove(key)

    def __iter__(self) -> Iterator[str]:
        return iter(self.allKeys())

    def __len__(self) -> int:
        return len(self.allKeys())

    def __ior__(self, other: Mapping):
        self.update(other)
        return self

    def set_value(self, key: str, value):
        if not self.applicationName():
            msg = "no app name defined"
            raise RuntimeError(msg)
        if isinstance(value, Settings_):
            value = dict(value)
        self.setValue(key, value)

    def get_value(self, key: str, default=None):
        return self.value(key) if self.contains(key) else default

    @classmethod
    def set_default_format(cls, fmt: FormatStr | core.QSettings.Format):
        """Set the default format.

        Args:
            fmt: the default format to use
        """
        cls.setDefaultFormat(FORMAT.get_enum_value(fmt))

    @classmethod
    def get_default_format(cls) -> FormatStr:
        """Return default settings format.

        Returns:
            default settings format
        """
        return FORMAT.inverse[cls.defaultFormat()]

    def get_scope(self) -> ScopeStr:
        """Return scope.

        Returns:
            scope
        """
        return SCOPE.inverse[self.scope()]

    @classmethod
    def set_path(
        cls,
        fmt: FormatStr | core.QSettings.Format,
        scope: ScopeStr | core.QSettings.Scope,
        path: datatypes.PathType,
    ):
        """Set the path to the settings file.

        Args:
            fmt: the default format to use
            scope: the scope to use
            path: the path to set
        """
        fmt_val = FORMAT.get_enum_value(fmt)
        scope_val = SCOPE.get_enum_value(scope)
        cls.setPath(fmt_val, scope_val, os.fspath(path))

    @contextlib.contextmanager
    def edit_group(self, prefix: str):
        """Context manager for setting groups.

        Args:
            prefix: setting prefix for group
        """
        self.beginGroup(prefix)
        yield None
        self.endGroup()

    def write_array(self, prefix: str, array: Sequence) -> Iterator[tuple[Self, Any]]:
        """Context manager for writing arrays.

        Args:
            prefix: prefix for settings array
            array: array used to write settings

        Returns:
            Iterator yielding (setting, arrayitem) items
        """
        self.beginWriteArray(prefix, len(array))
        for i, _ in enumerate(array):
            self.setArrayIndex(i)
            yield (self, array[i])
        self.endArray()

    def read_array(self, prefix: str) -> Iterator[Settings_]:
        """Context manager for reading arrays.

        Args:
            prefix: prefix for settings array
        """
        size = self.beginReadArray(prefix)
        for i in range(size):
            self.setArrayIndex(i)
            yield self
        self.endArray()

    # MutableMapping overrides

    def keys(self) -> list[str]:
        return self.allKeys()

    def setdefault(self, key: str, default: Any = None) -> Any:
        if not self.contains(key):
            self.set_value(key, default)
            return default
        return self.get_value(key)

    @classmethod
    def register_extensions(
        cls,
        *exts: str,
        app_name: str | None = None,
        app_path: None | datatypes.PathType = None,
    ):
        logger.debug("assigning extensions %s to %r", exts, app_name)
        s = cls("HKEY_CURRENT_USER\\SOFTWARE\\Classes", Settings.Format.NativeFormat)
        if app_path is None:
            app_path = str(core.CoreApplication.get_application_file_path())
        app_path = os.fspath(app_path)
        if app_name is None:
            app_name = core.CoreApplication.applicationName()
        for ext in exts:
            s.setValue(f"{ext}/DefaultIcon/.", app_path)  # perhaps ,0 after app_path
            s.setValue(f"{ext}/.", app_name)
        if getattr(sys, "frozen", False):
            s.setValue(f"{app_name}/shell/open/command/.", f"{app_path} %1")
        else:
            command_value = rf'{sys.executable} -m {app_path} "%V"'
            s.setValue(f"{app_name}/shell/open/command/.", f"{command_value} %1")


class Settings(Settings_):
    """Settings class which wraps everything into a dict to preserve data types."""

    def set_value(self, key: str, value):
        match value:
            case Settings_():
                setting = dict(value=dict(value), typ="subsetting")
            case _:
                setting = dict(value=value, typ="regular")
        super().set_value(key, setting)

    def get_value(self, key: str, default=None):
        if not self.contains(key):
            return default
        val = self.value(key)
        # TODO: convert settings dicts back?
        match val:
            case {"value": setting}:
                return setting
            case _:
                # this is for migration
                self.set_value(key, val)
                return val


if __name__ == "__main__":
    settings = Settings("1", "2")
    settings["1"] = True
    settings |= dict(abc="abc")
