import builtins
import enum
import importlib.util
import inspect
import logging
import os
import sys
import typing
from os.path import relpath
from pathlib import Path

from .commons import doublewrap_val, nested
from .fields import FunctionCall
from .serialization_wrappers import Deserializer, Serializer
from .structures import Field, ImmutableStructure, Structure
from .type_info_getter import get_all_type_info, get_type_info, is_typeddict
from .types_ast import (
    extract_attributes_from_init,
    functions_to_str,
    get_imports,
    get_models,
    models_to_src,
)
from .utility import type_is_generic

builtins_types = [
    getattr(builtins, k)
    for k in dir(builtins)
    if isinstance(getattr(builtins, k), type)
]

module = getattr(inspect, "__class__")

INDENT = " " * 4

AUTOGEN_NOTE = [
    "",
    "#### This stub was autogenerated by Typedpy",
    "###########################################",
    "",
]


_private_to_public_pkg = {"werkzeug.localxxx": "flask"}


def _get_package(v, attrs):
    pkg_name = attrs.get("__package__") or "%^$%^$%^#"
    if v.startswith(pkg_name):
        return v[len(pkg_name) :]
    return _private_to_public_pkg.get(v, v)


def _as_something(k, attrs):
    return f" as {k}" if attrs.get("__file__", "").endswith("__init__.py") else ""


def _get_struct_classes(attrs, only_calling_module=True):
    return {
        k: v
        for k, v in attrs.items()
        if (
            inspect.isclass(v)
            and issubclass(v, Structure)
            and (v.__module__ == attrs["__name__"] or not only_calling_module)
            and v not in {Deserializer, Serializer, ImmutableStructure, FunctionCall}
        )
    }


def _get_imported_classes(attrs):
    def _valid_module(v):
        return hasattr(v, "__module__") and attrs["__name__"] != v.__module__

    res = []
    for k, v in attrs.items():
        if (
            not k.startswith("_")
            and (isinstance(v, module) or _valid_module(v))
            and not _is_internal_sqlalchemy(v)
        ):
            if isinstance(v, module):
                if v.__name__ != k:
                    parts = v.__name__.split(".")
                    if len(parts) > 1:
                        first_parts = parts[:-1]
                        last_part = parts[-1]
                        res.append(
                            (
                                k,
                                f"from {'.'.join(first_parts)} import {last_part} as {k}",
                            )
                        )
                    elif nested(lambda: getattr(v.os, k)) == v:
                        res.append((k, f"from os import {k} as {k}"))
                else:
                    res.append((k, f"import {k}"))
            else:
                pkg = _get_package(v.__module__, attrs)

                name_in_pkg = getattr(v, "__name__", k)
                name_to_import = name_in_pkg if name_in_pkg and name_in_pkg != k else k
                as_import = (
                    _as_something(k, attrs) if name_to_import == k else f" as {k}"
                )
                import_stmt = f"from {pkg} import {name_to_import}{as_import}"
                if pkg == "__future__":
                    res = [(k, import_stmt)] + res
                else:
                    res.append((k, import_stmt))
    return res


def _get_ordered_args(unordered_args: dict):
    optional_args = {k: v for k, v in unordered_args.items() if v.endswith("= None")}
    mandatory_args = {k: v for k, v in unordered_args.items() if k not in optional_args}
    return {**mandatory_args, **optional_args}


def _get_mapped_extra_imports(additional_imports) -> dict:
    mapped = {}
    for c in additional_imports:
        try:
            name = get_type_info(c, {}, set())
            if inspect.isclass(c) and issubclass(c, Structure):
                module_name = c.__module__
            else:
                module_name = (
                    c.get_type.__module__
                    if isinstance(c, Field)
                    else c.__module__
                    if name != "Any"
                    else None
                )
            if module_name:
                mapped[name] = module_name
        except Exception as e:
            logging.exception(e)
    return mapped


def _is_sqlalchemy(attr):
    module_name = getattr(attr, "__module__", "")
    return module_name and (
        module_name.startswith("sqlalchemy.orm")
        or module_name.startswith("sqlalchemy.sql")
    )


def _is_internal_sqlalchemy(attr):
    module_name = getattr(attr, "__module__", "")
    return module_name in {"sqlalchemy.orm.decl_api", "sqlalchemy.sql.schema"}


