from pathlib import Path
from typing import (
    Callable,
    Type,
    Union,
    Dict,
    List,
    Any,
    Optional,
)


from . import (
    AutoHandler,
    prompt_namedtuple,
    namedtuple_sequence_dumps,
    namedtuple_sequence_load,
)
from .fileio import Format
from .typehelpers import PrimitiveType, NT, T, PromptFunctionorValue


def _normalize(_path: Union[Path, str]) -> Path:
    p: Path
    if isinstance(_path, str):
        p = Path(_path)
    else:
        p = _path
    return p.expanduser().absolute()


def _detect_format(path: Path, format: Optional[Format]) -> Optional[Format]:
    ext = path.suffix
    if ext == ".json":
        format = "json"
    elif ext in [".yml", ".yaml"]:
        format = "yaml"
    return format


# doesnt expose underlying kwargs from namedtuple_sequence_dump
# and namedtuple_sequence_load on purpose -- so that its less
# likely its mistyped. Can always use the underlying
# functions if you'd prefer to do that.


def dump_to(
    items: List[NT],
    path: Union[Path, str],
    *,
    attr_serializers: Optional[Dict[str, Callable[[T], PrimitiveType]]] = None,
    type_serializers: Optional[Dict[Type, Callable[[Any], PrimitiveType]]] = None,
    format: Optional[Format] = None,
) -> None:
    """
    Takes a list of NamedTuples (or subclasses) and a path to a file.
    Serializes the items into a string, using the attr_serializers and
    type_serializers to handle custom types if specified.

    If format is unset, uses the file extension to detect the type

    If serialization succeeds, writes to the file.
    """
    p = _normalize(path)
    format = _detect_format(p, format)
    # serialize to string before opening file
    # if serialization fails, file is left alone
    nt_string: str = namedtuple_sequence_dumps(
        items,
        attr_serializers=attr_serializers,
        type_serializers=type_serializers,
        format=format,
    )
    with p.open(mode="w") as f:
        f.write(nt_string)


# args are slightly reordered here, comapared to json.load
# to be consistent with dump_to
def load_from(
    to: Type[NT],
    path: Union[Path, str],
    *,
    attr_deserializers: Optional[Dict[str, Callable[[PrimitiveType], Any]]] = None,
    type_deserializers: Optional[Dict[Type, Callable[[PrimitiveType], Any]]] = None,
    format: Optional[Format] = None,
    allow_empty: bool = False,
) -> List[NT]:
    """
    Takes a type to load and a path to a file containing JSON.
    Reads from the file, and deserializes the items into a list,
    using the attr_deserializers and type_deserializers to handle
    custom types if specified.

    If format is unset, uses the file extension to detect the type

    If allow_empty is True, this returns an empty list if the file
    didn't exist

    Returns the list of items read from the file.
    """
    p: Path = _normalize(path)
    format = _detect_format(p, format)
    try:
        with p.open(mode="r") as f:
            items: List[NT] = namedtuple_sequence_load(
                f,
                to,
                attr_deserializers=attr_deserializers,
                type_deserializers=type_deserializers,
                format=format,
            )
    except FileNotFoundError as fne:
        if allow_empty:
            return []
        else:
            raise fne
    return items


def load_prompt_and_writeback(
    to: Type[NT],
    path: Union[Path, str],
    *,
    create_file: bool = True,
    attr_validators: Optional[Dict[str, AutoHandler]] = None,
    type_validators: Optional[Dict[Type[T], AutoHandler]] = None,
    attr_use_values: Optional[Dict[str, PromptFunctionorValue]] = None,
    type_use_values: Optional[Dict[Type[T], PromptFunctionorValue]] = None,
    attr_serializers: Optional[Dict[str, Callable[[T], PrimitiveType]]] = None,
    type_serializers: Optional[Dict[Type, Callable[[Any], PrimitiveType]]] = None,
    attr_deserializers: Optional[Dict[str, Callable[[PrimitiveType], Any]]] = None,
    type_deserializers: Optional[Dict[Type, Callable[[PrimitiveType], Any]]] = None,
    format: Optional[Format] = None,
    prompt_function: Optional[Callable[..., NT]] = None,
) -> List[NT]:
    """
    An entry point to entire library, essentially.

    Load the NamedTuples from the JSON file specified by 'path' and 'to'

    If format is unset, uses the file extension to detect the type

    If the file doesn't exist and the create_file flag is True, ignores the
    FileNotFound error when trying to read from the file.

    After reading the NamedTuples from the file, prompts you to add a new item,
    adds that to the list it reads in, and writes back to the file

    The 'prompt_function' overrides the default 'prompt_namedtuple', if you
    want to change how to prompt/the underling functions generated by namedtuple_prompt_funcs

    accepts validators, serializers and deserializers for each step of the process.
    """
    p: Path = _normalize(path)
    format = _detect_format(p, format)
    # read from file
    items: List[NT] = []
    try:
        items = load_from(
            to,
            p,
            attr_deserializers=attr_deserializers,
            type_deserializers=type_deserializers,
            format=format,
        )
    except FileNotFoundError as fne:
        if not create_file:
            raise fne
        # warn(f"File at {p} didn't exist, using empty list")
    # prompt for the new item
    chosen_prompt_function = prompt_function or prompt_namedtuple
    new_item: NT = chosen_prompt_function(
        to,
        attr_validators=attr_validators,
        type_validators=type_validators,
        attr_use_values=attr_use_values,
        type_use_values=type_use_values,
    )
    items.append(new_item)
    # dump back to file
    dump_to(
        items,
        p,
        attr_serializers=attr_serializers,
        type_serializers=type_serializers,
        format=format,
    )
    return items
