# Copyright (C) 2021-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""
Bindings for the association of segment with administrative boundary
"""
from dataclasses import dataclass
from typing import Dict, Iterable, List, Mapping, Optional

from google.protobuf.message import Message
from here.content.base import ContentIndexer, ContentPartition, SingleLayerBinding
from here.content.content import Content as ParentContent
from here.content.content import hmc_schema_version
from here.content.hmc2 import AdminLocations, AdminPlaces
from here.content.hmc2.admin_locations import Location
from here.content.hmc2.base_attributes import (
    Attribute,
    BaseAttributesMultiLayerBinding,
    SegmentRef,
)
from here.platform.adapter import Adapter, DecodedMessage, Partition, Ref, make_ref

admin_categories = (
    "country",
    "region",
    "state",
    "county",
    "city",
    "district",
    "sub_district",
    "postal_code",
    "further_zone",
)


@dataclass
class SegmentAdminBoundaryAttributes:
    """Segment to Admin Boundary"""

    partition_id: Partition
    segments: List[SegmentRef]
    forward: bool
    backward: bool
    place_ref: Ref
    place_name: List[DecodedMessage]
    admin_category: str
    location: Location

    indexer = ContentIndexer(
        partition="partition_id",
        refs={
            "segment": lambda a: [s.ref for s in a.segments],  # type: ignore
            "place": "place_ref",  # type: ignore
        },
    )


class SegmentAdminBoundary(BaseAttributesMultiLayerBinding):
    """class to associate segment with administrative boundary"""

    _content: ParentContent

    def __init__(self, content: ParentContent, layer_id: str, adapter: Optional[Adapter] = None):
        """
        Instantiate a class Multilayer SegmentAdminBoundary binding.
        Assign content to Class attribute _content.

        :param content: Content object that references a catalog at a fixed version
        :param layer_id: ID of the only layer that contains the content
        :param adapter: the ``Adapter`` to transform data between different representations,
            ``None`` to use the adapter defined in ``content``.
        """
        super().__init__(content, layer_id, adapter)
        SegmentAdminBoundary._content = content

    @classmethod
    def schema_hrn(cls) -> List[str]:
        """
        :return: the HRN of schema this binding can parse and index
        """

        return [
            f"hrn:here:schema:::com.here.schema.rib:address-attributes_v2:{hmc_schema_version}"
        ]

    @classmethod
    def associated_layers(cls) -> Dict[str, SingleLayerBinding]:
        """
        Provide associated dependent/independent layers to the base layer.

        :return: dict of key as single layer and value as SingleLayerBinding Object.
        """
        return {
            "administrative_places": AdminPlaces(
                content=cls._content, layer_id="administrative-places"
            ),
            "administrative_locations": AdminLocations(
                content=cls._content, layer_id="administrative-locations"
            ),
        }

    @classmethod
    def object_types(cls) -> Mapping[str, type]:
        """
        :return: the types of attributes supported by this binding
        """
        return {"segment_admin_boundary": SegmentAdminBoundaryAttributes}

    @classmethod
    def _internal_object_types(cls) -> Mapping[str, type]:
        """
        :return: the types of attributes supported by this binding
        """
        return {"administrative_context_attribute": Attribute}

    @classmethod
    def parse_and_index(cls, partition_id: str, msg: Message) -> ContentPartition:
        """
        Parse and index decoded message.

        :param partition_id: identifier of the partition
        :param msg: decoded content from the layer, in the form of a Protobuf message
        :return: the content of a partition parsed and indexed in its own data structure
        """

        assert hasattr(msg, "administrative_context_attribute")

        parsed_and_indexed = super().parse_and_index(partition_id, msg)

        admin_context_attr = parsed_and_indexed.objects("administrative_context_attribute")

        segment_admin_boundary: list = cls.associate_segment_location(
            admin_context_attr, partition_id
        )

        return ContentPartition(cls, segment_admin_boundary=segment_admin_boundary)

    @classmethod
    def associate_segment_location(
        cls, admin_context_attr: Iterable[object], partition_id: str
    ) -> List[SegmentAdminBoundaryAttributes]:
        """
        Associate environmental_zone_condition with environment_zone.

        :param admin_context_attr: a dict of segment to administrative context.
        :param partition_id: partition id of the base layer.
        :return: list of SegmentAdminBoundaryAttributes object.
        """

        admin_places = cls.associated_layers()["administrative_places"]
        admin_locations = cls.associated_layers()["administrative_locations"]

        segment_admin_boundary: Dict[str, SegmentAdminBoundaryAttributes] = {}
        context_key = "administrative_context"
        place_key = "place_ref"
        partition_key = "partition_name"
        partition_id_key = "identifier"

        for admin_context in admin_context_attr:
            for cat, atts in admin_context.attribute[context_key].items():  # type: ignore
                if cat in admin_categories:
                    for att in atts:
                        if (
                            place_key in att
                            and partition_key in att[place_key]
                            and partition_id_key in att[place_key]
                        ):
                            place_pid = att[place_key][partition_key]
                            place_id = att[place_key][partition_id_key]
                            ap = admin_places.get(place_pid, "place")
                            if place_pid in ap and place_id in ap[place_pid]:
                                place = ap[place_pid][place_id]
                                p_key = f"{place_pid}:{place_id}"  # noqa: E231
                                if p_key in segment_admin_boundary:
                                    seg_admin_boundary = segment_admin_boundary[p_key]
                                    seg_admin_boundary.segments = list(
                                        set(
                                            seg_admin_boundary.segments
                                            + admin_context.segments  # type: ignore
                                        )
                                    )
                                    segment_admin_boundary[p_key] = seg_admin_boundary
                                    continue
                                if (
                                    hasattr(place, "location_ref")
                                    and hasattr(place.location_ref, "partition")
                                    and hasattr(place.location_ref, "identifier")
                                ):
                                    location_pid = place.location_ref.partition
                                    location_id = place.location_ref.identifier
                                    al = admin_locations.get(location_pid, "location")
                                    location = al[location_pid][location_id]
                                    segment_admin_boundary[p_key] = SegmentAdminBoundaryAttributes(
                                        partition_id=partition_id,
                                        place_ref=make_ref(place_pid, place_id),
                                        place_name=place.name,
                                        admin_category=cat,
                                        segments=admin_context.segments,  # type: ignore
                                        forward=admin_context.forward,  # type: ignore
                                        backward=admin_context.backward,  # type: ignore
                                        location=location,
                                    )

        return list(segment_admin_boundary.values())
