from typing import List, Dict, Optional, TYPE_CHECKING, Union, Any
from urllib.parse import urljoin
from datetime import datetime

from .base import AsyncBase
from .task import AsyncTask
from ..enums import FieldType, TableExportFormat
from ..exception import NotFoundError 
from ..utils import clean_data

if TYPE_CHECKING:
    from . import AsyncGeoboxClient
    from .user import AsyncUser
    from .file import AsyncFile
    from .vectorlayer import AsyncVectorLayer
    from .feature import AsyncFeature
    from .relationship import AsyncRelationship


class AsyncTableRow(AsyncBase):

    def __init__(self, 
        table: 'AsyncTable',
        data: Optional[Dict] = {},
    ):
        """
        Constructs all the necessary attributes for the AsyncTableRow object.

        Args:
            table (AsyncTable): The table that the row belongs to.
            data (Dict, optional): The data of the field.
        """
        super().__init__(api=table.api, data=data)
        self.table = table
        self.endpoint = urljoin(table.endpoint, f'rows/{self.id}/') if self.data.get('id') else None


    def __repr__(self) -> str:
        """
        Return a string representation of the AsyncTableRow.

        Returns:
            str: The string representation of the AsyncTableRow.
        """
        return f"AsyncTableRow(id={self.id}, table_name={self.table.data.get('name', 'None')})"


    def __getattr__(self, name: str) -> Any:
        """
        Get an attribute from the resource.

        Args:
            name (str): The name of the attribute
        """
        if name in self.data or (self.data.get('properties') and name in self.data['properties']):
            value = self.data.get(name) or self.data.get('properties')[name]
            if isinstance(value, str):
                parsed = self._parse_datetime(value)
                if isinstance(parsed, datetime):
                    return parsed
            return value

        raise AttributeError(f"{self.__class__.__name__} has no attribute {name}")


    @classmethod
    async def create_row(cls, table: 'AsyncTable', **kwargs) -> 'AsyncTableRow':
        """
        [async] Create a new row in the table.

        Each keyword argument represents a field value for the row, where:
            - The keyword is the field name
            - The value is the field value

        Args:
            table (AsyncTable): table instance

        Keyword Args: 
            **kwargs: Arbitrary field values matching the table schema.

        Returns:
            AsyncTableRow: created table row instance

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable, AsyncTableRow
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     row_data = {
            ...        'field1': 'value1'
            ...     }
            >>>     row = await table.create_row(row_data)
            or
            >>>     row = await AsyncTableRow.create_row(table, row_data)
        """
        endpoint = urljoin(table.endpoint, 'rows/')
        return await cls._create(table.api, endpoint, kwargs, factory_func=lambda api, item: AsyncTableRow(table, data=item))


    @classmethod
    async def get_row(cls, 
        table: 'AsyncTable', 
        row_id: int,
        user_id: Optional[int],
    ) -> 'AsyncTableRow':
        """
        [async] Get a row by its id

        Args:
            table (AsyncTable): the table instance
            row_id (int): the row id
            user_id (int, optional): specific user. privileges required.

        Returns:
            TanbleRow: the table row instance

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable, AsyncTableRow
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")

            >>>     row = await table.get_row(row_id=1)
            or
            >>>     row = await AsyncTableRow.get_row(table, row_id=1)
        """
        param = {
            'f': 'json',
            'user_id': user_id
        }
        endpoint = urljoin(table.endpoint, f'rows/')
        return await cls._get_detail(table.api, endpoint, uuid=row_id, params=param, factory_func=lambda api, item: AsyncTableRow(table, data=item))


    async def update(self, **kwargs) -> Dict:
        """
        [async] Update a row

        Keyword Args:
            fields to update

        Returns:
            Dict: updated row data

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     row = await table.get_row(row_id=1)
            >>>     await row.update(field1='new_value')
        """
        await super()._update(self.endpoint, kwargs, clean=False)
        return self.data


    async def delete(self) -> None:
        """
        [async] Delete a row

        Returns: 
            None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     row = await table.get_row(row_id=1)
            >>>     await row.delete()
        """
        await super()._delete(self.endpoint)


    async def _get_other_side_of_relationship(
        self, 
        relationship: 'AsyncRelationship',
    ) -> Union['AsyncTable', 'AsyncVectorLayer']:
        """
        [async] Determine which side of a relationship this table is on and return the opposite side.
        
        Used internally to navigate bidirectional relationships.
        
        Args:
            relationship (AsyncRelationship): The relationship to examine.
            
        Returns:
            AsyncTable | AsyncVectorLayer: The endpoint (table or layer) on the opposite side
            of the relationship from this table.
            
        Raises:
            ValueError: If this table is not part of the given relationship.
            
        Note:
            This method assumes the table is either the source or target,
            not the relation table in Many-to-Many relationships.
        """
        if relationship.source_id == self.table.id:
            return await relationship.get_target()

        if relationship.target_id == self.table.id:
            return await relationship.get_source()

        raise ValueError("Relationship does not involve this table.")


    async def _fetch_related_from_target(
        self, 
        target: Union['AsyncTable', 'AsyncVectorLayer'], 
        relationship_uuid: str,
    ) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
        """
        [async] Fetch related rows/features from a relationship target.
        
        Internal helper that dispatches to the appropriate API method
        based on target type.
        
        Args:
            target (AsyncTable | AsyncVectorLayer): The target endpoint (Table or VectorLayer) to query.
            relationship_uuid (str): UUID of the relationship to traverse.
            
        Raises:
            TypeError: If target is not a Table or VectorLayer.

        Returns:
            List[AsyncTableRow] | List[AsyncFeature]: Related rows or features.
        """
        from .table import AsyncTable
        from .vectorlayer import AsyncVectorLayer

        if isinstance(target, AsyncTable):
            return await target.get_rows(
                relationship_uuid=relationship_uuid,
                related_record_id=self.id,
            )

        if isinstance(target, AsyncVectorLayer):
            return await target.get_features(
                relationship_uuid=relationship_uuid,
                related_record_id=self.id,
            )

        raise TypeError(f"Unsupported target type: {type(target)}")


    async def get_related_records(self,
        relationship_uuid: str,
    ) -> Union[List['AsyncTableRow'], List['AsyncFeature']]:
        """
        [async] Get the related records on the *other side* of the relationship that are linked to this row

        Args:
            relationship_uuid (str): The uuid of relationship

        Returns:
            List[AsyncTableRow] | List[AsyncFeature]: a list of the related records

        Raises:
            ValueError:
                If the given relationship does not involve the current table
                (i.e., this row is neither the source nor the target of the relationship).

            TypeError:
                If the relationship target type is not supported for fetching
                related records.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     row = await table.get_row(row_id=1)
            >>>     related_records = await row.get_related_records(relationship_uuid="12345678-1234-5678-1234-567812345678")
        """
        relationship = await self.api.get_relationship(relationship_uuid)

        other_side = await self._get_other_side_of_relationship(relationship)

        return await self._fetch_related_from_target(
            target=other_side,
            relationship_uuid=relationship_uuid,
        )


    async def associate_with(
        self,
        relationship_uuid: str,
        *,
        target_ids: Optional[List[int]] = None,
        q: Optional[str] = None,
    ) -> Dict:
        """
        [async] Create relationships between the source record and target records

        Args:
            relationship_uuid (str): the relationship uuid
            target_ids (List[int], optional): a list of target record ids to be associated with the current record
            q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current record

        Returns:
            Dict: the record association result

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     row = await table.get_row(row_id=1)
            >>>     await row.associate_with(
            ...         relationship_uuid="12345678-1234-5678-1234-567812345678",
            ...         target_ids=[1, 2, 3],
            ...     )
        """
        relationship = await self.api.get_relationship(uuid=relationship_uuid)
        return await relationship.associate_records(
            source_id=self.id,
            target_ids=target_ids,
            q=q,
        )


    async def disassociate_with(
        self,
        relationship_uuid: str,
        *,
        target_ids: Optional[List[int]] = None,
        q: Optional[str] = None,
    ) -> Dict:
        """
        [async] Remove relationships between the source record and target records

        Args:
            relationship_uuid (str): the relationship uuid
            target_ids (List[int], optional): a list of target record ids to be disassociated with the current record
            q (str, optional): query filter on target layer or table to select which target features or rows that are going to be related to the current 
            
        Returns:
            Dict: the record disassociation result

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     row = await table.get_row(row_id=1)
            >>>     await row.disassociate_with(
            ...         relationship_uuid="12345678-1234-5678-1234-567812345678",
            ...         target_ids=[1, 2, 3],
            ...     )
        """
        relationship = await self.api.get_relationship(uuid=relationship_uuid)
        return await relationship.disassociate_records(
            source_id=self.id,
            target_ids=target_ids,
            q=q,
        )


