"""
This module provides the `Json` class, which includes methods to read,
create and update JSON files, with support for comments inside the JSON data.
"""

from .file_sys import FileSys
from .data import Data
from .file import File

from typing import Literal, Any, cast
from pathlib import Path
import json as _json


class Json:
    """This class provides methods to read, create and update JSON files,
    with support for comments inside the JSON data."""

    @classmethod
    def read(
        cls,
        json_file: Path | str,
        comment_start: str = ">>",
        comment_end: str = "<<",
        return_original: bool = False,
    ) -> dict | tuple[dict, dict]:
        """Read JSON files, ignoring comments.\n
        ------------------------------------------------------------------------------------
        - `json_file` -⠀the path (relative or absolute) to the JSON file to read
        - `comment_start` -⠀the string that indicates the start of a comment
        - `comment_end` -⠀the string that indicates the end of a comment
        - `return_original` -⠀if true, the original JSON data is returned additionally:<br>
          ```python
        (processed_json, original_json)
          ```\n
        ------------------------------------------------------------------------------------
        For more detailed information about the comment handling,
        see the `Data.remove_comments()` method documentation."""
        if (json_path := Path(json_file) if isinstance(json_file, str) else json_file).suffix != ".json":
            json_path = json_path.with_suffix(".json")
        file_path = FileSys.extend_or_make_path(json_path, prefer_script_dir=True)

        with open(file_path, "r") as file:
            content = file.read()

        try:
            data = _json.loads(content)
        except _json.JSONDecodeError as e:
            fmt_error = "\n  ".join(str(e).splitlines())
            raise ValueError(f"Error parsing JSON in {file_path!r}:\n  {fmt_error}") from e

        if not (processed_data := dict(Data.remove_comments(data, comment_start, comment_end))):
            raise ValueError(f"The JSON file {file_path!r} is empty or contains only comments.")

        return (processed_data, data) if return_original else processed_data

    @classmethod
    def create(
        cls,
        json_file: Path | str,
        data: dict,
        indent: int = 2,
        compactness: Literal[0, 1, 2] = 1,
        force: bool = False,
    ) -> Path:
        """Create a nicely formatted JSON file from a dictionary.\n
        ---------------------------------------------------------------------------
        - `json_file` -⠀the path (relative or absolute) to the JSON file to create
        - `data` -⠀the dictionary data to write to the JSON file
        - `indent` -⠀the amount of spaces to use for indentation
        - `compactness` -⠀can be `0`, `1` or `2` and indicates how compact
          the data should be formatted (see `Data.render()` for more info)
        - `force` -⠀if true, will overwrite existing files
          without throwing an error (errors explained below)\n
        ---------------------------------------------------------------------------
        The method will throw a `FileExistsError` if a file with the same
        name already exists and a `SameContentFileExistsError` if a file
        with the same name and same content already exists."""
        if (json_path := Path(json_file) if isinstance(json_file, str) else json_file).suffix != ".json":
            json_path = json_path.with_suffix(".json")

        file_path = FileSys.extend_or_make_path(json_path, prefer_script_dir=True)
        File.create(
            file_path=file_path,
            content=Data.render(
                data=data,
                indent=indent,
                compactness=compactness,
                as_json=True,
                syntax_highlighting=False,
            ),
            force=force,
        )

        return file_path

    @classmethod
    def update(
        cls,
        json_file: Path | str,
        update_values: dict[str, Any],
        comment_start: str = ">>",
        comment_end: str = "<<",
        path_sep: str = "->",
    ) -> None:
        """Update single/multiple values inside JSON files,
        without needing to know the rest of the data.\n
        -----------------------------------------------------------------------------------
        - `json_file` -⠀the path (relative or absolute) to the JSON file to update
        - `update_values` -⠀a dictionary with the paths to the values to update
          and the new values to set (see explanation below – section 2)
        - `comment_start` -⠀the string that indicates the start of a comment
        - `comment_end` -⠀the string that indicates the end of a comment
        - `path_sep` -⠀the separator used inside the value-paths in `update_values`\n
        -----------------------------------------------------------------------------------
        For more detailed information about the comment handling,
        see the `Data.remove_comments()` method documentation.\n
        -----------------------------------------------------------------------------------
        The `update_values` is a dictionary, where the keys are the paths
        to the data to update, and the values are the new values to set.\n
        For example for this JSON data:
        ```python
        {
            "healthy": {
                "fruits": ["apples", "bananas", "oranges"],
                "vegetables": ["carrots", "broccoli", "celery"]
            }
        }
        ```
        … the `update_values` dictionary could look like this:
        ```python
        {
            # CHANGE FIRST LIST-VALUE UNDER 'fruits' TO "strawberries"
            "healthy->fruits->0": "strawberries",
            # CHANGE VALUE OF KEY 'vegetables' TO [1, 2, 3]
            "healthy->vegetables": [1, 2, 3]
        }
        ```
        In this example, if you want to change the value of `"apples"`,
        you can use `healthy->fruits->apples` as the value-path.<br>
        If you don't know that the first list item is `"apples"`,
        you can use the items list index inside the value-path, so `healthy->fruits->0`.\n
        ⇾ If the given value-path doesn't exist, it will be created."""
        processed_data, data = cls.read(
            json_file=json_file,
            comment_start=comment_start,
            comment_end=comment_end,
            return_original=True,
        )

        update: dict[str, Any] = {}
        for val_path, new_val in update_values.items():
            try:
                if (path_id := Data.get_path_id(data=processed_data, value_paths=val_path, path_sep=path_sep)) is not None:
                    update[cast(str, path_id)] = new_val
                else:
                    data = cls._create_nested_path(data, val_path.split(path_sep), new_val)
            except Exception:
                data = cls._create_nested_path(data, val_path.split(path_sep), new_val)

        if update:
            data = Data.set_value_by_path_id(data, update)

        cls.create(json_file=json_file, data=dict(data), force=True)

    @staticmethod
    def _create_nested_path(data_obj: dict, path_keys: list[str], value: Any) -> dict:
        """Internal method that creates nested dictionaries/lists based on the
        given path keys and sets the specified value at the end of the path."""
        last_idx, current = len(path_keys) - 1, data_obj

        for i, key in enumerate(path_keys):
            if i == last_idx:
                if isinstance(current, dict):
                    current[key] = value
                elif isinstance(current, list) and key.isdigit():
                    idx = int(key)
                    while len(current) <= idx:
                        current.append(None)
                    current[idx] = value
                else:
                    raise TypeError(f"Cannot set key '{key}' on {type(current)}")

            else:
                next_key = path_keys[i + 1]
                if isinstance(current, dict):
                    if key not in current:
                        current[key] = [] if next_key.isdigit() else {}
                    current = current[key]
                elif isinstance(current, list) and key.isdigit():
                    idx = int(key)
                    while len(current) <= idx:
                        current.append(None)
                    if current[idx] is None:
                        current[idx] = [] if next_key.isdigit() else {}
                    current = current[idx]
                else:
                    raise TypeError(f"Cannot navigate through {type(current)}")

        return data_obj