def _try_extract_column_type(attr):
    if attr.__class__.__name__ == "InstrumentedAttribute":
        return next(iter(attr.expression.base_columns)).type.python_type


def _skip_sqlalchemy_attribute(attribute):
    return attribute.startswith("_") or attribute in {"registry", "metadata"}


def _get_method_and_attr_list(cls, members):
    all_fields = cls.get_all_fields_by_name() if issubclass(cls, Structure) else {}
    ignored_methods = (
        dir(Structure)
        if issubclass(cls, Structure)
        else dir(Field)
        if issubclass(cls, Structure)
        else dir(enum)
        if issubclass(cls, enum.Enum)
        else {}
    )
    private_prefix = "_" if issubclass(cls, enum.Enum) else "__"
    method_list = []
    attrs = []
    cls_dict = cls.__dict__
    for attribute in members:
        if attribute.startswith(private_prefix) and attribute not in {
            "__init__",
            "__call__",
        }:
            continue
        attr = (
            cls_dict.get(attribute)
            if attribute in cls_dict
            else getattr(cls, attribute, None)
        )
        if _is_sqlalchemy(attr):
            if not _skip_sqlalchemy_attribute(attribute):
                members[attribute] = _try_extract_column_type(attr)
                attrs.append(attribute)
            continue
        is_func = not inspect.isclass(attr) and (
            callable(attr) or isinstance(attr, (property, classmethod, staticmethod))
        )
        if all(
            [
                attr is not None,
                not inspect.isclass(attr),
                not is_func,
                not (is_typeddict(cls) or isinstance(attr, cls)),
                not (issubclass(cls, Structure) and attribute.startswith("_")),
            ]
        ):
            attrs.append(attribute)
            continue

        if (
            is_func
            and attribute not in all_fields
            and (
                attribute not in ignored_methods
                or (issubclass(cls, Structure) and attribute == "__init__")
            )
        ):
            method_list.append(attribute)

    if (
        not issubclass(cls, Structure)
        and not issubclass(cls, enum.Enum)
        and "__init__" in members
        and "__init__" not in method_list
    ):
        method_list = ["__init__"] + method_list

    for name in cls_dict.get("__annotations__", {}):
        if name not in attrs:
            attrs.append(name)
    return method_list, attrs


def _is_sqlalchemy_orm_model(cls):
    return nested(lambda: str(cls.__class__.__module__), "").startswith(
        "sqlalchemy.orm"
    )


def _get_sqlalchemy_init(attributes_with_type):
    res = [f"def __init__(self, *,"]
    for p, p_type in attributes_with_type:
        res.append(f"{INDENT}{p}: {p_type} = None,")
    res.append(f"{INDENT}**kw")
    res.append(f"): ...")
    return res


def _is_not_generic_and_private_class_or_module(the_type):
    if type_is_generic(the_type):
        return False
    return getattr(the_type, "__module__", "").startswith("_") or nested(
        lambda: the_type.__class__.__name__, ""
    ).startswith("_")