class AsyncTableField(AsyncBase):

    def __init__(self, 
        table: 'AsyncTable',
        data_type: 'FieldType',
        field_id: int = None,
        data: Optional[Dict] = {},
    ):
        """
        Constructs all the necessary attributes for the Field object.

        Args:
            table (AsyncTable): The table that the field belongs to.
            data_type (FieldType): type of the field
            field_id (int): the id of the field
            data (Dict, optional): The data of the field.
        """
        super().__init__(api=table.api, data=data)
        self.table = table
        self.field_id = field_id
        if not isinstance(data_type, FieldType):
            raise ValueError("data_type must be a FieldType instance")
        self.data_type = data_type
        self.endpoint = urljoin(table.endpoint, f'fields/{self.id}/') if self.data.get('id') else None


    def __repr__(self) -> str:
        """
        Return a string representation of the field.

        Returns:
            str: The string representation of the field.
        """
        return f"AsyncTableField(id={self.id}, name={self.name}, data_type={self.data_type})"

        
    def __getattr__(self, name: str) -> Any:
        """
        Get an attribute from the resource.

        Args:
            name (str): The name of the attribute
        """
        if name == 'datatype':
            return FieldType(self.data['datatype'])
        return super().__getattr__(name)


    @property
    def domain(self) -> Dict:
        """
        Domain property

        Returns:
            Dict: domain data
        """
        return self.data.get('domain')


    @domain.setter
    def domain(self, value: Dict) -> None:
        """
        Domain property setter

        Returns:
            None
        """
        self.data['domain'] = value
        

    @classmethod
    async def create_field(cls, 
        table: 'AsyncTable', 
        name: str, 
        data_type: 'FieldType', 
        data: Dict = {},
    ) -> 'AsyncTableField':
        """
        [async] Create a new field

        Args:
            table (AsyncTable): field's table
            name (str): name of the field
            data_type (FieldType): type of the field
            data (Dict, optional): the data of the field

        Returns:
            AsyncTableField: the created field object

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable, AsyncTableField
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")

            >>>     field = await table.create_field(name='test', data_type=FieldType.Integer)
            or
            >>>     field = await AsyncTableField.create_field(client, table=table, name='test', data_type=FieldType.Integer)
        """
        data.update({
            "name": name,
            "datatype": data_type.value
        })
        endpoint = urljoin(table.endpoint, 'fields/')
        return await super()._create(table.api, endpoint, data, factory_func=lambda api, item: AsyncTableField(table, data_type, item['id'], item))


    async def delete(self) -> None:
        """
        [async] Delete the field.

        Returns:
            None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.field import AsyncTableField
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     field = await table.get_field(field_id=1)
            >>>     await field.delete()
        """
        await super()._delete(self.endpoint)
        self.field_id = None


    async def update(self, **kwargs) -> Dict:
        """
        [async] Update the field.

        Keyword Args:
            name (str): The name of the field.
            display_name (str): The display name of the field.
            description (str): The description of the field.
            domain (Dict): the domain of the field
            hyperlink (bool): the hyperlink field.

        Returns:
            Dict: The updated data.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTableField
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     field = await table.get_field(field_id=1)
            >>>     await field.update(name="my_field", display_name="My Field", description="My Field Description")
        """       
        data = {
            "name": kwargs.get('name'),
            "display_name": kwargs.get('display_name'),
            "description": kwargs.get('description'),
            "domain": kwargs.get('domain'),
            "hyperlink": kwargs.get('hyperlink')
        }
        return await super()._update(self.endpoint, data)


    async def update_domain(self, 
        range_domain: Dict = None, 
        list_domain: Dict = None,
    ) -> Dict:
        """
        [async] Update field domian values

        Args:
            range_domain (Dict): a dictionary with min and max keys.
            list_domain (Dict): a dictionary containing the domain codes and values.

        Returns:
            Dict: the updated field domain

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     field = await client.get_table(uuid="12345678-1234-5678-1234-567812345678").get_fields()[0]
            >>>     range_d = {'min': 1, 'max': 10}
            >>>     await field.update_domain(range_domain = range_d)
            or 
            >>>     list_d = {'1': 'value1', '2': 'value2'}
            >>>     await field.update_domain(list_domain=list_d)
        """
        if not self.domain:
            self.domain = {'min': None, 'max': None, 'items': {}}

        if range_domain:
            self.domain['min'] = range_domain['min']
            self.domain['max'] = range_domain['max']

        if list_domain:
            self.domain['items'] = {**self.domain['items'], **list_domain}

        await self.update(domain=self.domain)
        return self.domain


    async def add_index(self) -> Dict:
        """
        [async] Add an index to a field for better query performance

        Returns:
            Dict: updated field data

        Examples:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     field = await client.get_table(uuid="12345678-1234-5678-1234-567812345678").get_fields()[0]
            >>>     field.add_index()
        """
        endpoint = f"{self.endpoint}addIndex/"
        data = await self.api.post(
            endpoint=endpoint,
        )
        self.data = data
        return data


    async def remove_index(self) -> Dict:
        """
        [async] Remove an index from a field

        Returns:
            Dict: updated field data

        Examples:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     field = await client.get_table(uuid="12345678-1234-5678-1234-567812345678").get_fields()[0]
            >>>     field.remove_index()
        """
        endpoint = f"{self.endpoint}removeIndex/"
        data = await self.api.post(
            endpoint=endpoint,
        )
        self.data = data
        return data


