import re
import textwrap
from collections import defaultdict
from collections.abc import Callable, Iterable, Iterator
from typing import Any

from jinja2 import Environment

from xsdata.codegen.models import Attr, AttrType, Class
from xsdata.codegen.utils import ClassUtils
from xsdata.formats.converter import converter
from xsdata.formats.dataclass.models.elements import XmlType
from xsdata.models.config import (
    DocstringStyle,
    ExtensionType,
    GeneratorConfig,
    GeneratorExtension,
    ObjectType,
    OutputFormat,
)
from xsdata.models.enums import Tag
from xsdata.utils import collections, namespaces, text
from xsdata.utils.objects import literal_value


class Filters:
    """Jinja filters for code generation."""

    DEFAULT_KEY = "default"
    FACTORY_KEY = "default_factory"

    __slots__ = (
        "class_case",
        "class_safe_prefix",
        "constant_case",
        "constant_safe_prefix",
        "default_class_annotation",
        "docstring_style",
        "extensions",
        "field_case",
        "field_safe_prefix",
        "format",
        "generic_collections",
        "import_patterns",
        "max_line_length",
        "module_case",
        "module_safe_prefix",
        "package_case",
        "package_safe_prefix",
        "relative_imports",
        "substitutions",
    )

    def __init__(self, config: GeneratorConfig):
        """Initialize the filters."""
        self.substitutions: dict[ObjectType, dict[str, str]] = defaultdict(dict)
        for sub in config.substitutions.substitution:
            self.substitutions[sub.type][sub.search] = sub.replace

        self.import_patterns: dict[str, dict[str, set[str]]] = defaultdict(
            lambda: defaultdict(set)
        )
        self.extensions: dict[ExtensionType, list[GeneratorExtension]] = defaultdict(
            list
        )
        for ext in config.extensions.extension:
            self.extensions[ext.type].append(ext)

            is_annotation = ext.type is ExtensionType.DECORATOR
            patterns = self.import_patterns[ext.module_path][ext.func_name]

            if is_annotation:
                patterns.add(f"@{ext.func_name}")
            else:
                patterns.update(
                    [
                        f"({ext.func_name}",
                        f", {ext.func_name}",
                        f" {ext.func_name})",
                    ]
                )

        self.class_case: Callable = config.conventions.class_name.case
        self.field_case: Callable = config.conventions.field_name.case
        self.constant_case: Callable = config.conventions.constant_name.case
        self.package_case: Callable = config.conventions.package_name.case
        self.module_case: Callable = config.conventions.module_name.case
        self.class_safe_prefix: str = config.conventions.class_name.safe_prefix
        self.field_safe_prefix: str = config.conventions.field_name.safe_prefix
        self.constant_safe_prefix: str = config.conventions.constant_name.safe_prefix
        self.package_safe_prefix: str = config.conventions.package_name.safe_prefix
        self.module_safe_prefix: str = config.conventions.module_name.safe_prefix
        self.docstring_style: DocstringStyle = config.output.docstring_style
        self.max_line_length: int = config.output.max_line_length
        self.generic_collections: bool = config.output.generic_collections
        self.relative_imports: bool = config.output.relative_imports
        self.format = config.output.format

        # Build things
        for module, imports in self.build_import_patterns().items():
            for imp, patterns in imports.items():
                self.import_patterns[module][imp].update(patterns)

        self.default_class_annotation = self.build_class_annotation(self.format)

    def register(self, env: Environment) -> None:
        """Register the template filters to the jinja environment."""
        env.globals.update(
            {
                "docstring_name": self.docstring_style.name.lower(),
            }
        )
        env.filters.update(
            {
                "field_name": self.field_name,
                "field_type": self.field_type,
                "field_default": self.field_default_value,
                "field_metadata": self.field_metadata,
                "field_definition": self.field_definition,
                "class_name": self.class_name,
                "class_bases": self.class_bases,
                "class_annotations": self.class_annotations,
                "class_params": self.class_params,
                "format_string": self.format_string,
                "format_docstring": self.format_docstring,
                "constant_name": self.constant_name,
                "constant_value": self.constant_value,
                "default_imports": self.default_imports,
                "format_metadata": self.format_metadata,
                "type_name": self.type_name,
                "text_wrap": self.text_wrap,
                "clean_docstring": self.clean_docstring,
                "import_module": self.import_module,
                "import_class": self.import_class,
                "post_meta_hook": self.post_meta_hook,
            }
        )

    @classmethod
    def build_class_annotation(cls, fmt: OutputFormat) -> str:
        """Build the class annotations."""
        args = []
        if not fmt.repr:
            args.append("repr=False")
        if not fmt.eq:
            args.append("eq=False")
        if fmt.order:
            args.append("order=True")
        if fmt.unsafe_hash:
            args.append("unsafe_hash=True")
        if fmt.frozen:
            args.append("frozen=True")
        if fmt.slots:
            args.append("slots=True")

        args.append("kw_only=True")

        return f"@dataclass({', '.join(args)})" if args else "@dataclass"

    def class_params(self, obj: Class) -> Iterator[tuple[str, str]]:
        """Yield the class variables with their docstring text."""
        is_enum = obj.is_enumeration
        for attr in obj.attrs:
            name = attr.name
            docstring = self.clean_docstring(attr.help)
            if is_enum:
                yield self.constant_name(name, obj.name), docstring
            else:
                yield self.field_name(name, obj.name), docstring

    def class_name(self, name: str) -> str:
        """Class name filter.

        Steps:
            - Apply substitutions before naming conventions
            - Apply naming convention
            - Apply substitutions after naming conventions

        Args:
            name: The original class name

        Returns:
            The final class name
        """
        name = self.apply_substitutions(name, ObjectType.CLASS)
        name = self.safe_name(name, self.class_safe_prefix, self.class_case)
        return self.apply_substitutions(name, ObjectType.CLASS)

    def class_bases(self, obj: Class, class_name: str) -> list[str]:
        """Return a list of base class names."""
        bases = [self.type_name(x.type) for x in obj.extensions]

        derived = len(obj.extensions) > 0
        for ext in self.extensions[ExtensionType.CLASS]:
            is_valid = not derived or ext.apply_if_derived
            if is_valid and ext.pattern.match(class_name):
                if ext.prepend:
                    bases.insert(0, ext.func_name)
                else:
                    bases.append(ext.func_name)

        return collections.unique_sequence(bases)

    @classmethod
    def _parent_match(clazz, ext: GeneratorExtension, obj: Class) -> bool:
        if not ext.parent_pattern:
            return True
        parent_path = ".".join(obj.parent_names())
        return bool(ext.parent_pattern.match(parent_path))

    def class_annotations(self, obj: Class, class_name: str) -> list[str]:
        """Return a list of decorator names."""
        annotations = []
        if self.default_class_annotation:
            annotations.append(self.default_class_annotation)

        derived = len(obj.extensions) > 0
        for ext in self.extensions[ExtensionType.DECORATOR]:
            is_valid = not derived or ext.apply_if_derived
            if (
                is_valid
                and ext.pattern.match(class_name)
                and self._parent_match(ext, obj)
            ):
                if ext.prepend:
                    annotations.insert(0, f"@{ext.func_name}")
                else:
                    annotations.append(f"@{ext.func_name}")

        return collections.unique_sequence(annotations)

    def apply_substitutions(self, name: str, obj_type: ObjectType) -> str:
        """Apply name substitutions by obj type."""
        for search, replace in self.substitutions[obj_type].items():
            name = re.sub(search, replace, name)

        return name

    def field_definition(
        self,
        obj: Class,
        attr: Attr,
        parent_namespace: str | None,
    ) -> str:
        """Return the field definition with any extra metadata."""
        ns_map = obj.ns_map
        default_value = self.field_default_value(attr, ns_map)
        metadata = self.field_metadata(obj, attr, parent_namespace)

        kwargs: dict[str, Any] = {}
        if attr.fixed or attr.is_prohibited:
            kwargs["init"] = False

            if attr.is_prohibited:
                kwargs[self.DEFAULT_KEY] = None

        if default_value is not False and not attr.is_prohibited:
            key = self.FACTORY_KEY if attr.is_factory else self.DEFAULT_KEY
            kwargs[key] = default_value

        if metadata:
            kwargs["metadata"] = metadata

        return f"field({self.format_arguments(kwargs, 4)})"

    def field_name(self, name: str, class_name: str) -> str:
        """Field name filter.

        Steps:
            - Apply substitutions before naming conventions
            - Apply naming convention
            - Apply substitutions after naming conventions

        Args:
            name: The original field name
            class_name: The class name, some naming conventions require it

        Returns:
            The final field name
        """
        prefix = self.field_safe_prefix
        name = self.apply_substitutions(name, ObjectType.FIELD)
        name = self.safe_name(name, prefix, self.field_case, class_name=class_name)
        return self.apply_substitutions(name, ObjectType.FIELD)

    def constant_name(self, name: str, class_name: str) -> str:
        """Constant name filter.

        Steps:
            - Apply substitutions before naming conventions
            - Apply naming convention
            - Apply substitutions after naming conventions

        Args:
            name: The original constant name
            class_name: The class name, some naming conventions require it

        Returns:
            The final constant name
        """
        prefix = self.field_safe_prefix
        name = self.apply_substitutions(name, ObjectType.FIELD)
        name = self.safe_name(name, prefix, self.constant_case, class_name=class_name)
        return self.apply_substitutions(name, ObjectType.FIELD)

    def module_name(self, name: str) -> str:
        """Module name filter.

        Steps:
            - Apply substitutions before naming conventions
            - Apply naming convention
            - Apply substitutions after naming conventions

        Args:
            name: The original module name

        Returns:
            The final module name
        """
        prefix = self.module_safe_prefix
        name = self.apply_substitutions(name, ObjectType.MODULE)
        name = self.safe_name(namespaces.clean_uri(name), prefix, self.module_case)
        return self.apply_substitutions(name, ObjectType.MODULE)

    def package_name(self, name: str) -> str:
        """Package name filter.

        Steps:
            - Apply substitutions before naming conventions
            - Apply naming convention
            - Apply substitutions after naming conventions

        Args:
            name: The original package name

        Returns:
            The final package name
        """
        name = self.apply_substitutions(name, ObjectType.PACKAGE)

        if not name:
            return name

        def process_sub_package(pck: str) -> str:
            pck = self.safe_name(pck, self.package_safe_prefix, self.package_case)
            return self.apply_substitutions(pck, ObjectType.PACKAGE)

        parts = map(process_sub_package, name.split("."))
        name = ".".join(parts)

        return self.apply_substitutions(name, ObjectType.PACKAGE)

    def type_name(self, attr_type: AttrType) -> str:
        """Field type filter.

        Args:
            attr_type: The attr type instance.

        Returns:
            The python type name or the user type final name
        """
        datatype = attr_type.datatype
        if datatype:
            return datatype.type.__name__

        return self.class_name(attr_type.alias or attr_type.name)

    def safe_name(
        self,
        name: str,
        prefix: str,
        name_case: Callable,
        **kwargs: Any,
    ) -> str:
        """Sanitize names for safe generation."""
        if not name:
            return self.safe_name(prefix, prefix, name_case, **kwargs)

        if re.match(r"^-\d*\.?\d+$", name):
            return self.safe_name(f"{prefix}_minus_{name}", prefix, name_case, **kwargs)

        slug = text.alnum(name)
        if not slug or not slug[0].isalpha():
            return self.safe_name(f"{prefix}_{name}", prefix, name_case, **kwargs)

        result = name_case(name, **kwargs)
        if text.is_reserved(result):
            return self.safe_name(f"{name}_{prefix}", prefix, name_case, **kwargs)

        return result

    def import_module(self, module: str, from_module: str) -> str:
        """Convert import module to relative path if config is enabled."""
        if self.relative_imports:
            mp = module.split(".")
            fp = from_module.split(".")
            index = 0

            # Find common parts index
            while len(mp) > index and len(fp) > index and mp[index] == fp[index]:
                index += 1

            if index > 0:
                # Replace common parts with dots
                return f"{'.' * max(1, len(fp) - index)}{'.'.join(mp[index:])}"

        return module

    def import_class(self, name: str, alias: str | None) -> str:
        """Convert import class name with alias support."""
        if alias:
            return f"{self.class_name(name)} as {self.class_name(alias)}"

        return self.class_name(name)

    def post_meta_hook(self, obj: Class) -> str | None:
        """Plugin hook to render additional information after the xsdata meta class."""
        return None

    def field_metadata(
        self,
        obj: Class,
        attr: Attr,
        parent_namespace: str | None,
    ) -> dict:
        """Return a metadata dictionary for the given attribute."""
        if attr.is_prohibited:
            return {"type": XmlType.IGNORE}

        name = namespace = None

        if not attr.is_nameless and attr.local_name != self.field_name(
            attr.name, obj.name
        ):
            name = attr.local_name

        if parent_namespace != attr.namespace or attr.is_attribute:
            namespace = attr.namespace

        restrictions = attr.restrictions.asdict(attr.native_types)

        metadata = {
            "wrapper": attr.wrapper,
            "name": name,
            "type": attr.xml_type,
            "namespace": namespace,
            "mixed": attr.mixed,
            "choices": self.field_choices(obj, attr, parent_namespace),
            **restrictions,
        }

        if not attr.is_attribute or attr.default is None:
            metadata.pop("required", None)

        if self.docstring_style == DocstringStyle.ACCESSIBLE and attr.help:
            metadata["doc"] = self.clean_docstring(attr.help, False)

        return self.filter_metadata(metadata)

    def field_choices(
        self,
        obj: Class,
        attr: Attr,
        parent_namespace: str | None,
    ) -> tuple | None:
        """Return a tuple of field metadata if the attr has choices."""
        if not attr.choices:
            return None

        result = []
        for choice in attr.choices:
            types = choice.native_types
            restrictions = choice.restrictions.asdict(types)
            namespace = (
                choice.namespace if parent_namespace != choice.namespace else None
            )

            metadata = {
                "name": choice.local_name,
                "wildcard": choice.is_wildcard,
                "type": self.choice_type(obj, choice),
                "namespace": namespace,
            }

            if choice.is_nameless:
                del metadata["name"]

            if choice.is_tokens:
                metadata[self.FACTORY_KEY] = self.field_default_value(choice)

            metadata.update(restrictions)

            if self.docstring_style == DocstringStyle.ACCESSIBLE and choice.help:
                metadata["doc"] = self.clean_docstring(choice.help, False)

            result.append(self.filter_metadata(metadata))

        return tuple(result)

    @classmethod
    def filter_metadata(cls, data: dict) -> dict:
        """Filter out false,none keys from the given dict."""
        return {
            key: value
            for key, value in data.items()
            if value is not None and value is not False
        }

    def format_arguments(self, data: dict, indent: int = 0) -> str:
        """Return a pretty keyword arguments representation."""
        ind = " " * indent
        fmt = "    {}{}={}"
        lines = [
            fmt.format(ind, key, self.format_metadata(value, indent + 4, key))
            for key, value in data.items()
        ]

        return "\n{}\n{}".format(",\n".join(lines), ind) if lines else ""

    def format_metadata(self, data: Any, indent: int = 0, key: str = "") -> str:
        """Prettify field metadata for code generation."""
        if isinstance(data, dict):
            return self.format_dict(data, indent)

        if collections.is_array(data):
            return self.format_iterable(data, indent)

        if isinstance(data, str):
            return self.format_string(data, indent, key, 4)

        return literal_value(data)

    def format_dict(self, data: dict, indent: int) -> str:
        """Return a pretty string representation of a dict."""
        ind = " " * indent
        fmt = '    {}"{}": {},'
        lines = [
            fmt.format(ind, key, self.format_metadata(value, indent + 4, key))
            for key, value in data.items()
        ]

        return "{{\n{}\n{}}}".format("\n".join(lines), ind)

    def format_iterable(self, data: Iterable, indent: int) -> str:
        """Return a pretty string representation of an iterable."""
        ind = " " * indent
        fmt = "    {}{},"
        lines = [
            fmt.format(ind, self.format_metadata(value, indent + 4)) for value in data
        ]
        wrap = "(\n{}\n{})" if isinstance(data, tuple) else "[\n{}\n{}]"
        return wrap.format("\n".join(lines), ind)

    def format_string(self, data: str, indent: int, key: str = "", pad: int = 0) -> str:
        """Return a pretty string representation of a string.

        If the total length of the input string plus indent plus the key
        length and the additional pad is more than the max line length,
        wrap the text into multiple lines, avoiding breaking long words
        """
        if data.startswith("ForwardRef("):
            return data

        if data.startswith("Type["):
            return data[5:-1]

        if data.startswith("Literal[") and data.endswith("]"):
            return data[8:-1]

        if key in (self.FACTORY_KEY, self.DEFAULT_KEY):
            return data

        if key == "pattern":
            return f"r{data!r}".replace("\\\\", "\\")

        if data == "":
            return '""'

        start = indent + 2  # plus quotes
        start += len(key) + pad if key else 0

        value = text.escape_string(data)
        length = len(value) + start
        if length < self.max_line_length or " " not in value:
            return f'"{value}"'

        next_indent = indent + 4
        value = "\n".join(
            f'{" " * next_indent}"{line}"'
            for line in textwrap.wrap(
                value,
                width=self.max_line_length - next_indent - 2,  # plus quotes
                drop_whitespace=False,
                replace_whitespace=False,
                break_long_words=True,
            )
        )
        return f"(\n{value}\n{' ' * indent})"

    def text_wrap(
        self, string: str, offset: int = 0, subsequent_indent: str = "    "
    ) -> str:
        """Wrap text in respect to the max line length and the given offset."""
        return "\n".join(
            textwrap.wrap(
                string,
                width=self.max_line_length - offset,
                drop_whitespace=True,
                replace_whitespace=True,
                break_long_words=False,
                subsequent_indent=subsequent_indent,
            )
        )

    @classmethod
    def clean_docstring(cls, string: str | None, escape: bool = True) -> str:
        """Prepare string for docstring generation.

        - Strip whitespace from each line
        - Replace triple double quotes with single quotes
        - Escape backslashes

        Args:
            string: input value
            escape: skip backslashes escape, if string is going to
                pass through formatting.

        Returns:
            The cleaned docstring text.
        """
        if not string:
            return ""

        def _clean(txt: str) -> str:
            if escape:
                txt = txt.replace("\\", "\\\\")

            return txt.replace('"""', "'''").strip()

        return "\n".join(_clean(line) for line in string.splitlines() if line.strip())

    def format_docstring(self, doc_string: str, level: int) -> str:
        """Format docstrings with proper wrapping.

        Splits text into summary (first sentence) and description (rest),
        then wraps each appropriately.

        Args:
            doc_string: The raw docstring from the template
            level: The indentation level (1 for top-level class, 2+ for nested)

        Returns:
            The formatted docstring with proper line wrapping.
        """
        sep_pos = doc_string.rfind('"""')
        if sep_pos == -1:
            return ""

        content = doc_string[:sep_pos]
        params = doc_string[sep_pos + 3 :].strip()

        # Extract the text (between opening """ and closing """)
        text = content[3:].strip() if content.startswith('"""') else content.strip()

        # Empty docstring with no params
        if not text and not params:
            return ""

        # Calculate available width for text (accounting for indentation and quotes)
        indent_width = level * 4
        wrap_width = self.max_line_length - indent_width - 4

        # Process the text: split into summary and description
        if text:
            # Normalize whitespace
            text = " ".join(text.split())

            # Add period if text doesn't end with punctuation
            if text and text[-1] not in ".!?:":
                text += "."

            # Split into summary (first sentence) and description (rest)
            summary, description = self._split_docstring(text)

            # Wrap summary and description separately
            wrapped_summary = "\n".join(
                textwrap.wrap(
                    summary,
                    width=wrap_width,
                    break_long_words=False,
                    break_on_hyphens=False,
                )
            )

            if description:
                wrapped_description = "\n".join(
                    textwrap.wrap(
                        description,
                        width=wrap_width,
                        break_long_words=False,
                        break_on_hyphens=False,
                    )
                )
                wrapped_text = f"{wrapped_summary}\n\n{wrapped_description}"
            else:
                wrapped_text = wrapped_summary
        else:
            wrapped_text = ""

        # Build the final docstring
        if params:
            if wrapped_text:
                return f'"""\n{wrapped_text}\n\n{params}\n"""'
            return f'"""\n{params}\n"""'
        return f'"""\n{wrapped_text}\n"""'

    @staticmethod
    def _split_docstring(text: str) -> tuple[str, str]:
        """Split docstring text into summary and description.

        The summary is the first sentence (ending with '. ' followed by
        a capital letter). The description is everything after.

        Args:
            text: The normalized docstring text

        Returns:
            A tuple of (summary, description)
        """
        # Look for sentence end: period/exclamation/question followed by
        # space and a capital letter (start of new sentence)
        match = re.search(r"[.!?] (?=[A-Z])", text)
        if match:
            split_pos = match.start() + 1  # Include the punctuation
            return text[:split_pos], text[split_pos:].strip()
        return text, ""

    def field_default_value(self, attr: Attr, ns_map: dict | None = None) -> Any:
        """Generate the field default value/factory for the given attribute."""
        if attr.is_list or (attr.is_tokens and not attr.default):
            return "tuple" if self.format.frozen else "list"
        if attr.is_dict:
            return "dict"
        if attr.default is None:
            return None if attr.is_optional else False
        if not isinstance(attr.default, str):
            return literal_value(attr.default)
        if attr.default.startswith("@enum@"):
            return self.field_default_enum(attr)

        types = converter.sort_types(attr.native_types)

        if attr.is_tokens:
            return self.field_default_tokens(attr, types, ns_map)

        return literal_value(
            converter.deserialize(
                attr.default, types, ns_map=ns_map, format=attr.restrictions.format
            )
        )

    def field_default_enum(self, attr: Attr) -> str:
        """Generate the default value for enum fields."""
        assert attr.default is not None

        qname, reference = attr.default[6:].split("::", 1)
        qname = next(x.alias or qname for x in attr.types if x.qname == qname)
        name = namespaces.local_name(qname)
        class_name = self.class_name(name)

        if attr.is_tokens:
            members = [
                f"Literal[{class_name}.{self.constant_name(member, name)}]"
                for member in reference.split("@")
            ]
            return f"lambda: {self.format_metadata(members, indent=8)}"

        return f"{class_name}.{self.constant_name(reference, name)}"

    def field_default_tokens(
        self, attr: Attr, types: list[type], ns_map: dict | None
    ) -> str:
        """Generate the default value for tokens fields."""
        assert isinstance(attr.default, str)

        fmt = attr.restrictions.format
        factory = tuple if self.format.frozen else list
        tokens = factory(
            converter.deserialize(val, types, ns_map=ns_map, format=fmt)
            for val in attr.default.split()
        )

        if attr.is_enumeration:
            return self.format_metadata(tuple(tokens), indent=8)

        return f"lambda: {self.format_metadata(tokens, indent=8)}"

    def field_type(self, obj: Class, attr: Attr) -> str:
        """Generate type hints for the given attr."""
        if attr.is_prohibited:
            return "Any"

        if attr.tag == Tag.CHOICE:
            return self.compound_field_types(obj, attr)

        result = self._field_type_names(obj, attr, choice=False)

        iterable_fmt = self._get_iterable_format()
        if attr.is_tokens:
            result = iterable_fmt.format(result)

        if attr.is_list:
            return iterable_fmt.format(result)

        if attr.is_tokens:
            return result

        if attr.is_dict:
            if self.generic_collections:
                return "Mapping[str, str]"

            return "dict[str, str]"

        if attr.is_nillable or (attr.default is None and attr.is_optional):
            return f"None | {result}"

        return result

    def compound_field_types(self, obj: Class, attr: Attr) -> str:
        """Generate type hint for a compound field.

        Args:
            obj: The parent class instance
            attr: The compound attr instance

        Returns:
            The string representation of the type hint.
        """
        results = []
        iterable_fmt = self._get_iterable_format()
        for choice in attr.choices:
            names = self._field_type_names(obj, choice, choice=False)
            if choice.is_tokens:
                names = iterable_fmt.format(names)
            results.append(names)

        result = self._join_type_names(results)

        if attr.is_list:
            return iterable_fmt.format(result)

        if attr.is_optional:
            return f"None | {result}"

        return result

    def choice_type(self, obj: Class, choice: Attr) -> str:
        """Generate type hints for the given choice.

        Choices support a subset of features from normal attributes.
        First of all we don't have a proper type hint but a type
        metadata key. That's why we always need to wrap as Type[xxx].
        The second big difference is that our choice belongs to a
        compound field that might be a list, that's why list restriction
        is also ignored.

        Args:
            obj: The parent class instance
            choice: The choice instance

        Returns:
            The string representation of the type hint.
        """
        result = self._field_type_names(obj, choice, choice=True)

        if choice.is_tokens:
            iterable_fmt = self._get_iterable_format()
            result = iterable_fmt.format(result)

        if result.startswith('"'):
            return f"ForwardRef({result})"

        return f"Type[{result}]"

    def _field_type_names(
        self,
        obj: Class,
        attr: Attr,
        choice: bool = False,
    ) -> str:
        type_names = [self._field_type_name(obj, x, choice=choice) for x in attr.types]
        return self._join_type_names(type_names)

    def _join_type_names(self, type_names: list[str]) -> str:
        type_names = collections.unique_sequence(type_names)
        if len(type_names) == 1:
            return type_names[0]

        return " | ".join(type_names)

    def _field_type_name(
        self, obj: Class, attr_type: AttrType, choice: bool = False
    ) -> str:
        name = self.type_name(attr_type)
        if attr_type.forward:
            inner = ClassUtils.find_nested(obj, attr_type.qname)
            outer_str = ".".join(map(self.class_name, inner.parent_names()))
            name = f'"{outer_str}.{name}"'
        elif attr_type.circular:
            name = f'"{name}"'

        if not choice:
            name = name.strip('"')

        return name

    def constant_value(self, attr: Attr) -> str:
        """Return the attr default value or type as constant value."""
        attr_type = attr.types[0]
        if attr_type.native:
            return f'"{attr.default}"'

        if attr_type.alias:
            return self.class_name(attr_type.alias)

        return self.type_name(attr_type)

    def default_imports(self, output: str) -> str:
        """Generate the default imports for the given package output."""
        imports = ["from __future__ import annotations"]
        for library, types in self.import_patterns.items():
            names = [
                name
                for name, searches in types.items()
                if any(search in output for search in searches)
            ]

            if len(names) == 1 and names[0] == "__module__":
                imports.append(f"import {library}")
            elif names:
                imports.append(f"from {library} import {', '.join(names)}")

        return "\n".join(collections.unique_sequence(imports))

    def _get_iterable_format(self) -> str:
        if self.generic_collections:
            return "Sequence[{}]"

        return "tuple[{}, ...]" if self.format.frozen else "list[{}]"

    @classmethod
    def build_import_patterns(cls) -> dict[str, dict]:
        """Build import search patterns."""
        type_patterns = cls.build_type_patterns
        return {
            "dataclasses": {"dataclass": ["@dataclass"], "field": [" = field("]},
            "decimal": {"Decimal": type_patterns("Decimal")},
            "enum": {"Enum": ["(Enum)"]},
            "typing": {
                "ForwardRef": [": ForwardRef("],
                "Any": type_patterns("Any"),
            },
            "collections.abc": {
                "Sequence": [": Sequence["],
                "Mapping": [": Mapping["],
            },
            "xml.etree.ElementTree": {"QName": type_patterns("QName")},
            "xsdata.models.datatype": {
                "XmlDate": type_patterns("XmlDate"),
                "XmlDateTime": type_patterns("XmlDateTime"),
                "XmlDuration": type_patterns("XmlDuration"),
                "XmlPeriod": type_patterns("XmlPeriod"),
                "XmlTime": type_patterns("XmlTime"),
            },
        }

    @classmethod
    def build_type_patterns(cls, x: str) -> tuple[str, ...]:
        """Return all possible type occurrences in the generated code."""
        return (
            f": {x} =",
            f"[{x}]",
            f"[{x},",
            f" {x},",
            f" {x}]",
            f" {x}(",
            f" | {x}",
            f"{x} |",
        )
