import asyncio
import re
import warnings
from functools import partial
from typing import Optional

from jinja2 import Template

from pynspd import AsyncNspd
from pynspd._async.api import retry_on_http_error
from pynspd.schemas import _autogen_features as cached_feature_configs
from pynspd.schemas.base_feature import BaseFeature
from pynspd.schemas.layer_configs import Card, CardField, LayerNode, LayersTree

CLIENT = AsyncNspd()
request = partial(retry_on_http_error(AsyncNspd.request), CLIENT)


TYPES_TEMPLATE = """# autogenerated
from typing import Literal


LayerTitle = Literal[
    {%- for layer in layers %}
    "{{ layer.title }}",
    {%- endfor %}
]
"""

SCHEMAS_TEMPLATE = """# autogenerated
from datetime import datetime
from typing import Union, Optional

from pynspd.schemas.base_feature import BaseFeature
from pynspd.schemas.geometries import Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon
from pynspd.schemas.layer_configs import LayerNode
from pynspd.schemas.properties import NspdProperties, OptionProperties


{% for category_id, (title_key, fields) in layers_fields.items() %}
class Options{{ category_id }}(OptionProperties): {% if fields|length == 0 %}...
    {% else %}
    {% if title_key %}title_key = "{{ title_key }}"{% endif %}
    {%- for field in fields %}
    {{ field.key_value }}: Optional[{{field.key_type}}] = None
    \"\"\"{{field.key_name}}{% if field.postfix != None %} ({{ field.postfix }}){% endif %}\"\"\"
    {%- endfor %}
    {% endif %}
{% endfor %}


{% for layer in layers %}
class Layer{{ layer.layer_id }}Feature(BaseFeature[
    Union[Multi{{ layer.geometry_type }}, {{ layer.geometry_type }}{%- if layer.geometry_type == 'Polygon' %}, Point{% endif -%}],
    NspdProperties[Options{{ layer.category_id }}]
]):
    \"\"\"{{ layer.title }}\"\"\"
    layer_meta = LayerNode.model_validate({{ layer.model_dump(by_alias=True) }})
{% endfor %}
"""

OVERLOAD_TEMPLATE = """{%- for layer in layers %}
    @overload
    @classmethod
    def by_title(cls, title: Literal["{{ layer.title }}"]) -> Type[auto.Layer{{ layer.layer_id }}Feature]: ...
{% endfor %}
    @overload
    @classmethod
    def by_title(cls, title: Any) -> NoReturn: ...
"""


def generate_files(layers: list[LayerNode], layers_fields: dict[int, Card]):
    output = Template(SCHEMAS_TEMPLATE).render(
        layers=layers, layers_fields=layers_fields
    )
    with open("src/pynspd/schemas/_autogen_features.py", "w", encoding="utf-8") as file:
        file.write(output)

    output = Template(TYPES_TEMPLATE).render(layers=layers)
    with open("src/pynspd/map_types/_autogen_layers.py", "w", encoding="utf-8") as file:
        file.write(output)

    output = Template(OVERLOAD_TEMPLATE).render(layers=layers)
    lines = []
    with open("src/pynspd/schemas/feature.py", "r", encoding="utf-8") as from_file:
        for line in from_file:
            lines.append(line)
    with open("src/pynspd/schemas/feature.py", "w", encoding="utf-8") as to_file:
        write_flag = True
        for line in lines:
            if "# END_AUTOGEN" in line:
                write_flag = True
            if write_flag:
                to_file.write(line)
            if "# START_AUTOGEN" in line:
                write_flag = False
                to_file.write(output)
                to_file.write("\n\n")


def get_cached_geometry_types() -> dict[int, str]:
    data = {}
    for i in dir(cached_feature_configs):
        if not re.match(r"Layer\d+Feature", i):
            continue
        cl: BaseFeature = getattr(cached_feature_configs, i)
        data[cl.layer_meta.layer_id] = LayerNode._geometry_type_serializer(
            cl.layer_meta.geometry_type
        )
    return data


async def get_layer_tree() -> LayersTree:
    r = await request(
        "get", "/api/geoportal/v1/layers-theme-tree", params={"themeId": 1}
    )
    data = r.json()
    # 01.04.2025 - исчезла geometryType, берем из предыдущих значений
    cached_geometry_types = get_cached_geometry_types()
    for layer in data["layers"]:
        if "geometryType" in layer:
            continue
        warnings.warn(
            "Not found geometry type in tree; cached used instead", stacklevel=2
        )
        layer["geometryType"] = cached_geometry_types.get(layer["layerId"], "POLYGON")
    tree = LayersTree.model_validate(data)
    return tree


async def get_category_card(
    category_id: int,
) -> tuple[int, tuple[Optional[str], list[CardField]]]:
    r = await request(
        "get", f"/api/geoportal/v1/geom-card-display-settings/{category_id}"
    )
    card = Card.model_validate(r.json())
    title_key = card.title.key_value

    fields = []
    for field in card.card:
        if field.key_value == "-":
            continue
        if field.key_value == "":
            continue
        if not field.key_value.isascii():
            continue
        if " " in field.key_value:
            continue
        fields.append(field)
    return category_id, (title_key, fields)


async def get_layers_fields(layers: list[LayerNode]) -> dict[int, list[CardField]]:
    layers_fields = {}
    categories = list(set([i.category_id for i in layers]))
    result = await asyncio.gather(*[get_category_card(i) for i in categories])
    layers_fields = dict(result)
    return layers_fields


async def main():
    tree = await get_layer_tree()
    layers = tree.layers
    layers_fields = await get_layers_fields(layers)
    generate_files(layers, layers_fields)


if __name__ == "__main__":
    asyncio.run(main())