def _get_methods_info(
    cls, locals_attrs, additional_classes, ignore_attributes=None
) -> list:
    ignore_attributes = ignore_attributes or set()
    members = {}
    members.update(dict(cls.__dict__))
    annotations = cls.__dict__.get("__annotations__", {})
    for a in annotations:
        members[a] = annotations[a]
    members.update(annotations)

    method_list, cls_attrs_draft = _get_method_and_attr_list(cls, members)
    cls_attrs = [a for a in cls_attrs_draft if a not in ignore_attributes]
    attributes_with_type = []
    for attr in cls_attrs:
        the_type = members.get(attr, None)
        if _is_not_generic_and_private_class_or_module(the_type):
            continue
        if inspect.isclass(the_type) or type_is_generic(the_type):
            resolved_type = (
                get_type_info(the_type, locals_attrs, additional_classes)
                if the_type
                else "Any"
            )
        else:
            resolved_type = (
                get_type_info(the_type.__class__, locals_attrs, additional_classes)
                if the_type is not None
                else "Any"
            )
        attributes_with_type.append((attr, resolved_type))
    method_by_name = [
        f"{attr}: {resolved_type}" for (attr, resolved_type) in attributes_with_type
    ]
    cls_dict = cls.__dict__

    for name in method_list:
        method_cls = members[name].__class__
        is_property = False
        func = cls_dict.get(name) if name in cls_dict else getattr(cls, name, None)
        func = (
            getattr(cls, name)
            if isinstance(func, (classmethod, staticmethod, property))
            and not _is_sqlalchemy(func)
            else func
        )
        if isinstance(func, property):
            is_property = True
            func = func.__get__
        try:
            sig = inspect.signature(func)
            return_annotations = (
                ""
                if sig.return_annotation == inspect._empty
                else f" -> None"
                if sig.return_annotation is None
                else f" -> {get_type_info(sig.return_annotation, locals_attrs, additional_classes)}"
            )
            params_by_name = []
            if _is_sqlalchemy_orm_model(cls) and name == "__init__":
                method_by_name.extend(_get_sqlalchemy_init(attributes_with_type))
                continue

            if method_cls is classmethod:
                params_by_name.append(("cls", ""))
            if is_property:
                params_by_name.append(("self", ""))
            found_last_positional = False
            arg_position = 0
            for p, v in sig.parameters.items():
                if is_property and arg_position < 2:
                    continue
                arg_position += 1
                optional_globe = (
                    "**"
                    if v.kind == inspect.Parameter.VAR_KEYWORD
                    else "*"
                    if v.kind == inspect.Parameter.VAR_POSITIONAL
                    else ""
                )
                if v.kind == inspect.Parameter.VAR_POSITIONAL:
                    found_last_positional = True
                if (
                    v.kind == inspect.Parameter.KEYWORD_ONLY
                    and not found_last_positional
                ):
                    params_by_name.append(("*", ""))
                    found_last_positional = True
                default = (
                    ""
                    if v.default == inspect._empty
                    else f" = {v.default.__name__}"
                    if inspect.isclass(v.default)
                    else " = None"
                )
                type_annotation = (
                    ""
                    if v.annotation == inspect._empty
                    else f": {get_type_info(v.annotation, locals_attrs, additional_classes)}"
                )
                p_name = f"{optional_globe}{p}"
                type_annotation = (
                    type_annotation[: -len(" = None")]
                    if (type_annotation.endswith("= None") and default)
                    else type_annotation
                )
                params_by_name.append((p_name, f"{type_annotation}{default}"))

            params_as_str = ", ".join([f"{k}{v}" for (k, v) in params_by_name])
            method_by_name.append("")
            if method_cls is staticmethod:
                method_by_name.append("@staticmethod")
            elif method_cls is classmethod:
                method_by_name.append("@classmethod")
            elif is_property:
                method_by_name.append("@property")
            method_by_name.append(
                f"def {name}({params_as_str}){return_annotations}: ..."
            )
            if name == "__init__" and not issubclass(cls, Structure):
                try:
                    source = inspect.getsource(members[name])
                    init_type_by_attr = extract_attributes_from_init(
                        source, locals_attrs, additional_classes
                    )
                    for attr_name, attr_type in init_type_by_attr.items():
                        if attr_name not in dict(attributes_with_type):
                            method_by_name = [
                                f"{attr_name}: {attr_type}"
                            ] + method_by_name
                except:
                    logging.info("not __init__ implementation found")
        except Exception as e:
            logging.warning(e)
            method_by_name.append(f"def {name}(self, *args, **kw): ...")

    return method_by_name


def _get_init(cls, ordered_args: dict, additional_properties_default: bool) -> str:
    init_params = f",\n{INDENT * 2}".join(
        [f"{INDENT * 2}self"] + [f"{k}: {v}" for k, v in ordered_args.items()]
    )
    kw_opt = (
        f",\n{INDENT * 2}**kw"
        if getattr(cls, "_additionalProperties", additional_properties_default)
        else ""
    )
    return f"    def __init__(\n{init_params}{kw_opt}\n{INDENT}): ..."


