# pylint: disable=no-name-in-module

import sys
from collections import OrderedDict
from decimal import Decimal
from os import PathLike
from pathlib import Path
from typing import Any, ClassVar, Dict, Optional, Sequence, Type

from pydantic import BaseConfig, BaseModel
from pydantic.config import Extra
from pydantic.utils import deep_update

from .dumploads import StrPathIO, json_dump
from .source import (
    DotEnvSettingsStrategy,
    EnvSettingsStrategy,
    Hcl2SettingsStrategy,
    JsonSettingsStrategy,
    KwSettingsStrategy,
    SettingsStrategyCallable,
    TomlSettingsStrategy,
    YamlSettingsStrategy,
)


class BaseSettings(BaseModel):
    __slots__ = ()

    class Config(BaseConfig):  # pylint: disable=too-few-public-methods
        env_prefix: Optional[str] = 'APP'
        env_file: Optional[str | PathLike] = '.env'
        env_file_encoding: Optional[str] = None
        case_sensitive: bool = False
        validate_all: bool = True
        extra: Extra = Extra.ignore
        arbitrary_types_allowed = True

    __config__: ClassVar[Type[Config]] = Config

    def __init__(  # pylint: disable=too-many-arguments
        self,
        env_prefix: Optional[str] = None,
        env_file: Optional[str | PathLike] = None,
        config_file: Optional[str | PathLike] = None,
        config_format: Optional[str] = None,
        source_strategies: Sequence[SettingsStrategyCallable] | None = None,
        **kw: Any,
    ) -> None:
        if config_file is not None:
            config_file = Path(config_file)

        if source_strategies is None:
            cfg = self.__config__
            source_strategies = [
                KwSettingsStrategy(**kw),
                EnvSettingsStrategy(
                    env_prefix=env_prefix or cfg.env_prefix,
                    case_sensitive=cfg.case_sensitive,
                ),
                DotEnvSettingsStrategy(
                    env_prefix=env_prefix or cfg.env_prefix,
                    env_file=env_file or cfg.env_file,
                    env_file_encoding=cfg.env_file_encoding,
                    case_sensitive=cfg.case_sensitive,
                ),
            ]
            if config_file:
                for s in (
                    JsonSettingsStrategy,
                    YamlSettingsStrategy,
                    TomlSettingsStrategy,
                    Hcl2SettingsStrategy,
                ):
                    if s.is_acceptable(config_file, config_format):
                        source_strategies.append(
                            s(
                                config_file,
                                config_format,  # type: ignore[abstract]
                            )
                        )
                        break
                else:
                    raise NotImplementedError(
                        f"No readers found for the config file: {config_file}"
                    )

        super().__init__(
            **deep_update(*reversed([s(self) for s in source_strategies]))
        )

    def _write_json(self, o: Dict[str, Any], f: StrPathIO, **kw: Any) -> None:
        # take encoder from `self.__config__.json_encoders`
        encoder = kw.pop('default', self.__json_encoder__)
        json_dump(o, f, default=encoder, **kw)

    def write_schema(self, f: StrPathIO = sys.stdout, **kw: Any) -> None:
        return self._write_json(self.schema(), f, **kw)

    def write_json(self, f: StrPathIO = sys.stdout, **kw: Any) -> None:
        return self._write_json(self.dict(), f, **kw)

    def to_env(self, **kw: Any) -> Dict[str, str]:
        prefix = self.__config__.env_prefix
        if not prefix:
            return self._to_env(self.dict(**kw))
        return self._to_env(self.dict(**kw), prefix)

    def _to_env(self, ns: Dict[str, Any], *path: str) -> Dict[str, str]:
        res = OrderedDict()

        for name, model in ns.items():
            _path = path + (name,)
            env_name = '_'.join(_.upper() for _ in _path)

            if not isinstance(model, dict):
                # for mypy
                env_val: bool | bytes | str | int | float | complex | Decimal
                if model is None:
                    env_val = ''
                elif isinstance(model, bool):
                    env_val = int(model)
                elif isinstance(model, bytes):
                    env_val = model.decode(
                        encoding='utf-8', errors='surrogateescape'
                    )
                elif isinstance(model, (str, int, float, complex, Decimal)):
                    env_val = model
                else:
                    raise NotImplementedError(
                        f"Not a scalar: {env_name}={type(model)}"
                    )

                res[env_name] = str(env_val)
            else:
                for k, v in self._to_env(model, *_path).items():
                    res[k] = v

        return res
