from typing import ClassVar
from dataclasses import dataclass, asdict, field
from pathlib import Path

import yaml

from typing import Mapping, Union, Sequence, Optional

from .versions import VersionRaw, Version, guess_version
from ._types import StrOrFile, IOBase

META_FILENAME = "data.txt"
DEFAULT_API_VERSION = 1


@dataclass
class MetaRaw:
    """Absolute minimum metadata for a pin.

    Parameters
    ----------
    file:
        All relevant files contained in the pin. Note that these be absolute paths
        to fetch from the target filesystem.
    type:
        The type of pin data stored. This is used to determine how to read / write it.
    """

    file: "str | Sequence[str] | None"
    type: str
    name: str


@dataclass
class Meta:
    """Represent metadata for a pin version.

    Parameters
    ----------
    title:
        A title for the pin.
    description:
        A detailed description of the pin contents.
    created:
        Datetime the pin was created (TODO: document format).
    pin_hash:
        A hash of the pin.
    file:
        All relevant files in the pin. Should be relative to this pin's folder.
    file_size:
        The total size of the files in the pin.
    type:
        The type of pin data stored. This is used to determine how to read / write it.
    api_version:
        The internal version of the metadata format.
    name:
        TODO - where is this in R pins?
    user:
        A dictionary of additional metadata that may be specified by the user.

    """

    title: Optional[str]
    description: Optional[str]

    # TODO(defer): different from R pins, which has a local field
    created: str
    pin_hash: str

    file: Union[str, Sequence[str]]
    file_size: int
    type: str

    api_version: int

    # In the metadata yaml, the created field uses a custom format, so
    # we need a version object in order to render it. You can think of
    # the version here as "the thing that was used to create version_name,
    # pin_hash, created, etc.."
    version: VersionRaw

    name: Optional[str] = None
    user: Mapping = field(default_factory=dict)

    def to_dict(self) -> Mapping:
        data = asdict(self)

        return data

    def to_pin_dict(self):
        d = self.to_dict()
        del d["name"]
        del d["version"]
        return d

    @classmethod
    def from_pin_dict(cls, data, pin_name, version) -> "Meta":

        # TODO: re-arrange Meta argument positions to reflect what's been
        # learned about default arguments. e.g. title was not used at some
        # point in api_version 1
        extra = {"title": None} if "title" not in data else {}
        return cls(**data, **extra, name=pin_name, version=version)

    def to_pin_yaml(self, f: Optional[IOBase] = None) -> "str | None":
        data = self.to_pin_dict()

        return yaml.dump(data, f)


@dataclass
class MetaV0:
    file: Union[str, Sequence[str]]
    type: str
    description: "str | None"

    name: str

    version: VersionRaw

    # holds raw data.txt contents
    original_fields: dict = field(default_factory=dict)
    user: dict = field(default_factory=dict, init=False)

    title: ClassVar[None] = None
    created: ClassVar[None] = None
    pin_hash: ClassVar[None] = None
    file_size: ClassVar[None] = None
    api_version: ClassVar[None] = None

    def to_dict(self):
        return asdict(self)

    @classmethod
    def from_pin_dict(cls, data, pin_name, version) -> "MetaV0":
        # could infer from dataclasses.fields(), but seems excessive.
        req_fields = {"type", "description"}

        # Note that we need to .get(), since fields may not be in metadata
        req_inputs = {k: data.get(k) for k in req_fields}
        req_inputs["file"] = data["path"]

        return cls(**req_inputs, name=pin_name, original_fields=data, version=version)

    def to_pin_dict(self):
        raise NotImplementedError("v0 pins metadata are read only.")

    def to_pin_yaml(self, *args, **kwargs):
        self.to_pin_dict()


class MetaFactory:
    """Responsible for creating and loading (e.g. from yaml) of meta objects."""

    def get_meta_name(self, *args, **kwargs) -> str:
        return META_FILENAME

    def get_version_for_meta(self, api_version) -> Version:
        if api_version != 1:
            raise NotImplementedError("Unsupported api_version: %s" % api_version)

        return Version

    def create(
        self,
        base_folder: "str | Path",
        files: Sequence[StrOrFile],
        type,
        # TODO: when files is a string name should be okay as None
        name,
        title,
        description=None,
        created=None,
        user=None,
    ) -> Meta:

        if title is None:
            raise NotImplementedError("title arguments required")
        if isinstance(files, str):
            from pathlib import Path

            version = Version.from_files([files], created)
            p_file = Path(files)
            file_size = p_file.stat().st_size
            file_name = str(Path(files).relative_to(Path(base_folder)))

        elif isinstance(files, IOBase):
            # TODO: in theory can calculate size from a file object, but let's
            # wait until it's clear how calculating file size fits into pins
            # e.g. in combination with folders, etc..

            # from os import fstat
            #
            # version = Version.from_files([files], created)
            # files_size = fstat(files.fileno()).st_size

            raise NotImplementedError("Cannot create from file object.")
        else:
            raise NotImplementedError("TODO: creating meta from multiple files")

        return Meta(
            title=title,
            description=description,
            file=file_name,  # TODO: FINISH
            file_size=file_size,
            pin_hash=version.hash,
            created=version.render_created(),
            type=type,
            api_version=DEFAULT_API_VERSION,
            name=name,
            user=user if user is not None else {},
            version=version,
        )

    def create_raw(self, files: Sequence[StrOrFile], type: str, name: str) -> MetaRaw:
        return MetaRaw(files, type, name)

    def read_pin_yaml(
        self, f: IOBase, pin_name: str, version: "str | VersionRaw"
    ) -> Meta:
        if isinstance(version, str):
            version_obj = guess_version(version)
        else:
            version_obj = version

        data = yaml.safe_load(f)

        api_version = data.get("api_version", 0)
        if api_version >= 2:
            raise NotImplementedError(
                f"api_version {api_version} by this version of the pins library"
            )
        elif api_version == 0:
            cls_meta = MetaV0
        else:
            cls_meta = Meta

        return cls_meta.from_pin_dict(data, pin_name, version=version_obj)