def _get_additional_structure_methods(
    cls, ordered_args: dict, additional_properties_default: bool
) -> str:
    ordered_args_with_none = {}
    for k, v in ordered_args.items():
        ordered_args_with_none[k] = v if v.endswith("= None") else f"{v} = None"
    params = [f"{k}: {v}" for k, v in ordered_args_with_none.items()]
    params_with_self = f",\n{INDENT * 2}".join([f"{INDENT * 2}self"] + params)
    kw_opt = (
        f",\n{INDENT * 2}**kw"
        if getattr(cls, "_additionalProperties", additional_properties_default)
        else ""
    )
    shallow_clone = f"    def shallow_clone_with_overrides(\n{params_with_self}{kw_opt}\n{INDENT}): ..."

    params_with_cls = f",\n{INDENT*2}".join(
        [f"{INDENT}cls", "source_object: Any", "*"] + params
    )

    from_other_class = f"\n{INDENT}".join(
        [
            "",
            "@classmethod",
            "def from_other_class(",
            f"{params_with_cls}{kw_opt}\n{INDENT}): ...",
        ]
    )
    return "\n".join([shallow_clone, "", from_other_class])


def get_stubs_of_structures(
    struct_classe_by_name: dict,
    local_attrs,
    additional_classes,
    additional_properties_default,
) -> list:
    out_src = []
    for cls_name, cls in struct_classe_by_name.items():
        fields_info = get_all_type_info(
            cls, locals_attrs=local_attrs, additional_classes=additional_classes
        )
        method_info = _get_methods_info(
            cls,
            locals_attrs=local_attrs,
            additional_classes=additional_classes,
            ignore_attributes=set(fields_info.keys()),
        )

        ordered_args = _get_ordered_args(fields_info)
        bases = _get_bases_for_structure(cls, local_attrs, additional_classes)
        bases_str = f"({', '.join(bases)})" if bases else ""
        out_src.append(f"class {cls_name}{bases_str}:")
        if not fields_info and not method_info:
            out_src.append(f"{INDENT}pass")
            out_src.append("")
            continue

        if not [m for m in method_info if m.startswith("def __init__(self")]:
            out_src.append(_get_init(cls, ordered_args, additional_properties_default))
        out_src.append("")
        out_src.append(
            _get_additional_structure_methods(
                cls, ordered_args, additional_properties_default
            )
        )
        out_src.append("")
        out_src.append("")

        for field_name, type_name in ordered_args.items():
            out_src.append(f"    {field_name}: {type_name}")
        out_src += [f"{INDENT}{m}" for m in method_info]
        out_src.append("\n")
    return out_src


def get_stubs_of_enums(
    enum_classes_by_name: dict, local_attrs, additional_classes
) -> list:
    out_src = []
    for cls_name, cls in enum_classes_by_name.items():

        method_info = _get_methods_info(
            cls, locals_attrs=local_attrs, additional_classes=additional_classes
        )
        bases = _get_bases(cls, local_attrs, additional_classes)
        bases_str = f"({', '.join(bases)})" if bases else ""
        out_src.append(f"class {cls_name}{bases_str}:")

        enum_values = [f"{INDENT}{v.name} = enum.auto()" for v in cls] or [
            f"{INDENT}pass"
        ]
        out_src.extend(enum_values)
        out_src.append("")

        out_src.append("")
        out_src += [f"{INDENT}{m}" for m in method_info]
        out_src.append("\n")
    return out_src


def add_imports(local_attrs: dict, additional_classes, existing_imports: set) -> list:
    base_typing = ["Union", "Optional", "Any", "TypeVar", "Type", "NoReturn"]
    typing_types_to_import = [t for t in base_typing if t not in existing_imports]
    base_import_statements = []
    if typing_types_to_import:
        base_import_statements.append(
            f"from typing import {', '.join(typing_types_to_import)}"
        )
    base_import_statements += [
        "from typedpy import Structure",
        "",
    ]
    extra_imports_by_name = _get_mapped_extra_imports(additional_classes)
    extra_imports = {
        f"from {_get_package(v, local_attrs)} import {k}{_as_something(k, local_attrs)}"
        for k, v in extra_imports_by_name.items()
        if (
            (
                k not in local_attrs
                or local_attrs[k].__module__ != local_attrs["__name__"]
            )
            and k not in existing_imports
        )
    }
    return base_import_statements + sorted(extra_imports)


def _get_enum_classes(attrs, only_calling_module):
    res = {}
    for k, v in attrs.items():
        if (
            inspect.isclass(v)
            and issubclass(v, enum.Enum)
            and (v.__module__ == attrs["__name__"] or not only_calling_module)
        ):
            res[k] = v

    return res


