from typing import Any
from hashlib import md5
import os
import json
from jsonschema import validate
from ._schemas import _fetch_schema
from copy import copy


def _strip_none_values(x):
    if isinstance(x, dict):
        kill = []
        for k, v in x.items():
            if isinstance(v, dict) or isinstance(v, list):
                _strip_none_values(v)
            elif v is None:
                kill.append(k)
        if len(kill):
            for k in kill:
                del x[k]
    elif isinstance(x, list):
        for v in x:
            if isinstance(v, dict) or isinstance(v, list):
                _strip_none_values(v)


def write_metadata(meta: dict[str, Any], dir: str, ignore_none: bool = True) -> dict[str, str]:
    """Write metadata to disk after validating it against its schema. 

    ``meta`` should contain at least the ``$schema`` and ``path`` keys. 
    ``$schema`` is used to identify the schema for validation; it should be
    a string or a 2-tuple containing the schema name and the name of the 
    package in which the schema file can be found.

    If the metadata describes a resource with a file representation, that file
    should already be present inside ``dir`` at the specified ``path``.  Also,
    if no ``md5sum`` key is present,  the MD5 checksum of the file at ``path``
    is computed and added to the metadata before saving.

    Any preceding ``./`` in the ``path`` is automatically removed.

    Args:
        meta: 
            Metadata for an object, typically generated by a
            :py:meth:`~dolomite_base.stage_object.stage_object` method.

        dir:
            Path to the staging directory.

        ignore_none:
            Whether keys with None values should be automatically removed from
            ``meta``. This is useful for sanitizing messy user-supplied inputs.

    Returns:
        Dictionary that can be added as the ``resource`` property of parent
        metadata, if the current object is a child of another object. The
        metadata itself is written to a JSON file at a path derived from
        (or equal to) ``meta["path"]``.
    """
    meta = copy(meta) # make an internal copy.
    if ignore_none:
        _strip_none_values(meta)

    schema = meta["$schema"]
    if isinstance(schema, tuple):
        meta["$schema"] = schema[0]
        pkg = schema[1]
        schema = schema[0]
    else:
        pkg = "dolomite_schemas"
    schema_details = _fetch_schema(pkg, schema)

    if meta["path"].startswith("./"): # removing for convenience
        meta["path"] = meta["path"][2:]

    meta_only = False
    if "_attributes" in schema_details:
        attributes = schema_details["_attributes"]
        if "metadata_only" in attributes:
            meta_only = attributes["metadata_only"]

    jpath = meta["path"]
    if not meta_only:
        jpath += ".json"
        if "md5sum" not in meta and not schema.startswith("redirection/"):
            with open(os.path.join(dir, meta["path"]), "rb") as handle:
                hasher = md5()
                while True:
                    chunk = handle.read(65536)
                    if not chunk:
                        break
                    hasher.update(chunk)
                meta["md5sum"] = hasher.hexdigest()

    validate(meta, schema_details)
    with open(os.path.join(dir, jpath), "w") as handle:
        json.dump(meta, handle)

    return {
        "type": "local",
        "path": meta["path"]
    }
