import json
from collections import OrderedDict
from pathlib import Path
from typing import Dict, Optional

from event_model.documents import ALL_DOCUMENTS, DocumentType
from event_model.documents.generate.type_wrapper import (
    GenerateJsonSchema,
    TypeAdapter,
    extra_schema,
    to_snake,
)


def sort_alphabetically(schema: Dict) -> Dict:
    """Sorts the schema alphabetically by key name, exchanging the
    properties dicts for OrderedDicts"""
    schema = OrderedDict(sorted(list(schema.items()), key=lambda x: x[0]))

    return schema


SortOrder = {
    "title": 0,
    "description": 1,
    "type": 2,
    "$defs": 3,
    "properties": 4,
    "required": 5,
    "additionalProperties": 6,
    "patternProperties": 7,
}


def sort_schema(document_schema: Dict) -> Dict:
    assert isinstance(document_schema, dict)
    document_schema = OrderedDict(
        sorted(
            list(document_schema.items()),
            key=lambda x: SortOrder.get(x[0], len(SortOrder)),
        )
    )

    for key in document_schema:
        if key in ("$defs", "properties", "required"):
            if isinstance(document_schema[key], dict):
                document_schema[key] = sort_alphabetically(document_schema[key])
                for key2 in document_schema[key]:
                    if isinstance(document_schema[key][key2], dict):
                        document_schema[key][key2] = sort_schema(
                            document_schema[key][key2]
                        )
            elif isinstance(document_schema[key], list):
                document_schema[key].sort()

    return document_schema


def add_extra_schema(document_schema: Dict, extra_schema: Dict):
    """
    Used to add user written schema to autogenerated schema.

    Once https://github.com/pydantic/pydantic/issues/8698 is fixed we can
    use the following instead of this function:

    class document_type(document_type):
        __pydantic_config__ = ConfigDict(
            title=document_type.__name__,
            json_schema_extra=extra_schema.get(document_type, None)
        )
    """
    for key in extra_schema:
        if key not in document_schema:
            document_schema[key] = extra_schema[key]

        elif not isinstance(document_schema[key], type(extra_schema[key])):
            raise ValueError(
                f"Cannot merge {repr(document_schema[key])}:"
                f"{type(document_schema[key])} with {extra_schema[key]}:"
                f"{type(extra_schema[key])} in {document_schema['title']}[{key}]"
            )

        elif isinstance(document_schema[key], dict):
            add_extra_schema(document_schema[key], extra_schema[key])

        elif isinstance(document_schema[key], list):
            document_schema[key] += extra_schema[key]
        else:
            raise ValueError(
                f"{repr(document_schema[key])}:{type(document_schema[key])} in "
                f"{document_schema['title']}[{key}] is of unsupport type"
            )


def dump_json(dictionary: Dict, file_path: Path, mode="w"):
    with open(file_path, mode) as f:
        json.dump(dictionary, f, indent=4)


class _GenerateJsonSchema(GenerateJsonSchema):
    def generate(self, schema, mode="validation"):
        json_schema = super().generate(schema, mode=mode)
        json_schema["title"] = to_snake(json_schema["title"])
        json_schema["description"] = (
            json_schema["description"].replace("\n", " ").strip()
        )
        return json_schema


def typeddict_to_schema(
    document_type: DocumentType, schema_dir: Optional[Path] = None, sort: bool = True
) -> Dict:
    assert document_type in ALL_DOCUMENTS

    type_adapter = TypeAdapter(document_type)
    type_adapter.by_alias = True
    document_schema = type_adapter.json_schema(schema_generator=_GenerateJsonSchema)

    if sort:
        document_schema = sort_schema(document_schema)

    # Add the manually defined extra schema
    add_extra_schema(document_schema, extra_schema.get(document_type, {}))

    if schema_dir:
        file_path = schema_dir / f"{to_snake(document_schema['title'])}.json"

        # Check if the file has been updated
        if not file_path.exists():
            print(f"{str(file_path)} does not exist yet, writing")
            dump_json(document_schema, file_path)
        else:
            with open(file_path) as json_file:
                skip_writing = json.load(json_file) == document_schema

            if skip_writing:
                print(f"{document_schema['title']} is unchanged")
            else:
                print(
                    f"{document_schema['title']} has been changed, writing new schema"
                )
                dump_json(document_schema, file_path)

    return document_schema