def _get_other_classes(attrs, only_calling_module):
    res = {}
    for k, v in attrs.items():
        if (
            inspect.isclass(v)
            and not issubclass(v, enum.Enum)
            and not issubclass(v, Structure)
        ):
            res[k] = v

    return res


def _get_functions(attrs, only_calling_module):
    return {
        k: v
        for k, v in attrs.items()
        if (
            inspect.isfunction(v)
            and (v.__module__ == attrs["__name__"] or not only_calling_module)
        )
    }


def _get_type_annotation(
    prefix: str, annotation, default: str, local_attrs, additional_classes
):
    def _correct_for_return_annotation(res: str):
        if "->" in prefix:
            return res[:-7] if res.endswith("= None") else res
        if default:
            cleaned_res = res[: -len(" = None")] if res.endswith("= None") else res
            return f"{cleaned_res}{default}"
        return res

    try:
        res = (
            ""
            if annotation == inspect._empty
            else f"{prefix}{get_type_info(annotation, local_attrs, additional_classes)}"
        )
        return _correct_for_return_annotation(res)
    except Exception as e:
        logging.exception(e)
        return ""


def get_stubs_of_functions(func_by_name, local_attrs, additional_classes) -> list:
    def _convert_default(d):
        return d.__name__ if inspect.isclass(d) else None

    out_src = []
    for name, func in func_by_name.items():
        sig = inspect.signature(func)
        return_annotations = _get_type_annotation(
            " -> ", sig.return_annotation, "", local_attrs, additional_classes
        )
        params_by_name = []
        found_last_positional = False
        for p, v in sig.parameters.items():
            default = (
                ""
                if v.default == inspect._empty
                else f" = {_convert_default(v.default)}"
            )
            type_annotation = _get_type_annotation(
                ": ", v.annotation, default, local_attrs, additional_classes
            )
            optional_globe = (
                "**"
                if v.kind == inspect.Parameter.VAR_KEYWORD
                else "*"
                if v.kind == inspect.Parameter.VAR_POSITIONAL
                else ""
            )
            if v.kind == inspect.Parameter.VAR_POSITIONAL:
                found_last_positional = True
            if v.kind == inspect.Parameter.KEYWORD_ONLY and not found_last_positional:
                params_by_name.append(("*", ""))
                found_last_positional = True
            p_name = f"{optional_globe}{p}"
            params_by_name.append((p_name, type_annotation))
        params_as_str = ", ".join([f"{k}{v}" for (k, v) in params_by_name])

        out_src.append(f"def {name}({params_as_str}){return_annotations}: ...")
        out_src.append("\n")
    return out_src


def _get_bases(cls, local_attrs, additional_classes) -> list:
    res = []
    for b in cls.__bases__:
        if b is object or b.__module__ == "typing" and b is not typing.Generic:
            continue
        if not _is_sqlalchemy(b):
            the_type = get_type_info(b, local_attrs, additional_classes)
            if b is typing.Generic and the_type == "Generic":
                params = [p.__name__ for p in cls.__parameters__]
                params_st = f"[{', '.join(params)}]" if params else ""
                res.append(f"{the_type}{params_st}")

            elif the_type != "Any":
                res.append(the_type)
    return res


def _get_bases_for_structure(cls, local_attrs, additional_classes) -> list:
    res = []
    for b in cls.__bases__:
        if b is object or b.__module__ == "typing":
            continue
        if b.__module__.startswith("typedpy"):
            continue
        the_type = get_type_info(b, local_attrs, additional_classes)
        if the_type != "Any":
            res.append(the_type)
    res.append("Structure")
    return res


def get_stubs_of_other_classes(
    *, other_classes, local_attrs, additional_classes, additional_imports
):
    out_src = []
    for cls_name, cls in other_classes.items():
        if cls.__module__ != local_attrs["__name__"]:
            if cls_name not in additional_imports:
                out_src += [f"class {cls_name}:", f"{INDENT}pass", ""]
            continue

        bases = _get_bases(cls, local_attrs, additional_classes)
        method_info = _get_methods_info(
            cls, locals_attrs=local_attrs, additional_classes=additional_classes
        )
        bases_str = f"({', '.join(bases)})" if bases else ""
        out_src.append(f"class {cls_name}{bases_str}:")
        if not method_info:
            out_src.append(f"{INDENT}pass")
        out_src.append("")
        out_src += [f"{INDENT}{m}" for m in method_info]
        out_src.append("\n")
    return out_src