class AsyncTable(AsyncBase):

    BASE_ENDPOINT = 'tables/'

    def __init__(self, 
        api: 'AsyncGeoboxClient', 
        uuid: str,
        data: Optional[Dict] = {}):
        """
        Initialize a table instance.

        Args:
            api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
            uuid (str): The unique identifier for the table.
            data (Dict): The response data of the table.
        """
        super().__init__(api, uuid=uuid, data=data)


    @classmethod
    async def get_tables(cls, api: 'AsyncGeoboxClient', **kwargs) -> Union[List['AsyncTable'], int]:
        """
        [async] Get a list of tables with optional filtering and pagination.

        Args:
            api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.

        Keyword Args:
            include_settings (bool): Whether to include table settings. default: False
            temporary (bool): Whether to return temporary tables. default: False
            q (str): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
            search (str): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value. NOTE: if q param is defined this param will be ignored
            search_fields (str): comma separated list of fields for searching
            order_by (str): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
            return_count (bool): Whether to return total count. default: False.
            skip (int): Number of items to skip. default: 0
            limit (int): Number of items to return. default: 10
            user_id (int): Specific user. privileges required
            shared (bool): Whether to return shared tables. default: False

        Returns:
            List[AsyncTable] | int: A list of table instances or the total number of tables.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     tables = await client.get_tables(q="name LIKE '%My table%'")
            or
            >>>     tables = await AsyncTable.get_tables(client, q="name LIKE '%My table%'")
        """
        params = {
           'f': 'json',
           'include_settings': kwargs.get('include_settings', False),
           'temporary': kwargs.get('temporary'),
           'q': kwargs.get('q'),
           'search': kwargs.get('search'),
           'search_fields': kwargs.get('search_fields'),
           'order_by': kwargs.get('order_by'),
           'return_count': kwargs.get('return_count', False),
           'skip': kwargs.get('skip', 0),
           'limit': kwargs.get('limit', 10),
           'user_id': kwargs.get('user_id'),
           'shared': kwargs.get('shared', False)
        }
        return await super()._get_list(api, cls.BASE_ENDPOINT, params, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))
    

    @classmethod
    async def create_table(cls, 
        api: 'AsyncGeoboxClient', 
        name: str, 
        display_name: Optional[str] = None, 
        description: Optional[str] = None, 
        temporary: bool = False,
        fields: Optional[List[Dict]] = None,
    ) -> 'AsyncTable':
        """
        [async] Create a new table.

        Args:
            api (AsyncGeoboxClient): The AsyncGeoboxClient instance for making requests.
            name (str): The name of the AsyncTable.
            display_name (str, optional): The display name of the table.
            description (str, optional): The description of the table.
            temporary (bool, optional): Whether to create a temporary tables. default: False
            fields (List[Dict], optional): raw table fields. you can use create_field method for simpler and safer field addition. required dictionary keys: name, datatype

        Returns:
            AsyncTable: The newly created table instance.

        Raises:
            ValidationError: If the table data is invalid.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.create_table(name="my_table")
            or
            >>>     table = await AsyncTable.create_table(client, name="my_table")            
        """
        data = {
            "name": name,
            "display_name": display_name,
            "description": description,
            "temporary": temporary,
            "fields": fields,
        }
        return await super()._create(api, cls.BASE_ENDPOINT, data, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))

    
    @classmethod
    async def get_table(cls, api: 'AsyncGeoboxClient', uuid: str, user_id: int = None) -> 'AsyncTable':
        """
        [async] Get a table by UUID.

        Args:
            api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
            uuid (str): The UUID of the table to get.
            user_id (int): Specific user. privileges required.

        Returns:
            AsyncTable: The AsyncTable object.

        Raises:
            NotFoundError: If the table with the specified UUID is not found.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
        """
        params = {
            'f': 'json',
            'user_id': user_id,
        }
        return await super()._get_detail(api, cls.BASE_ENDPOINT, uuid, params, factory_func=lambda api, item: AsyncTable(api, item['uuid'], item))


    @classmethod
    async def get_table_by_name(cls, api: 'AsyncGeoboxClient', name: str, user_id: int = None) -> Union['AsyncTable', None]:
        """
        [async] Get a table by name

        Args:
            api (AsyncGeoboxClient): The GeoboxClient instance for making requests.
            name (str): the name of the table to get
            user_id (int, optional): specific user. privileges required.

        Returns:
            AsyncTable | None: returns the table if a table matches the given name, else None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table_by_name(name='test')
            or
            >>>     table = await AsyncTable.get_table_by_name(client, name='test')
        """
        tables = await cls.get_tables(api, q=f"name = '{name}'", user_id=user_id)
        if tables and tables[0].name == name:
            return tables[0]
        else:
            return None


    async def update(self, **kwargs) -> Dict:
        """
        [async] Update the table.

        Keyword Args:
            name (str): The name of the table.
            display_name (str): The display name of the table.
            description (str): The description of the table.

        Returns:
            Dict: The updated table data.

        Raises:
            ValidationError: If the table data is invalid.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     await table.update(display_name="New Display Name")
        """
        data = {
            "name": kwargs.get('name'),
            "display_name": kwargs.get('display_name'),   
            "description": kwargs.get('description'),
        }
        return await super()._update(self.endpoint, data)
    

    async def delete(self) -> None:
        """
        [async] Delete the AsyncTable.

        Returns:
            None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     await table.delete()
        """
        await super()._delete(self.endpoint)


    @property
    async def settings(self) -> Dict:
        """
        [async] Get the table's settings.
        
        Returns:
            Dict: The table settings.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     setting = await table.settings
        """
        return await super()._get_settings(endpoint=self.endpoint)
    

    async def update_settings(self, settings: Dict) -> Dict:
        """
        [async] Update the settings

        settings (Dict): settings dictionary

        Returns:
            Dict: updated settings

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> async with AsyncGeoboxClient() as client:
            >>>     table1 = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     table2 = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            >>>     await table1.update_settings(table2.settings)
        """
        return await super()._set_settings(self.endpoint, settings)


    async def get_fields(self) -> List['AsyncTableField']:
        """
        [async] Get all fields of the table.
        
        Returns:
            List[AsyncTableField]: A list of Field instances representing the table's fields.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     fields = await table.get_fields()
        """
        endpoint = urljoin(self.endpoint, 'fields/')
        return await super()._get_list(
            api=self.api,
            endpoint=endpoint,
            factory_func=lambda api, item: AsyncTableField(table=self, data_type=FieldType(item['datatype']), field_id=item['id'], data=item),
        )


    async def get_field(self, field_id: int) -> 'AsyncTableField':
        """
        [async] Get a specific field by ID.
        
        Args:
            field_id (int, optional): The ID of the field to retrieve.

        Returns:
            AsyncTableField: The requested field instance.
            
        Raises:
            NotFoundError: If the field with the specified ID is not found. 

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     field = await table.get_field(field_id=1)
        """        
        field = next((f for f in await self.get_fields() if f.id == field_id), None)
        if not field:
            raise NotFoundError(f'Field with ID {field_id} not found in table {self.name}')
            
        return field


    async def get_field_by_name(self, name: str) -> 'AsyncTableField':
        """
        [async] Get a specific field by name.
        
        Args:
            name (str): The name of the field to retrieve.

        Returns:
            AsyncTableField: The requested field instance.
            
        Raises:
            NotFoundError: If the field with the specified name is not found. 

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     field = await table.get_field_by_name(name='test')
        """        
        field = next((f for f in await self.get_fields() if f.name == name), None)
        if not field:
            raise NotFoundError(f"Field with name '{name}' not found in table {self.name}")
            
        return field


    async def add_field(self, name: str, data_type: 'FieldType', data: Dict = {}) -> 'AsyncTableField':
        """
        [async] Add a new field to the table.
        
        Args:
            name (str): The name of the new field.
            data_type (FieldType): The data type of the new field.
            data (Dict, optional): Additional field properties (display_name, description, etc.).
            
        Returns:
            AsyncTableField: The newly created field instance.
            
        Raises:
            ValidationError: If the field data is invalid.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     field = await table.add_field(name="new_field", data_type=FieldType.String)
        """
        return await AsyncTableField.create_field(self, name=name, data_type=data_type, data=data)


    async def calculate_field(self, 
        target_field: str, 
        expression: str, 
        q: Optional[str] = None, 
        search: Optional[str] = None, 
        search_fields: Optional[str] = None, 
        row_ids: Optional[str] = None, 
        run_async: bool = True, 
        user_id: Optional[int] = None,
    ) -> Union['AsyncTask', Dict]:
        """
        [async] Calculate values for a field based on an expression.
        
        Args:
            target_field (str): The field to calculate values for.
            expression (str): The expression to use for calculation.
            q (str, optional): Query to filter features. default: None.
            search (str, optional): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value
            search_fields (str, optional): comma separated list of fields for searching
            row_ids (str, optional): List of specific row IDs to include. default: None
            run_async (bool, optional): Whether to run the calculation asynchronously. default: True.
            user_id (int, optional): Specific user. privileges required.
            
        Returns:
            Task | Dict: The task instance of the calculation operation or the api response if the run_async=False.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     task = await table.calculate_field(target_field="target_field", 
            ...         expression="expression", 
            ...         q="name like 'my_layer'", 
            ...         row_ids=[1, 2, 3], 
            ...         run_async=True)
        """
        data = clean_data({
            "target_field": target_field,
            "expression": expression,
            "q": q,
            "search": search,
            "search_fields": search_fields,
            "row_ids": row_ids,
            "run_async": run_async,
            "user_id": user_id
        })
        
        endpoint = urljoin(self.endpoint, 'calculateField/')
        response = await self.api.post(endpoint, data, is_json=False)
        if run_async:
            task = await AsyncTask.get_task(self.api, response.get('task_id'))
            return task

        return response


    async def get_rows(self, **kwargs) -> List['AsyncTableRow']:
        """
        [async] Query rows of a table

        Keyword Args:
            relationship_uuid (str): The uuid of relationship
            related_record_id (int): This is the id of the feature/row that these rows are related to. This id belongs to the related layer/table not this table
            q (str): Advanced filtering expression, e.g., 'status = "active" and age > 20'
            search (str): Search term for keyword-based searching among fields/columns
            search_fields (str): Comma separated column names to search in
            row_ids (str): Comma separated list of row ids to filter for
            fields (str): Comma separated column names to include in results, or [ALL]
            exclude (str): Comma separated column names to exclude from result
            order_by (str): Comma separated list for ordering, e.g., 'name A, id D'
            skip (int): Number of records to skip for pagination. default: 0
            limit (int): Maximum number of records to return. default: 100
            return_count (bool): If true, returns only the count of matching rows
            user_id (int): Specific user. privileges required

        Returns:
            List[AsyncTableRow]: list of table rows objects

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")

            >>>     rows = await table.get_rows()
        """
        params = {
            'f': 'json',
            'relationship_uuid': kwargs.get('relationship_uuid'),
            'related_record_id': kwargs.get('related_record_id'),
            'q': kwargs.get('q'),
            'search': kwargs.get('search'),
            'search_fields': kwargs.get('search_fields'),
            'row_ids': kwargs.get('row_ids'),
            'fields': kwargs.get('fields'),
            'exclude': kwargs.get('exclude'),
            'order_by': kwargs.get('order_by'),
            'skip': kwargs.get('skip', 0),
            'limit': kwargs.get('limit', 100),
            'return_count': kwargs.get('return_count', False),
            'user_id': kwargs.get('user_id'),
        }

        endpoint = f'{self.endpoint}rows/'

        return await super()._get_list(
            api=self.api,
            endpoint=endpoint,
            params=params,
            factory_func=lambda api, item: AsyncTableRow(self, item),
        )


    async def get_row(self, 
        row_id: int, 
        user_id: Optional[int] = None,
    ) -> 'AsyncTableRow':
        """
        [async] Get a row by its id

        Args:
            row_id (int): the row id
            user_id (int, optional): specific user. privileges required.

        Returns:
            TanbleRow: the table row instance

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")

            >>>     row = await table.get_row(row_id=1)
        """
        return await AsyncTableRow.get_row(self, row_id, user_id)


    async def create_row(self, **kwargs) -> 'AsyncTableRow':
        """
        [async] Create a new row in the table.

        Each keyword argument represents a field value for the row, where:
            - The keyword is the field name
            - The value is the field value

        Keyword Args: 
            **kwargs: Arbitrary field values matching the table schema.

        Returns:
            AsyncTableRow: created table row instance

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")

            >>>     row = await table.create_row(
            ...         field1=value1
            ...     )
        """
        return await AsyncTableRow.create_row(self, **kwargs)


    async def import_rows(self, 
        file: 'AsyncFile',
        *,
        file_encoding: str = "utf-8",
        input_dataset: Optional[str] = None,
        delimiter: str = ',',
        has_header: bool = True,
        report_errors: bool = False,
        bulk_insert: bool = True,
    ) -> 'AsyncTask':
        """
        Import rows from a CSV file into a table
        
        Args:
            file (AsyncFile): file object to import.
            file_encoding (str, optional): Character encoding of the input file. default: utf-8
            input_dataset (str, optional): Name of the dataset in the input file.
            delimiter (str, optional): the delimiter of the dataset. default: ,
            has_header (bool, optional): Whether the file has header or not. default: True
            report_errors (bool, optional): Whether to report import errors. default: False
            bulk_insert (bool, optional): 
            
        Returns:
            AsyncTask: The task instance of the import operation.
            
        Raises:
            ValidationError: If the import parameters are invalid.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     file = await client.get_file(uuid="12345678-1234-5678-1234-567812345678")
            >>>     task = await table.import_rows(
            ...         file=file,
            ...     )
        """
        data = clean_data({
            "file_uuid": file.uuid,
            "file_encoding": file_encoding,
            "input_dataset": file.name if not input_dataset else input_dataset,
            "delimiter": delimiter,
            "has_header": has_header,
            "report_errors": report_errors,
            "bulk_insert": bulk_insert,
        })

        endpoint = urljoin(self.endpoint, 'import-csv/')
        response = await self.api.post(endpoint, data, is_json=False)
        task = await AsyncTask.get_task(self.api, response.get('task_id'))
        return task


    async def export_rows(self,
        out_filename: str,
        *,
        out_format: 'TableExportFormat' = TableExportFormat.CSV, 
        q: Optional[str] = None,
        search: Optional[str] = None,
        search_fields: Optional[str] = None,
        row_ids: Optional[str] = None,
        fields: Optional[str] = None,
        exclude: Optional[str] = None,
        order_by: Optional[str] = None,
        zipped: bool = False,
        run_async: bool = True,
    ) -> Union['AsyncTask', str]:
        """
        [async] Export rows of a table to a file
        
        Args:
            out_filename (str): Name of the output file without the format (.csv) 
            out_format (TableExportFormat, optional): Format of the output file
            q (str, optional): query filter based on OGC CQL standard. e.g. "field1 LIKE '%GIS%' AND created_at > '2021-01-01'"
            search (str, optional): search term for keyword-based searching among search_fields or all textual fields if search_fields does not have value
            search_fields (str, optional): comma separated list of fields for searching
            row_ids (str, optional): List of specific row IDs to include
            fields (str, optional): List of specific field names to include
            exclude (str, optional): List of specific field names to exclude
            order_by (str, optional): comma separated list of fields for sorting results [field1 A|D, field2 A|D, …]. e.g. name A, type D. NOTE: "A" denotes ascending order and "D" denotes descending order.
            zipped (str, optional): Whether to compress the output file
            run_async (bool, optional): Whether to run the export asynchronously. default: True
            
        Returns:
            AsyncTask | Dict: The task instance of the export operation (run_async=True) or the export result (run_async=False)
            
        Raises:
            ValidationError: If the export parameters are invalid.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(api=client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     file = await client.get_file(uuid="12345678-1234-5678-1234-567812345678")
            >>>     task = await table.export_rows(
            ...         file=file,
            ...     )
        """
        data = clean_data({
            "out_filename": out_filename,
            "out_format": out_format.value if out_format else None,
            "q": q,
            "search": search,
            "search_fields": search_fields,
            "row_ids": row_ids,
            "fields": fields,
            "exclude": exclude,
            "order_by": order_by,
            "zipped": zipped,
            "run_async": run_async,
        })

        endpoint = urljoin(self.endpoint, 'export/')
        response = await self.api.post(endpoint, data, is_json=False)
        if run_async:
            task = await AsyncTask.get_task(self.api, response.get('task_id'))
            return task

        return response


    async def share(self, users: List['AsyncUser']) -> None:
        """
        [async] Shares the table with specified users.

        Args:
            users (List[AsyncUser]): The list of user objects to share the table with.

        Returns:
            None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient             
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     users = await client.search_users(search='John')
            >>>     await table.share(users=users)
        """
        await super()._share(self.endpoint, users)
    

    async def unshare(self, users: List['AsyncUser']) -> None:
        """
        [async] Unshares the table with specified users.

        Args:
            users (List[User]): The list of user objects to unshare the table with.

        Returns:
            None

        Example:
            >>> from geobox.aio import AsyncGeoboxClient             
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     users = await client.search_users(search='John')
            >>>     await table.unshare(users=users)
        """
        await super()._unshare(self.endpoint, users)


    async def get_shared_users(self, search: str = None, skip: int = 0, limit: int = 10) -> List['AsyncUser']:
        """
        [async] Retrieves the list of users the table is shared with.

        Args:
            search (str, optional): The search query.
            skip (int, optional): The number of users to skip.
            limit (int, optional): The maximum number of users to retrieve.

        Returns:
            List[AsyncUser]: The list of shared users.

        Example:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     await table.get_shared_users(search='John', skip=0, limit=10)
        """
        params = {
            'search': search,
            'skip': skip,
            'limit': limit
        }
        return await super()._get_shared_users(self.endpoint, params)


    async def get_relations(self) -> List['AsyncRelationship']:
        """
        [async] Get the relationships that include this table

        Returns:
            List[AsyncRelationship]: list of the relationships

        Examples:
            >>> from geobox.aio import AsyncGeoboxClient
            >>> from geobox.aio.table import AsyncTable
            >>> async with AsyncGeoboxClient() as client:
            >>>     table = await client.get_table(uuid="12345678-1234-5678-1234-567812345678")
            or  
            >>>     table = await AsyncTable.get_table(client, uuid="12345678-1234-5678-1234-567812345678")
            >>>     relationships = await table.get_relations()
        """
        from .relationship import AsyncRelationship

        endpoint = f"{self.endpoint}relations/"
        return await super()._get_list(
            api=self.api,
            endpoint=endpoint,
            factory_func=lambda api, item: AsyncRelationship(self.api, item['uuid'], item),
        )
