"""
SPDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

    Manage locations in Egeria.

"""

import asyncio
from typing import Optional

from pyegeria.core._server_client import ServerClient
from pyegeria.view.base_report_formats import get_report_spec_match
from pyegeria.view.base_report_formats import select_report_spec
from pyegeria.models import (SearchStringRequestBody, FilterRequestBody, GetRequestBody, NewElementRequestBody,
                             TemplateRequestBody, UpdateElementRequestBody,
                             NewRelationshipRequestBody, DeleteElementRequestBody, DeleteRelationshipRequestBody)
from pyegeria.view.output_formatter import populate_columns_from_properties, \
    _extract_referenceable_properties, get_required_relationships
from pyegeria.core.utils import dynamic_catch
from loguru import logger


class LocationArena(ServerClient):
    """
    Manage Locations in Egeria..

    This client provides asynchronous and synchronous helpers to create, update, search,
    and relate LocationArena elements and their subtypes (Campaign, StudyProject, Task, PersonalProject).

    References


    Parameters
    -----------
    view_server : str
        The name of the View Server to connect to.
    platform_url : str
        URL of the server platform to connect to.
    user_id : str
        Default user identity for calls (can be overridden per call).
    user_pwd : str, optional
        Password for the user_id. If a token is supplied, this may be None.

    Notes
    -----
    - Most high-level list/report methods accept an `output_format` and an optional `report_spec` and
      delegate rendering to `pyegeria.output_formatter.generate_output` along with shared helpers such as
      `populate_common_columns`.
    - Private extractor methods follow the convention: `_extract_<entity>_properties(element, columns_struct)` and
      must return the same `columns_struct` with per-column `value` fields populated.
    """

    def __init__(
            self,
            view_server: str = None,
            platform_url: str = None,
            user_id: str = None,
            user_pwd: Optional[str] = None,
            token: Optional[str] = None,
    ):
        ServerClient.__init__(self, view_server, platform_url, user_id, user_pwd, token)
        self.view_server = self.server_name
        self.platform_url = self.platform_url
        self.user_id = self.user_id
        self.user_pwd = self.user_pwd
        self.ref_location_command_base: str = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/location-arena"
        )
        self.url_marker = 'locations'

    @dynamic_catch
    async def _async_create_location(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """Create a new location. Async version.

        Parameters
        ----------
        body : dict | NewElementRequestBody, optional
            The properties for the location.

        Returns
        -------
        str
            The unique identifier of the newly created location.

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "NewElementRequestBody",
          "properties": {
            "class" : "LocationProperties",
            "qualifiedName": "LocationArena::Add name here",
            "displayName": "Add short name here",
            "description": "Add description here"
          }
        }
        ```
        """
        url = f"{self.ref_location_command_base}/locations"
        return await self._async_create_element_body_request(url, ["LocationProperties"], body)

    @dynamic_catch
    def create_location(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """Create a new location.

        Parameters
        ----------
        body : dict | NewElementRequestBody, optional
            The properties for the location.

        Returns
        -------
        str
            The unique identifier of the newly created location.

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "NewElementRequestBody",
          "properties": {
            "class" : "LocationProperties",
            "qualifiedName": "LocationArena::Add name here",
            "displayName": "Add short name here",
            "description": "Add description here"
          }
        }
        ```
        """
        return asyncio.get_event_loop().run_until_complete(self._async_create_location(body))

    #######

    @dynamic_catch
    async def _async_create_location_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str:
        """ Create a new metadata element to represent a LocationArena using an existing metadata element as a template.
            The template defines additional classifications and relationships that should be added to the new element.
            Async version.
    
        Parameters
        ----------
        body: dict
            A dict representing the details of the LocationArena to create.
    
        Returns
        -------
        str - the guid of the created LocationArena
    
        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        PyegeriaUnAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action
    
        Notes
        -----
        JSON Structure looks like:
    
        {
          "class": "TemplateRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "templateGUID": "template GUID",
          "replacementProperties": {
            "class": "ElementProperties",
            "propertyValueMap" : {
              "propertyName" : {
                "class": "PrimitiveTypePropertyValue",
                "typeName": "string",
                "primitiveTypeCategory" : "OM_PRIMITIVE_TYPE_STRING",
                "primitiveValue" : "value of property"
              }
            }
          },
          "placeholderPropertyValues" : {
            "placeholderProperty1Name" : "property1Value",
            "placeholderProperty2Name" : "property2Value"
          }
        }
    
        """
        url = f"{self.ref_location_command_base}/locations/from-template"

        return await self._async_create_element_from_template("POST", url, body)

    @dynamic_catch
    def create_location_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str:
        """ Create a new metadata element to represent a LocationArena using an existing metadata element as a template.
            The template defines additional classifications and relationships that should be added to the new element.

        Parameters
        ----------
        body: dict
            A dict representing the details of the LocationArena to create.

        Returns
        -------
        str - the guid of the created LocationArena

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        PyegeriaUnAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:

        {
          "class": "TemplateRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "templateGUID": "template GUID",
          "replacementProperties": {
            "class": "ElementProperties",
            "propertyValueMap" : {
              "propertyName" : {
                "class": "PrimitiveTypePropertyValue",
                "typeName": "string",
                "primitiveTypeCategory" : "OM_PRIMITIVE_TYPE_STRING",
                "primitiveValue" : "value of property"
              }
            }
          },
          "placeholderPropertyValues" : {
            "placeholderProperty1Name" : "property1Value",
            "placeholderProperty2Name" : "property2Value"
          }
        }

        """
        loop = asyncio.get_event_loop()
        resp = loop.run_until_complete(self._async_create_location_from_template(body))
        return resp

    @dynamic_catch
    async def _async_update_location(self, location_guid: str,
                                     body: dict | UpdateElementRequestBody) -> None:
        """Update an LocationArena. Async version.

        Parameters
        ----------
        location_guid: str
            The guid of the LocationArena to update.
        body: dict | UpdateElementRequestBody, optional
            A dict or UpdateElementRequestBody representing the updates to apply.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "LocationProperties",
            "displayName": "Updated Name"
          }
        }
        ```
        """
        url = f"{self.ref_location_command_base}/locations/{location_guid}/update"
        await self._async_update_element_body_request(url, ["LocationProperties"], body)

    @dynamic_catch
    def update_location(self, location_guid: str, body: dict | UpdateElementRequestBody) -> None:
        """Update an LocationArena.

        Parameters
        ----------
        location_guid: str
            The guid of the LocationArena to update.
        body: dict | UpdateElementRequestBody, optional
            A dict or UpdateElementRequestBody representing the updates to apply.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "LocationProperties",
            "displayName": "Updated Name"
          }
        }
        ```
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_update_location(location_guid, body))

    @dynamic_catch
    async def _async_link_peer_locations(self, location1_guid: str, location2_guid: str,
                                         body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """Link peer locations. Async version.

        Parameters
        ----------
        location1_guid : str
            The unique identifier of the first location.
        location2_guid : str
            The unique identifier of the second location.
        body : dict | NewRelationshipRequestBody, optional
            The properties for the relationship.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "NewRelationshipRequestBody",
          "properties": {
            "class": "AdjacentLocationProperties",
            "label": "add label here",
            "description": "add description here"
          }
        }
        ```
        """
        url = f"{self.ref_location_command_base}/locations/{location1_guid}/adjacent-locations/{location2_guid}/attach"
        await self._async_new_relationship_request(url, ["AdjacentLocationProperties"], body)
        logger.info(f"Linking location {location1_guid} to location {location2_guid}")

    @dynamic_catch
    def link_peer_locations(self, location1_guid: str, location2_guid: str,
                            body: Optional[dict | NewRelationshipRequestBody] = None):
        """Link peer locations.

        Parameters
        ----------
        location1_guid : str
            The unique identifier of the first location.
        location2_guid : str
            The unique identifier of the second location.
        body : dict | NewRelationshipRequestBody, optional
            The properties for the relationship.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "NewRelationshipRequestBody",
          "properties": {
            "class": "AdjacentLocationProperties",
            "label": "add label here",
            "description": "add description here"
          }
        }
        ```
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_peer_locations(location1_guid, location2_guid, body))

    @dynamic_catch
    async def _async_detach_peer_locations(self, location1_guid: str, location2_guid: str,
                                           body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """ Unlink peer locations. Async version.
    
        Parameters
        ----------
        location1_guid: str
            The unique identifier of the subscriber.
        location2_guid: str
            The unique identifier of the subscription.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.
    
        Returns
        -------
        Nothing
    
        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action
    
        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        url = (f"{self.command_root}/elements/{location1_guid}/locations/{location2_guid}/detach")

        await self._async_delete_element_request(url, body)
        logger.info(f"Unlink {location1_guid} from location {location2_guid}")

    def detach_peer_locations(self, location1_guid: str, location2_guid: str,
                              body: Optional[dict | DeleteRelationshipRequestBody] = None):
        """ Unlink peer locations.

        Parameters
        ----------
        location1_guid: str
            The unique identifier of the subscriber.
        location2_guid: str
            The unique identifier of the subscription.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_peer_locations(location1_guid, location2_guid, body))

    @dynamic_catch
    async def _async_link_nested_location(self, location_guid: str, nested_location_guid: str,
                                          body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """ AAttach a super location to a nested location.
            Async version.

        Parameters
        ----------
        location_guid: str
            The unique identifier of the location.
        nested_location_guid: str
            The identifier of the nested location.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "NestedLocationProperties",
            "label": "add label here",
            "description": "add description here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """

        url = f"{self.ref_location_command_base}/locations/{location_guid}/nested-locations/{nested_location_guid}/attach"
        await self._async_new_relationship_request(url, ["NestedLocationProperties"], body)
        logger.info(f"Linking element {location_guid} to nested location  {nested_location_guid}")

    @dynamic_catch
    def link_nested_location(self, location_guid: str, nested_location_guid: str,
                             body: Optional[dict | NewRelationshipRequestBody] = None):
        """ AAttach a super location to a nested location.

        Parameters
        ----------
        location_guid: str
            The unique identifier of the location.
        nested_location_guid: str
            The identifier of the nested location.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "NestedLocationProperties",
            "label": "add label here",
            "description": "add description here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_nested_location(location_guid, nested_location_guid, body))

    @dynamic_catch
    async def _async_detach_nested_location(self, location_guid: str, nested_location_guid: str,
                                            body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """ Detach a nested location from a location. Async version.

        Parameters
        ----------
        location_guid: str
            The unique identifier of the location.
        nested_location_guid: str
            The unique identifier of the nested location.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        url = (
            f"{self.ref_location_command_base}/locations{location_guid}/nested-locations/{nested_location_guid}/detach")

        await self._async_delete_element_request(url, body)
        logger.info(f"Detached location {location_guid} from nested location {nested_location_guid}")

    def detach_nested_location(self, location_guid: str, nested_location_guid: str,
                               body: Optional[dict | DeleteRelationshipRequestBody] = None):
        """ Detach a nested location from a location.

        Parameters
        ----------
        location_guid: str
            The unique identifier of the location.
        nested_location_guid: str
            The unique identifier of the nested location.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_nested_location(location_guid, nested_location_guid, body))

    @dynamic_catch
    async def _async_link_known_location(self, element_guid: str, location_guid: str,
                                         body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """ Attach an element to its location.
            Async version.

        Parameters
        ----------
        element_guid: str
            The unique identifier of the element.
        location_guid: str
            The identifier of the location.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "KnownLocationProperties",
            "label": "add label here",
            "description": "add description here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """

        url = f"{self.ref_location_command_base}elements/{element_guid}/known-locations/{location_guid}/attach"
        await self._async_new_relationship_request(url, ["KnownLocationProperties"], body)
        logger.info(f"Linking element {element_guid} to location {location_guid}")

    @dynamic_catch
    def link_known_location(self, element_guid: str, location_guid: str,
                            body: Optional[dict | NewRelationshipRequestBody] = None):
        """ Attach an element to its location.
            Async version.

        Parameters
        ----------
        element_guid: str
            The unique identifier of the element.
        location_guid: str
            The identifier of the location.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "KnownLocationProperties",
            "label": "add label here",
            "description": "add description here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_known_location(element_guid, location_guid, body))

    @dynamic_catch
    async def _async_detach_known_location(self, element_guid: str, location_guid: str,
                                           body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """ Detach an element from an known location. Async version.

        Parameters
        ----------
        element_guid: str
            The unique identifier of the element.
        location_guid: str
            The unique identifier of the location.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        url = (f"{self.command_root}/location_arena/elements/{element_guid}/known-locations/{location_guid}/detach")

        await self._async_delete_element_request(url, body)
        logger.info(f"Detached element {element_guid} from location {location_guid}")

    def detach_known_location(self, element_guid: str, location_guid: str,
                              body: Optional[dict | DeleteRelationshipRequestBody] = None):
        """ Detach an element from an known location. Async version.

        Parameters
        ----------
        element_guid: str
            The unique identifier of the element.
        location_guid: str
            The unique identifier of the location.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "deleteMethod": "LOOK_FOR_LINEAGE",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_known_location(element_guid, location_guid, body))

    @dynamic_catch
    async def _async_delete_location(self, location_guid: str,
                                     body: Optional[dict | DeleteElementRequestBody] = None,
                                     cascade: bool = False) -> None:
        """Delete a location. Async version.

        Parameters
        ----------
        location_guid : str
            The GUID of the location to delete.
        body : dict | DeleteElementRequestBody, optional
            Request body for deletion.
        cascade : bool, optional, default=False
            If true, performs a cascade delete.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "DeleteElementRequestBody",
          "cascadedDelete" : false
        }
        ```
        """
        url = f"{self.ref_location_command_base}/locations/{location_guid}/delete"

        await self._async_delete_element_request(url, body, cascade)
        logger.info(f"Deleted location {location_guid} with cascade {cascade}")

    @dynamic_catch
    def delete_location(self, location_guid: str, body: Optional[dict | DeleteElementRequestBody] = None,
                        cascade: bool = False) -> None:
        """Delete a location.

        Parameters
        ----------
        location_guid : str
            The GUID of the location to delete.
        body : dict | DeleteElementRequestBody, optional
            Request body for deletion.
        cascade : bool, optional, default=False
            If true, performs a cascade delete.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            If there are issues in communications, message format, or Egeria errors.

        Notes
        -----
        Sample JSON body:
        ```json
        {
          "class" : "DeleteElementRequestBody",
          "cascadedDelete" : false
        }
        ```
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_delete_location(location_guid, body, cascade))

    @dynamic_catch
    async def _async_find_locations(
        self,
        search_string: str = "*",
        body: Optional[dict | SearchStringRequestBody] = None,
        starts_with: bool = True,
        ends_with: bool = False,
        ignore_case: bool = False,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = "Referenceable",
        **kwargs
    ) -> list | str:
        """ Retrieve the list of location metadata elements that contain the search string. Async Version.

        Parameters
        ----------
        search_string: str
            Search string to match against - None or '*' indicate match against all locations.
        starts_with : bool, [default=True], optional
            Starts with the supplied string.
        ends_with : bool, [default=False], optional
            Ends with the supplied string
        ignore_case : bool, [default=False], optional
            Ignore case when searching
        anchor_domain: str, optional
            The anchor domain to search in.
        metadata_element_type: str, optional
            The type of metadata element to search for.
        metadata_element_subtypes: list[str], optional
            The subtypes of metadata element to search for.
        skip_relationships: list[str], optional
            The types of relationships to skip.
        include_only_relationships: list[str], optional
            The types of relationships to include.
        skip_classified_elements: list[str], optional
            The types of classified elements to skip.
        include_only_classified_elements: list[str], optional
            The types of classified elements to include.
        graph_query_depth: int, [default=3], optional
            The depth of the graph query.
        governance_zone_filter: list[str], optional
            The governance zones to search in.
        as_of_time: str, optional
            The time to search as of.
        effective_time: str, optional
            The effective time to search at.
        relationship_page_size: int, [default=0], optional
            The page size for relationships.
        limit_results_by_status: list[str], optional
            The statuses to limit results by.
        sequencing_order: str, optional
            The order to sequence results by.
        sequencing_property: str, optional
            The property to sequence results by.
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
        report_spec: str | dict , optional, default = "Referenceable"
            - The desired output columns/fields to include.
        start_from: int, [default=0], optional
            When multiple pages of results are available, the page number to start from.
        page_size: int, [default=100]
            The number of items to return in a single page.
        property_names: list[str], optional
            The names of properties to search for.
        body: dict | SearchStringRequestBody, optional, default = None
            - if provided, the search parameters in the body will supercede other attributes, such as "search_string"

        Returns
        -------
        List | str

        Output depends on the output format specified.

        Raises
        ------

        ValidationError
          If the client passes incorrect parameters on the request that don't conform to the data model.
        PyegeriaException
          Issues raised in communicating or server side processing.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        """
        url = f"{self.ref_location_command_base}/locations/by-search-string"
        
        # Merge explicit parameters with kwargs
        params = {
            'search_string': search_string,
            'body': body,
            'starts_with': starts_with,
            'ends_with': ends_with,
            'ignore_case': ignore_case,
            'start_from': start_from,
            'page_size': page_size,
            'output_format': output_format,
            'report_spec': report_spec
        }
        params.update(kwargs)
        
        # Filter out None values, but keep search_string even if None (it's required)
        params = {k: v for k, v in params.items() if v is not None or k == 'search_string'}
        
        response = await self._async_find_request(url, _type="Referenceable",
                                                  _gen_output=self._generate_location_output, **params)

        return response

    @dynamic_catch
    def find_locations(
        self,
        search_string: str = "*",
        body: Optional[dict | SearchStringRequestBody] = None,
        starts_with: bool = True,
        ends_with: bool = False,
        ignore_case: bool = False,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = "Referenceable",
        **kwargs
    ) -> list | str:
        """ Retrieve the list of location metadata elements that contain the search string.

        Parameters
        ----------
        search_string: str
            Search string to match against - None or '*' indicate match against all locations.
        starts_with : bool, [default=True], optional
            Starts with the supplied string.
        ends_with : bool, [default=False], optional
            Ends with the supplied string
        ignore_case : bool, [default=False], optional
            Ignore case when searching
        anchor_domain: str, optional
            The anchor domain to search in.
        metadata_element_type: str, optional
            The type of metadata element to search for.
        metadata_element_subtypes: list[str], optional
            The subtypes of metadata element to search for.
        skip_relationships: list[str], optional
            The types of relationships to skip.
        include_only_relationships: list[str], optional
            The types of relationships to include.
        skip_classified_elements: list[str], optional
            The types of classified elements to skip.
        include_only_classified_elements: list[str], optional
            The types of classified elements to include.
        graph_query_depth: int, [default=3], optional
            The depth of the graph query.
        governance_zone_filter: list[str], optional
            The governance zones to search in.
        as_of_time: str, optional
            The time to search as of.
        effective_time: str, optional
            The effective time to search at.
        relationship_page_size: int, [default=0], optional
            The page size for relationships.
        limit_results_by_status: list[str], optional
            The statuses to limit results by.
        sequencing_order: str, optional
            The order to sequence results by.
        sequencing_property: str, optional
            The property to sequence results by.
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
        report_spec: str | dict , optional, default = "Referenceable"
            - The desired output columns/fields to include.
        start_from: int, [default=0], optional
            When multiple pages of results are available, the page number to start from.
        page_size: int, [default=100]
            The number of items to return in a single page.
        property_names: list[str], optional
            The names of properties to search for.
        body: dict | SearchStringRequestBody, optional, default = None
            - if provided, the search parameters in the body will supercede other attributes, such as "search_string"

        Returns
        -------
        List | str

        Output depends on the output format specified.

        Raises
        ------

        ValidationError
          If the client passes incorrect parameters on the request that don't conform to the data model.
        PyegeriaException
          Issues raised in communicating or server side processing.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        """
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(
            self._async_find_locations(
                search_string=search_string,
                body=body,
                starts_with=starts_with,
                ends_with=ends_with,
                ignore_case=ignore_case,
                start_from=start_from,
                page_size=page_size,
                output_format=output_format,
                report_spec=report_spec,
                **kwargs
            )
        )

    @dynamic_catch
    async def _async_get_locations_by_name(self, filter_string: Optional[str] = None,
                                           classification_names: Optional[list[str]] = None,
                                           body: Optional[dict | FilterRequestBody] = None,
                                           start_from: int = 0, page_size: int = 0,
                                           output_format: str = 'JSON',
                                           report_spec: str | dict = "Locations") -> list | str:
        """ Returns the list of Locations with a particular name. Async version.

            Parameters
            ----------
            filter_string: str,
                name to use to find matching locations.
            classification_names: list[str], optional, default = None
                type of collection to filter by - e.g., DataDict, Folder, Root
            body: dict, optional, default = None
                Provides, a full request body. If specified, the body supercedes the name parameter.
            start_from: int, [default=0], optional
                        When multiple pages of results are available, the page number to start from.
            page_size: int, [default=None]
                The number of items to return in a single page. If not specified, the default will be taken from
                the class instance.
            output_format: str, default = "JSON"
                - one of "DICT", "MERMAID" or "JSON"
            report_spec: dict , optional, default = None
                The desired output columns/fields to include.

            Returns
            -------
            List | str

            A list of collections match matching the name. Returns a string if none found.

            Raises
            ------

            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action
        """
        url = f"{self.ref_location_command_base}/locations/by-name"
        response = await self._async_get_name_request(url, _type="Referenceable",
                                                      _gen_output=self._generate_location_output,
                                                      filter_string=filter_string,
                                                      classification_names=classification_names, start_from=start_from,
                                                      page_size=page_size, output_format=output_format,
                                                      report_spec=report_spec, body=body)

        return response

    def get_locations_by_name(self, filter_string: Optional[str] = None, classification_names: Optional[list[str]] = None,
                              body: Optional[dict | FilterRequestBody] = None,
                              start_from: int = 0, page_size: int = 0, output_format: str = 'JSON',
                              report_spec: str | dict = "Locations") -> list | str:
        """ Returns the list of Locations with a particular name.

            Parameters
            ----------
            filter_string: str,
                name to use to find matching locations.
            classification_names: list[str], optional, default = None
                type of collection to filter by - e.g., DataDict, Folder, Root
            body: dict, optional, default = None
                Provides, a full request body. If specified, the body supercedes the name parameter.
            start_from: int, [default=0], optional
                        When multiple pages of results are available, the page number to start from.
            page_size: int, [default=None]
                The number of items to return in a single page. If not specified, the default will be taken from
                the class instance.
            output_format: str, default = "JSON"
                - one of "DICT", "MERMAID" or "JSON"
            report_spec: dict , optional, default = None
                The desired output columns/fields to include.

            Returns
            -------
            List | str

            A list of collections match matching the name. Returns a string if none found.

            Raises
            ------

            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_locations_by_name(filter_string, classification_names, body, start_from, page_size,
                                              output_format, report_spec))

    @dynamic_catch
    async def _async_get_location_by_guid(self, location_guid: str, element_type: Optional[str] = None,
                                          body: Optional[dict | GetRequestBody] = None, output_format: str = 'JSON',
                                          report_spec: str | dict = "Locations") -> dict | str:
        """Return the properties of a specific location. Async version.

        Parameters
        ----------
        location_guid: str,
            unique identifier of the location to retrieve.
        element_type: str, default = None, optional
             type of element, etc.
        body: dict | GetRequestBody, optional, default = None
            full request body.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
         report_spec: str | dict, optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        dict | str

        A JSON dict representing the specified collection. Returns a string if none found.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        ----
        Body sample:
        {
          "class": "GetRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """

        url = f"{self.ref_location_command_base}/locations/{location_guid}/retrieve"
        type = element_type if element_type else "Referenceable"

        response = await self._async_get_guid_request(url, _type=type,
                                                      _gen_output=self._generate_location_output,
                                                      output_format=output_format, report_spec=report_spec,
                                                      body=body)

        return response

    @dynamic_catch
    def get_location_by_guid(self, location_guid: str, element_type: Optional[str] = None, body: Optional[dict | GetRequestBody] = None,
                             output_format: str = 'JSON', report_spec: str | dict = "Locations") -> dict | str:
        """Return the properties of a specific location. Async version.

        Parameters
        ----------
        location_guid: str,
            unique identifier of the location to retrieve.
        element_type: str, default = None, optional
             type of element, etc.
        body: dict | GetRequestBody, optional, default = None
            full request body.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
         report_spec: str | dict, optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        dict | str

        A JSON dict representing the specified collection. Returns a string if none found.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        ----
        Body sample:
        {
          "class": "FilterRequestBody",
          "filter": "Add name here",
          "startFrom": 0,
          "pageSize": 10,
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false,
          "limitResultsByStatus": ["ACTIVE"],
          "sequencingOrder": "PROPERTY_ASCENDING",
          "sequencingProperty": "qualifiedName"
        }
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_location_by_guid(location_guid, element_type, body, output_format, report_spec))

    @dynamic_catch
    def _extract_location_properties(self, element: dict, columns_struct: dict) -> dict:
        """
        Extract common properties from a location element and populate into the provided columns_struct.

        Args:
            element (dict): The location element
            columns_struct (dict): The columns structure to populate

        Returns:
            dict: columns_struct with column 'value' fields populated
        """
        # First, populate from element.properties using the utility
        col_data = populate_columns_from_properties(element, columns_struct)

        columns_list = col_data.get("formats", {}).get("attributes", [])

        # Populate header-derived values
        header_props = _extract_referenceable_properties(element)
        for column in columns_list:
            key = column.get('key')
            if key in header_props:
                column['value'] = header_props.get(key)
            elif isinstance(key, str) and key.lower() == 'guid':
                column['value'] = header_props.get('GUID')

        # Derived/computed fields
        # locationCategories are classifications
        classification_names = ""
        classifications = element.get('elementHeader', {}).get("locationCategories", [])
        for classification in classifications:
            classification_names += f"{classification['classificationName']}, "
        if classification_names:
            for column in columns_list:
                if column.get('key') == 'classifications':
                    column['value'] = classification_names[:-2]
                    break

        # Populate requested relationship-based columns generically from top-level keys
        col_data = get_required_relationships(element, col_data)

        # Subject area classification
        subject_area = element.get('elementHeader', {}).get("subjectArea", "") or ""
        subj_val = ""
        if isinstance(subject_area, dict):
            subj_val = subject_area.get("classificationProperties", {}).get("subjectAreaName", "")
        for column in columns_list:
            if column.get('key') == 'subject_area':
                column['value'] = subj_val
                break

        # Mermaid graph
        mermaid_val = element.get('mermaidGraph', "") or ""
        for column in columns_list:
            if column.get('key') == 'mermaid':
                column['value'] = mermaid_val
                break

        logger.trace(f"Extracted/Populated columns: {col_data}")

        return col_data

    @dynamic_catch
    def _generate_location_output(
        self,
        elements: dict | list[dict],
        filter_string: Optional[str] = None,
        element_type_name: Optional[str] = None,
        output_format: str = "DICT",
        report_spec: dict | str | None = None,
        **kwargs,
    ) -> str | list[dict]:
        """Generate output for locations using the centralized formatter."""
        return self._generate_formatted_output(
            elements=elements,
            query_string=filter_string,
            element_type_name=element_type_name or "Locations",
            output_format=output_format,
            report_spec=report_spec,
            extract_properties_func=self._extract_location_properties,
            **kwargs,
        )