def get_typevars(attrs, additional_classes):
    res = []
    for k, v in attrs.items():
        if isinstance(v, typing.TypeVar):
            constraints = ", ".join(
                [get_type_info(x, attrs, additional_classes) for x in v.__constraints__]
            )
            constraints_s = f", {constraints}" if constraints else ""
            res.append(f'{k} = TypeVar("{k}"{constraints_s})')
    return [""] + res + [""]


def create_pyi(
    calling_source_file,
    attrs: dict,
    only_current_module: bool = True,
    additional_properties_default=True,
):
    full_path: Path = Path(calling_source_file)
    pyi_path = (full_path.parent / f"{full_path.stem}.pyi").resolve()
    out_src = []
    additional_imports = []
    imported = list(get_imports(attrs.get("__file__")))
    if only_current_module:
        for level, pkg_name, val_info, alias in imported:
            level_s = "." * level
            val = ".".join(val_info)
            alias = alias or val
            if val and pkg_name:
                if val != "*":
                    out_src.append(f"from {level_s}{pkg_name} import {val} as {alias}")
                    additional_imports.append(alias)
                else:
                    out_src.append(f"from {level_s}{pkg_name} import {val}")
            elif pkg_name:
                out_src.append(f"import {level_s}{pkg_name}")
                additional_imports.append(pkg_name)
            else:
                out_src.append(f"import {level_s}{alias}")
                additional_imports.append(alias)

    enum_classes = _get_enum_classes(attrs, only_calling_module=only_current_module)
    if enum_classes and enum not in additional_imports:
        out_src += ["import enum", ""]
    struct_classes = _get_struct_classes(attrs, only_calling_module=only_current_module)
    other_classes = _get_other_classes(attrs, only_calling_module=only_current_module)
    functions = _get_functions(attrs, only_calling_module=only_current_module)

    additional_classes = set()
    out_src += get_typevars(attrs, additional_classes=additional_classes)

    out_src += _get_consts(
        attrs,
        additional_classes=additional_classes,
        additional_imports=additional_imports,
    )

    out_src += get_stubs_of_enums(
        enum_classes, local_attrs=attrs, additional_classes=additional_classes
    )
    out_src += get_stubs_of_other_classes(
        other_classes=other_classes,
        local_attrs=attrs,
        additional_classes=additional_classes,
        additional_imports=additional_imports,
    )
    out_src += get_stubs_of_structures(
        struct_classes,
        local_attrs=attrs,
        additional_classes=additional_classes,
        additional_properties_default=additional_properties_default,
    )

    out_src += get_stubs_of_functions(
        functions, local_attrs=attrs, additional_classes=additional_classes
    )

    from_future_import = [
        (i, s) for i, s in enumerate(out_src) if s.startswith("from __future__")
    ]
    for number_of_deletions, (i, s) in enumerate(from_future_import):
        out_src = (
            out_src[: i - number_of_deletions] + out_src[i + 1 - number_of_deletions :]
        )
    out_src = (
        [x[1] for x in from_future_import]
        + add_imports(
            local_attrs=attrs,
            additional_classes=additional_classes,
            existing_imports=set(additional_imports),
        )
        + out_src
    )

    out_src = AUTOGEN_NOTE + out_src
    out_s = "\n".join(out_src)
    with open(pyi_path, "w", encoding="UTF-8") as f:
        f.write(out_s)


def _get_consts(attrs, additional_classes, additional_imports):
    def _is_of_builtin(v) -> bool:
        return isinstance(
            v, (int, float, str, dict, list, set, complex, bool, frozenset)
        )

    def _as_builtin(v) -> str:
        if isinstance(v, str):
            return ""
        return v if isinstance(v, (int, float, complex, bool)) else v.__class__()

    res = []
    annotations = attrs.get("__annotations__", None) or {}
    constants = {
        k: v
        for (k, v) in attrs.items()
        if (
            _is_of_builtin(v)
            or v is None
            or k in annotations
            or (
                not inspect.isclass(v)
                and not inspect.isfunction(v)
                and not isinstance(v, typing.TypeVar)
            )
        )
        and not k.startswith("__")
        and k not in additional_imports
    }
    for c in constants:
        the_type = (
            get_type_info(annotations[c], attrs, additional_classes)
            if c in annotations
            else get_type_info(attrs[c].__class__, attrs, additional_classes)
            if not inspect.isclass(attrs[c]) and attrs[c] is not None
            else None
        )
        type_str = f": {the_type}" if the_type else ""
        val = (
            str(doublewrap_val(_as_builtin(attrs[c])))
            if _is_of_builtin(attrs[c])
            else "None"
            if attrs[c] is None
            else ""
        )
        val_st = f" = {val}" if val else ""
        res.append(f"{c}{type_str}{val_st}")
        res.append("")
    return res


