"""Provides some of the classes and logic for handling configuration data."""


import os
from abc import ABCMeta
from typing import Literal, Optional, Generic, TypeVar, Union, ClassVar, TypeAlias, get_args
from contextlib import AbstractContextManager

import arcpy
import attrs
import attrs.validators as v
import yaml

from .iterablenamespace import IterableNamespace, FrozenDict
from .misc import ArcFieldTypeKeyword, ArcFieldTypeStr, FIELD_TYPE_KEYWORDS

CodedValueDict: TypeAlias = dict[str, str]
DomainType: TypeAlias = Literal["CODED", "RANGE"]
DomainInfoNamespace: TypeAlias = IterableNamespace[Union[str, CodedValueDict]]
FieldInfoNamespace: TypeAlias = IterableNamespace[Union[str, int, DomainInfoNamespace, None]]
FieldPriority: TypeAlias = Literal["M", "C", "O", "T"]
FeatureClassInfoNamespace: TypeAlias = IterableNamespace[Union[str, list[str]]]


class YAMLIterableNamespaceMeta(ABCMeta, yaml.YAMLObjectMetaclass):
    """Empty class that serves to prevent a metaclass conflict in
    :class:`_NG911Config`."""
    pass


class WDManager(AbstractContextManager[None]):
    """Context manager that temporarily changes the working directory."""

    target_directory: Optional[str]
    original_directory: str

    def __init__(self, directory: Optional[str] = None):
        self.target_directory = directory
        self.original_directory = os.getcwd()

    def __enter__(self):
        super().__enter__()
        if self.target_directory:
            os.chdir(self.target_directory)

    def __exit__(self, __exc_type, __exc_value, __traceback):
        os.chdir(self.original_directory)


class ConfigDumper(yaml.Dumper):
    """YAML dumper that can generate ``config.yml``."""

    anchored_scalars: ClassVar[dict[Union[str, int, float, bool, None], str]] = {}
    """Registry for scalar values that should be assigned specifically-named
    anchors. The keys are the scalars and the values are the anchor names."""

    @property
    def serialized_scalar_values(self) -> list[Union[str, int, float, bool, None]]:
        """Returns a list of all scalar values that have been serialized (so
        far) by an instance."""
        return [n.value for n in self.serialized_nodes if isinstance(n, yaml.ScalarNode)]

    @classmethod
    def anchor_scalar(cls, value: Union[str, int, float, bool, None], alias: str):
        """Registers a scalar value that should be assigned an anchor named
        *alias* in the output YAML."""
        cls.anchored_scalars[value] = alias

    def generate_anchor(self, node):
        if isinstance(node, yaml.ScalarNode) and node.value in self.anchored_scalars:
            alias = self.anchored_scalars[node.value]
        elif node.tag == "!Domain":
            alias = {k.value: v.value for k, v in node.value}["name"]  # Name of the domain
            pass
        elif node.tag == "!Field":
            alias = {k.value: v.value for k, v in node.value}["role"]  # Role of the field
        else:
            alias = super().generate_anchor(node)
        if alias in self.anchors.values():
            raise ValueError(f"Alias '{alias}' already in use.")
        return alias

    def represent_data(self, data):
        if isinstance(data, IterableNamespace) and not hasattr(data, "yaml_tag"):
            return self.represent_dict(dict(data))
            # return self.yaml_representers[dict](self, dict(data))
        else:
            return super().represent_data(data)

    def serialize_node(self, node, parent, index):
        # Contains some code duplicated and/or adapted from serializer.py
        if isinstance(node, yaml.ScalarNode) \
                and node.value in self.anchored_scalars \
                and (node.value is None or isinstance(node.value, (str, int, float, bool))):
            alias = self.anchored_scalars[node.value]
            if node.value in self.serialized_scalar_values:
                self.emit(yaml.AliasEvent(alias))
            else:
                self.serialized_nodes[node] = True
                self.descend_resolver(parent, index)
                detected_tag = self.resolve(yaml.ScalarNode, node.value, (True, False))
                default_tag = self.resolve(yaml.ScalarNode, node.value, (False, True))
                implicit = (node.tag == detected_tag), (node.tag == default_tag)
                self.emit(yaml.ScalarEvent(alias, node.tag, implicit, node.value, style=node.style))
                self.ascend_resolver()
        else:
            return super().serialize_node(node, parent, index)


@attrs.frozen(repr=False, str=False)
class NG911Domain(yaml.YAMLObject):
    yaml_tag = "!Domain"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    name: str
    description: str
    type: DomainType
    entries: FrozenDict[str, str] = attrs.field(converter=FrozenDict)

    @property
    def as_namespace(self) -> DomainInfoNamespace:
        return IterableNamespace(
            name=self.name,
            description=self.description,
            type=self.type,
            entries=self.entries
        )

    @classmethod
    def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.Node):
        # Explicitly writing this method was necessary when changing from @dataclass to @attrs.frozen
        assert isinstance(node, yaml.MappingNode)
        mapping = loader.construct_mapping(node, deep=True)
        return cls(**mapping)

    def __repr__(self):
        num_entries = len(self.entries)
        entry_word = "Entry" if num_entries == 1 else "Entries"
        return f"<{__class__.__name__} '{self.name}' [{num_entries} {entry_word}]>"

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``n`` - Returns the :attr:`name` attribute
        - ``d`` - Returns the :attr:`description` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "n":
                return self.name
            case "d":
                return self.description
            case _:
                return super().__format__(format_spec)


