# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""The base class for a format handler implementation."""

from __future__ import annotations

import dataclasses
import typing

from tina_mgr import db
from tina_mgr import defs
from tina_mgr import parse


if typing.TYPE_CHECKING:
    from typing import Any, Final


@dataclasses.dataclass
class FormatError(defs.Error, ValueError):
    """An error generated by a format handler."""


@dataclasses.dataclass
class DeserializationError(FormatError):
    """An error that occurred while deserializing some data."""

    # It turns out PyYAML's YAMLError is not a ValueError
    err: Exception
    """The deserialization error that occurred."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return f"Could not deserialize a Tina database: {self.err}"


@dataclasses.dataclass
class NoFormatVersionError(FormatError):
    """The serialized database does not declare its format version."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return "Missing format.version.major/minor fields"


@dataclasses.dataclass
class UnsupportedFormatVersionError(FormatError):
    """The serialized database specifies an invalid format version."""

    major: int
    """The major version number."""

    minor: int
    """The minor version number."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return f"Unsupported format version {self.major}.{self.minor}"


@dataclasses.dataclass
class NoTinaError(FormatError):
    """The serialized database does not contain a 'tina' element."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return "Missing 'tina' field"


@dataclasses.dataclass
class NoTinaListError(FormatError):
    """The 'tina' element is of some weird type."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return "The 'tina' field must be a list of dicts"


@dataclasses.dataclass
class NoFieldError(FormatError):
    """An item is missing a required field."""

    name: str
    """The name of the missing field."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return f"Database item with no {self.name} field"


@dataclasses.dataclass
class InvalidTypeFieldError(FormatError):
    """An item has a field of an invalid type."""

    name: str
    """The name of the invalid field."""

    ftype: type[Any]
    """The type of the invalid field."""

    def __str__(self) -> str:
        """Provide a human-readable description of the error."""
        return f"Database item with a {self.name} field of invalid type {self.ftype.__name__}"


class FormatHandler:
    """A handler for converting the Tina database between different formats."""

    @classmethod
    def _do_dumps(cls, entries: list[db.TinaEntry]) -> str:
        """Store entries encoded in that format."""
        raise NotImplementedError(repr((cls, "dumps")))

    @classmethod
    def _do_loads(cls, contents: str) -> list[db.TinaEntry]:
        """Load entries encoded in that format."""
        raise NotImplementedError(repr((cls, "loads")))

    @classmethod
    def dumps(cls, entries: list[db.TinaEntry]) -> str:
        """Store entries encoded in that format."""
        parse.check_for_duplicates(entries)
        return cls._do_dumps(entries)

    @classmethod
    def loads(cls, contents: str) -> list[db.TinaEntry]:
        """Load entries encoded in that format."""
        res: Final = cls._do_loads(contents)
        parse.check_for_duplicates(res)
        return res


def entries_to_raw(entries: list[db.TinaEntry]) -> list[dict[str, Any]]:
    """Prepare a simplistic hierarchical structure."""

    def entry_to_raw(entry: db.TinaEntry) -> dict[str, Any]:
        """Represent a single entry."""
        return {"id": entry.id, "desc": entry.desc, "children": entries_to_raw(entry.children)}

    return [entry_to_raw(entry) for entry in entries]


def raw_to_entries(raw: list[dict[str, Any]]) -> list[db.TinaEntry]:
    """Parse the simplistic hierarchical structure."""

    def raw_to_entry(entry: dict[str, Any]) -> db.TinaEntry:
        """Parse a single entry."""
        try:
            item_id = entry["id"]
            desc = entry["desc"]
            children = entry.get("children", [])
        except KeyError as err:
            raise NoFieldError(err.args[0]) from err

        if not isinstance(item_id, str):
            raise InvalidTypeFieldError("id", type(item_id))
        if not isinstance(desc, str):
            raise InvalidTypeFieldError("desc", type(desc))
        if not isinstance(children, list):
            raise InvalidTypeFieldError("children", type(children))

        return db.TinaEntry(
            id=item_id,
            desc=desc,
            children=raw_to_entries(children),
        )

    return [raw_to_entry(entry) for entry in raw]


def serialize(entries: list[db.TinaEntry]) -> dict[str, Any]:
    """Prepare the serialization data with the format version."""
    return {"format": {"version": {"major": 1, "minor": 0}}, "tina": entries_to_raw(entries)}


def deserialize(value: dict[str, Any]) -> list[db.TinaEntry]:
    """Parse the 'tina' element."""
    try:
        major, minor = value["format"]["version"]["major"], value["format"]["version"]["minor"]
    except (AttributeError, TypeError, KeyError) as err:
        raise NoFormatVersionError from err
    if major != 1:
        raise UnsupportedFormatVersionError(major, minor)

    try:
        tina: Final[list[dict[str, Any]]] = value["tina"]
    except KeyError as err:
        raise NoTinaError from err

    if not isinstance(tina, list) or not all(isinstance(item, dict) for item in tina):
        raise NoTinaListError
    return raw_to_entries(tina)