def create_stub_for_file(
    abs_module_path: str,
    src_root: str,
    stubs_root: str = None,
    *,
    additional_properties_default=True,
):
    ext = os.path.splitext(abs_module_path)[-1].lower()
    if ext != ".py":
        return
    stem = Path(abs_module_path).stem
    dir_name = str(Path(abs_module_path).parent)
    relative_dir = relpath(dir_name, src_root)
    package_name = ".".join(Path(relative_dir).parts)
    module_name = stem if stem != "__init__" else package_name
    sys.path.append(str(Path(dir_name).parent))
    sys.path.append(src_root)
    spec = importlib.util.spec_from_file_location(module_name, abs_module_path)
    the_module = importlib.util.module_from_spec(spec)
    if not the_module.__package__:
        the_module.__package__ = package_name
    spec.loader.exec_module(the_module)

    pyi_dir = (
        Path(stubs_root) / Path(relative_dir)
        if stubs_root
        else Path(abs_module_path).parent
    )
    pyi_dir.mkdir(parents=True, exist_ok=True)
    (pyi_dir / Path("__init__.pyi")).touch(exist_ok=True)

    pyi_path = (pyi_dir / f"{stem}.pyi").resolve()
    if not getattr(the_module, "__package__", None):
        the_module.__package__ = ".".join(Path(relative_dir).parts)
    create_pyi(
        str(pyi_path),
        the_module.__dict__,
        additional_properties_default=additional_properties_default,
    )


def create_pyi_ast(calling_source_file, pyi_path):
    out_src = [
        "import datetime",
        "from typing import Optional, Any, Iterable, Union",
        "from typedpy import Structure",
    ]
    additional_imports = []
    imported = list(get_imports(calling_source_file))
    found_sqlalchmy = False
    for level, pkg_name, val_info, alias in imported:
        level_s = "." * level
        val = ".".join(val_info)
        alias = alias or val
        if pkg_name and pkg_name.startswith("sqlalchemy"):
            found_sqlalchmy = True
        if val and pkg_name:
            if val != "*":
                out_src.append(f"from {level_s}{pkg_name} import {val} as {alias}")
                additional_imports.append(alias)
            else:
                out_src.append(f"from {level_s}{pkg_name} import {val}")
        elif pkg_name:
            out_src.append(f"import {level_s}{pkg_name}")
            additional_imports.append(pkg_name)
        else:
            out_src.append(f"import {level_s}{alias}")
            additional_imports.append(alias)

    if found_sqlalchmy:
        out_src.extend(["from sqlalchemy import Column"])
        out_src.extend([""] * 3)
        models, functions = get_models(calling_source_file)
        out_src += models_to_src(models)
        out_src += functions_to_str(functions)
    out_src = AUTOGEN_NOTE + out_src
    out_s = "\n".join(out_src)
    with open(pyi_path, "w", encoding="UTF-8") as f:
        f.write(out_s)


def create_stub_for_file_using_ast(
    abs_module_path: str, src_root: str, stubs_root: str = None, **kw
):
    ext = os.path.splitext(abs_module_path)[-1].lower()
    if ext != ".py":
        return
    stem = Path(abs_module_path).stem
    dir_name = str(Path(abs_module_path).parent)
    relative_dir = relpath(dir_name, src_root)

    pyi_dir = (
        Path(stubs_root) / Path(relative_dir)
        if stubs_root
        else Path(abs_module_path).parent
    )
    pyi_dir.mkdir(parents=True, exist_ok=True)
    (pyi_dir / Path("__init__.pyi")).touch(exist_ok=True)

    pyi_path = (pyi_dir / f"{stem}.pyi").resolve()
    create_pyi_ast(abs_module_path, str(pyi_path))