@attrs.frozen
class NG911Field(yaml.YAMLObject):
    yaml_tag = "!Field"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    role: str = attrs.field(validator=v.instance_of(str))
    name: str = attrs.field(validator=v.instance_of(str))
    type: ArcFieldTypeStr = attrs.field(validator=v.in_(get_args(ArcFieldTypeStr)))
    priority: FieldPriority = attrs.field(validator=v.in_(get_args(FieldPriority)))
    length: int | None = attrs.field(default=None, validator=v.optional([v.instance_of(int), v.ge(1)]))
    domain: NG911Domain | None = attrs.field(default=None, validator=v.optional(v.instance_of(NG911Domain)))
    fill_value: str | int | float | None = None

    @classmethod
    def from_yaml(cls, loader: yaml.SafeLoader, node: yaml.Node):
        # Explicitly writing this method was necessary when changing from @dataclass to @attrs.frozen
        assert isinstance(node, yaml.MappingNode)
        mapping = loader.construct_mapping(node, deep=True)
        return cls(**mapping)

    @property
    def arcpy_field(self) -> arcpy.Field:
        field = arcpy.Field()
        field.name = self.name
        field.type = self.type
        if self.length:
            field.length = self.length
        if self.domain:
            field.domain = self.domain.name
        return field

    @property
    def type_keyword(self) -> ArcFieldTypeKeyword:
        """
        Returns the keyword used in field creation based on the field's
        :attr:`type` attribute.

        .. seealso::

           :data:`FIELD_TYPE_KEYWORDS`
        """
        return FIELD_TYPE_KEYWORDS[self.type]

    @property
    def value_table_row(self) -> list[str | int | ArcFieldTypeKeyword | None]:
        """Returns a list representing the field's specifications, ordered as
        ``[<name>, <type>, <alias>, <length>, <default_value>, <domain>]``.
        Intended for use as an element of a ``list`` of ``list``\ s passed to
        ``arcpy.management.AddFields``."""
        return [self.name, self.type_keyword, None, self.length, None, self.domain.name if self.domain else None]

    @property
    def as_namespace(self) -> FieldInfoNamespace:
        return IterableNamespace(
            name=self.name,
            type=self.type,
            length=self.length,
            domain=self.domain.as_namespace if self.domain else None
        )

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``r`` - Returns the :attr:`role` attribute
        - ``n`` - Returns the :attr:`name` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "r":
                return self.role
            case "n":
                return self.name
            case _:
                return super().__format__(format_spec)


@attrs.frozen(repr=False, str=False)
class NG911GeodatabaseInfo(yaml.YAMLObject):
    yaml_tag = "!GDBInfo"
    yaml_loader = yaml.SafeLoader
    yaml_dumper = ConfigDumper

    spatial_reference_factory_code_2d: int
    spatial_reference_factory_code_3d: int
    required_dataset_name: str
    optional_dataset_name: str


T_FieldNamespace = TypeVar("T_FieldNamespace", bound=IterableNamespace[NG911Field])


@attrs.frozen(repr=False, str=False)
class NG911FeatureClass(Generic[T_FieldNamespace]):
    role: str
    name: str
    geometry_type: Literal["POINT", "POLYLINE", "POLYGON"]
    dataset: str
    unique_id: NG911Field
    fields: T_FieldNamespace

    def __repr__(self):
        return f"<{__class__.__name__} '{self.role}'>"

    @property
    def field_value_table_rows(self) -> list[list[str | int | ArcFieldTypeKeyword | None]]:
        """
        Returns a ``list`` of ``list``\ s suitable for passing to
        ``arcpy.management.AddFields`` as the argument for
        ``field_description``.

        ..seealso::

           :attr:`NG911Field.value_table_row`
        """
        f: NG911Field
        return [f.value_table_row for f in self.fields.values()]

    @property
    def as_namespace(self) -> FeatureClassInfoNamespace:
        return IterableNamespace(
            name=self.name,
            geometry_type=self.geometry_type,
            fields=[*self.fields.keys()]
        )

    def __format__(self, format_spec: str) -> str:
        """
        The following custom format specifiers are implemented:

        - ``r`` - Returns the :attr:`role` attribute
        - ``n`` - Returns the :attr:`name` attribute
        """
        # noinspection PyUnreachableCode
        match format_spec:
            case "r":
                return self.role
            case "n":
                return self.name
            case _:
                return super().__format__(format_spec)


__all__ = ["NG911Domain", "NG911Field", "NG911FeatureClass", "NG911GeodatabaseInfo", "FeatureClassInfoNamespace", "FieldInfoNamespace", "DomainInfoNamespace", "CodedValueDict", "DomainType", "ConfigDumper", "WDManager", "YAMLIterableNamespaceMeta"]