from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Final

from beekeepy._communication.url import Url
from beekeepy._runnable_handle.settings import strtobool
from schemas._preconfigured_base_model import PreconfiguredBaseModel

if TYPE_CHECKING:
    from typing_extensions import Self

CONFIG_MEMBER_REGEX: Final[re.Pattern[str]] = re.compile(r"^([a-zA-Z0-9]+)(\-([a-zA-Z0-9]+))*$")


class Config(PreconfiguredBaseModel):
    DEFAULT_FILE_NAME: ClassVar[str] = "config.ini"

    class Config:
        arbitrary_types_allowed = True

    def save(self, destination: Path) -> None:
        destination = destination / Config.DEFAULT_FILE_NAME if destination.is_dir() else destination
        with destination.open("wt", encoding="utf-8") as out_file:
            out_file.write("# config automatically generated by helpy\n")
            for line in self.write_to_lines():
                out_file.write(line + "\n")

    def write_to_lines(self) -> list[str]:
        result = []
        for member_name, member_value in self.dict().items():
            if member_value is not None:
                if isinstance(member_value, list) and len(member_value) == 0:
                    continue

                entry_name = self._convert_member_name_to_config_name(member_name)
                entry_value = self._convert_member_value_to_config_value(member_name, member_value)
                for value in [entry_value] if not isinstance(entry_value, list) else entry_value:
                    result.append(f"{entry_name} = {value}")  # noqa: PERF401  # would be unreadable
        return result

    def load(self, source: Path | Self) -> None:
        new_config = self.from_path(source) if isinstance(source, Path) else source
        for member_name, member_value in new_config.dict().items():
            setattr(self, member_name, member_value)

    @classmethod
    def from_path(cls, source: Path) -> Self:
        source = source / Config.DEFAULT_FILE_NAME if source.is_dir() else source
        assert source.exists(), "Given file does not exists."
        return cls.from_lines(source.read_text().strip().splitlines())

    @classmethod
    def from_lines(cls, lines: list[str]) -> Self:
        default_object = cls()
        values_to_write: dict[str, Any | list[Any]] = {}
        for line in lines:
            if (line := line.strip("\n")) and not line.startswith("#"):
                config_name, config_value = line.split("=")
                member_name = cls._convert_config_name_to_member_name(config_name)
                values_to_write[member_name] = cls._convert_config_value_to_member_value(
                    config_value,
                    current_value=values_to_write.get(member_name, default_object[member_name]),
                    member_name=member_name,
                )
        return cls.parse_builtins(values_to_write)

    @classmethod
    def _convert_member_name_to_config_name(cls, member_name: str) -> str:
        return member_name.replace("_", "-")

    @classmethod
    def _convert_config_name_to_member_name(cls, config_name: str) -> str:
        config_name = config_name.strip("""" """)
        if CONFIG_MEMBER_REGEX.match(config_name) is None:
            raise KeyError(f"""Unknown config entry name: `{config_name}`.""")
        return config_name.strip().replace("-", "_")

    @classmethod
    def _convert_member_value_to_config_value(cls, member_name: str, member_value: Any) -> str | list[str]:  # noqa: ARG003
        if isinstance(member_value, list):
            return member_value

        if isinstance(member_value, bool):
            return "yes" if member_value else "no"

        if isinstance(member_value, Url):
            return member_value.as_string(with_protocol=False)

        if isinstance(member_value, Path):
            return member_value.as_posix()

        return str(member_value)

    @classmethod
    def _convert_config_value_to_member_value(
        cls, config_value: str, *, current_value: Any, member_name: str
    ) -> Any | None:
        config_value = config_value.strip(" \t\n\"'")
        member_type: str = cls.__annotations__[member_name]
        if config_value is not None and "bool" in member_type:
            return strtobool(config_value)

        if config_value is not None and "int" in member_type and "[" not in member_type:
            return int(config_value)

        if len(config_value) == 0:
            return current_value

        if current_value is None:
            return config_value

        if isinstance(current_value, list):
            current_value.extend(config_value.strip().split(" "))
            return current_value

        return config_value

    def get_differences_between(self, other: Self) -> dict[str, tuple[Any, Any]]:
        differences = {}
        for member_name in self.dict():
            self_value = getattr(self, member_name)
            other_value = getattr(other, member_name)
            if self_value != other_value:
                differences[member_name] = (self_value, other_value)
        return differences
