from __future__ import annotations

from pathlib import Path
from typing import Sequence

import serde.yaml

from sera.models._property import (
    DataProperty,
    ForeignKeyOnDelete,
    ForeignKeyOnUpdate,
    WeakRef,
)
from sera.models._schema import Schema
from sera.models.parse.parse_class import parse_class_without_prop
from sera.models.parse.parse_enum import parse_enum
from sera.models.parse.parse_event import parse_data_event
from sera.models.parse.parse_property import parse_property


def parse_schema(name: str, files: Sequence[Path | str]) -> Schema:
    schema = Schema(name=name, classes={}, enums={})

    # parse all classes
    raw_defs = {}
    for file in files:
        for k, v in serde.yaml.deser(file).items():
            if k.startswith("enum:"):
                schema.enums[k[5:]] = parse_enum(schema, k[5:], v)
                continue
            cdef = parse_class_without_prop(schema, k, v)
            assert k not in schema.classes
            schema.classes[k] = cdef
            raw_defs[k] = v

    # now parse properties of the classes
    for clsname, v in raw_defs.items():
        cdef = schema.classes[clsname]
        for propname, prop in (v["props"] or {}).items():
            assert propname not in cdef.properties
            parsed_prop = parse_property(schema, cdef, propname, prop)
            cdef.properties[propname] = parsed_prop

    resolve_weakrefs(schema, raw_defs)

    # parse events after all properties are defined
    all_props = {
        f"{cls.name}.{prop.name}": prop
        for cls in schema.classes.values()
        for prop in cls.properties.values()
    }

    for clsname, v in raw_defs.items():
        cdef = schema.classes[clsname]
        all_props = dict(all_props, **cdef.properties)
        for event_data in v.get("events", []):
            event = parse_data_event(event_data, all_props)
            cdef.events.append(event)

    return schema


def resolve_weakrefs(schema: Schema, raw_defs: dict[str, dict]) -> None:
    for clsname, raw_def in raw_defs.items():
        cls = schema.classes[clsname]
        for prop_name, prop_def in (raw_def.get("props") or {}).items():
            if "weakref" not in prop_def:
                continue

            prop = cls.properties[prop_name]
            if not isinstance(prop, DataProperty):
                raise ValueError(
                    f"Weakref is only supported for data properties: {cls.name}.{prop.name}"
                )
            if cls.db is None:
                raise ValueError(
                    f"Weakref requires database class: {cls.name}.{prop.name}"
                )
            if prop.db is None:
                raise ValueError(
                    f"Weakref requires database property: {cls.name}.{prop.name}"
                )
            if prop.db.foreign_key is not None:
                raise ValueError(
                    f"Weakref cannot be combined with db.foreign_key: {cls.name}.{prop.name}"
                )
            weakref_data = prop_def["weakref"]
            if not isinstance(weakref_data, dict):
                raise ValueError(
                    f"Weakref for {cls.name}.{prop.name} must be a mapping"
                )
            target = weakref_data.get("target")
            if not isinstance(target, str) or target.strip() == "":
                raise ValueError(
                    f"Weakref target for {cls.name}.{prop.name} must be a non-empty string"
                )
            if "." not in target:
                raise ValueError(
                    f"Weakref target must be in format Class.prop: {cls.name}.{prop.name}"
                )
            target_cls_name, target_prop_name = target.split(".", 1)
            target_cls = schema.classes.get(target_cls_name)
            if target_cls is None:
                raise ValueError(
                    f"Weakref target class not found: {target_cls_name} for {cls.name}.{prop.name}"
                )
            target_prop = target_cls.properties.get(target_prop_name)
            if target_prop is None:
                raise ValueError(
                    f"Weakref target property not found: {target_cls_name}.{target_prop_name}"
                )
            if not isinstance(target_prop, DataProperty):
                raise ValueError(
                    f"Weakref target must be a data property: {target_cls_name}.{target_prop_name}"
                )
            if target_prop.db is None:
                raise ValueError(
                    f"Weakref target property must be stored in DB: {target_cls_name}.{target_prop_name}"
                )
            if prop.datatype != target_prop.datatype:
                raise ValueError(
                    "Weakref data type mismatch: "
                    f"{cls.name}.{prop.name} ({prop.datatype}) vs "
                    f"{target_cls_name}.{target_prop_name} ({target_prop.datatype})"
                )
            if (
                ForeignKeyOnDelete(weakref_data.get("on_delete", "restrict")).value
                == "set null"
                and not prop.is_optional
            ):
                raise ValueError(
                    f"Weakref on_delete set null requires optional property: {cls.name}.{prop.name}"
                )

            prop.weakref = WeakRef(
                target_class=target_cls,
                target_prop=target_prop,
                on_delete=ForeignKeyOnDelete(weakref_data.get("on_delete", "restrict")),
                on_update=ForeignKeyOnUpdate(weakref_data.get("on_update", "restrict")),
            )
