from pathlib import Path
from typing import Optional, Set, Union, List

from crudhex.domain.config.SpecConfig import SpecConfig
from crudhex.domain.models import IdMeta, Relation, GenerationType, RelationType
from crudhex.domain.models import Entity, Field
from crudhex.domain.services import TypeResolver
from crudhex.domain.services.type_resolver import get_type_resolver

from crudhex.domain.ports import spec_loader


def parse_spec_file(spec_path: Path) -> List[Entity]:
    spec_data = spec_loader.load_spec_config(spec_path)
    type_resolver = get_type_resolver()

    return parse_spec_data(spec_data, type_resolver)


def parse_spec_data(spec_data: dict, type_resolver: Optional[TypeResolver] = None) -> List[Entity]:
    entities = []
    # noinspection PyTypeChecker
    generated_types = spec_data.keys()  # type: set
    for name, spec in spec_data.items():
        entities.append(parse_entity_data(name, spec, generated_types, type_resolver))

    return entities


def parse_entity_data(name: str, data: dict, generated_types: Set[str],
                      type_resolver: Optional[TypeResolver]) -> Entity:
    entity = Entity(name)
    entity.meta = parse_entity_meta(data)

    fields_data = data.copy()
    if SpecConfig.META in fields_data: fields_data.pop(SpecConfig.META)

    for name, field_data in fields_data.items():
        entity.fields.append(parse_field(name, field_data, generated_types, type_resolver))

    return entity


def parse_entity_meta(data: dict) -> Optional[Entity.Meta]:
    meta = data.get(SpecConfig.META)
    if not meta or SpecConfig.TABLE not in meta: return None

    return Entity.Meta(meta[SpecConfig.TABLE])


def parse_field(name: str, data: Union[str, dict], generated_types: Set[str],
                type_resolver: Optional[TypeResolver]) -> Field:
    if type(data) is str:
        class_type = data if not type_resolver else type_resolver.resolve_type(data)
        return Field(name, class_type, class_type in generated_types)

    if SpecConfig.TYPE not in data: raise RuntimeError(f'Missing required key {SpecConfig.TYPE}')

    class_type = (data[SpecConfig.TYPE]
                  if not type_resolver
                  else type_resolver.resolve_type(data[SpecConfig.TYPE]))

    field = Field(name, class_type, class_type in generated_types)
    field.column = data.get(SpecConfig.COLUMN)
    field.id_meta = parse_field_id_meta(data)
    field.relation = parse_field_relation(data)

    # TODO: Configurable in dsl file
    if field.has_relation() and field.relation.type.has_multiple():
        field.type.collection_type = TypeResolver.DEFAULT_COLLECTION

    return field


def parse_field_id_meta(data: dict) -> Optional[IdMeta]:
    id_data = data.get(SpecConfig.ID)
    if not id_data: return None

    if type(id_data) is str:
        return IdMeta(GenerationType(id_data))

    id_meta = IdMeta(GenerationType(id_data[SpecConfig.TYPE]))
    id_meta.sequence = id_data.get(SpecConfig.SEQUENCE)

    return id_meta


def parse_field_relation(data: dict) -> Optional[Relation]:
    relation_data = data.get(SpecConfig.RELATION)
    if not relation_data: return None

    relation = Relation(RelationType(relation_data[SpecConfig.TYPE]))
    relation.join_table = relation_data.get(SpecConfig.JOIN_TABLE)
    relation.join_column = relation_data.get(SpecConfig.JOIN_COLUMN)
    relation.inverse_join_column = relation_data.get(SpecConfig.INVERSE_JOIN_COLUMN)
    relation.mapped_by = relation_data.get(SpecConfig.MAPPED_BY)

    return relation
