# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import logging
from typing import Any, Callable, Dict, List, Optional, Union
from pypaimon.api.api_response import GetTableResponse, PagedList, ErrorResponse
from pypaimon.api.rest_api import RESTApi
from pypaimon.catalog.catalog_exception import IllegalArgumentError
from pypaimon.api.rest_exception import (NoSuchResourceException, AlreadyExistsException,
                                         ForbiddenException, BadRequestException)
from pypaimon.catalog.catalog import Catalog
from pypaimon.catalog.catalog_context import CatalogContext
from pypaimon.catalog.catalog_environment import CatalogEnvironment
from pypaimon.catalog.catalog_exception import (
    TableNotExistException, DatabaseAlreadyExistException,
    TableAlreadyExistException, DatabaseNotExistException,
    TableNoPermissionException, DatabaseNoPermissionException,
    FunctionNotExistException, FunctionAlreadyExistException,
    DefinitionAlreadyExistException, DefinitionNotExistException,
    TagNotExistException, TagAlreadyExistException,
    BranchNotExistException, BranchAlreadyExistException,
)
from pypaimon.catalog.database import Database
from pypaimon.catalog.rest.property_change import PropertyChange
from pypaimon.catalog.rest.rest_token_file_io import RESTTokenFileIO
from pypaimon.catalog.rest.table_metadata import TableMetadata
from pypaimon.common.options.config import CatalogOptions, FuseOptions
from pypaimon.common.options.core_options import CoreOptions
from pypaimon.common.file_io import FileIO
from pypaimon.filesystem.caching_file_io import CachingFileIO
from pypaimon.common.identifier import Identifier
from pypaimon.schema.schema import Schema
from pypaimon.schema.schema_change import SchemaChange
from pypaimon.schema.table_schema import TableSchema
from pypaimon.snapshot.snapshot import Snapshot
from pypaimon.snapshot.snapshot_commit import PartitionStatistics
from pypaimon.table.file_store_table import FileStoreTable
from pypaimon.table.format.format_table import FormatTable, Format
from pypaimon.table.iceberg.iceberg_table import IcebergTable
from pypaimon.table.object.object_table import ObjectTable

logger = logging.getLogger(__name__)

FORMAT_TABLE_TYPE = "format-table"
ICEBERG_TABLE_TYPE = "iceberg-table"
OBJECT_TABLE_TYPE = "object-table"


class RESTCatalog(Catalog):
    def __init__(self, context: CatalogContext, config_required: Optional[bool] = True):
        self.warehouse = context.options.get(CatalogOptions.WAREHOUSE)
        self.rest_api = RESTApi(context.options, config_required)
        self.context = CatalogContext.create(self.rest_api.options, context.hadoop_conf,
                                             context.prefer_io_loader, context.fallback_io_loader)
        self.data_token_enabled = self.rest_api.options.get(CatalogOptions.DATA_TOKEN_ENABLED)
        self._cache_manager = CachingFileIO.create_cache_manager(self.context.options)

        # FUSE support (lazy import only when enabled)
        self.fuse_enabled = self.context.options.get(FuseOptions.FUSE_ENABLED, False)
        self._fuse_resolver = None
        if self.fuse_enabled:
            from pypaimon.catalog.rest.fuse_support import FusePathResolver
            self._fuse_resolver = FusePathResolver(self.context.options, self.rest_api)

    def catalog_loader(self):
        """
        Create and return a RESTCatalogLoader for this catalog.

        Returns:
            A RESTCatalogLoader instance configured with this catalog's context
        """
        from pypaimon.catalog.rest.rest_catalog_loader import RESTCatalogLoader
        return RESTCatalogLoader(self.context)

    def supports_version_management(self) -> bool:
        """
        Return whether this catalog supports version management for tables.

        Returns:
            True since REST catalogs support version management
        """
        return True

    def commit_snapshot(
            self,
            identifier: Identifier,
            table_uuid: Optional[str],
            snapshot: Snapshot,
            statistics: List[PartitionStatistics]
    ) -> bool:
        """
        Commit the Snapshot for table identified by the given Identifier.

        Args:
            identifier: Path of the table
            table_uuid: UUID of the table to avoid wrong commit
            snapshot: Snapshot to be committed
            statistics: Statistics information of this change

        Returns:
            True if commit was successful, False otherwise

        Raises:
            TableNotExistException: If the target table does not exist
            TableNoPermissionException: If no permission to access this table
        """
        try:
            return self.rest_api.commit_snapshot(identifier, table_uuid, snapshot, statistics)
        except NoSuchResourceException as e:
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e
        except Exception as e:
            # Handle other exceptions that might be thrown by the API
            raise RuntimeError(f"Failed to commit snapshot for table {identifier.get_full_name()}: {e}") from e

    def list_databases(self) -> List[str]:
        return self.rest_api.list_databases()

    def list_databases_paged(self, max_results: Optional[int] = None, page_token: Optional[str] = None,
                             database_name_pattern: Optional[str] = None) -> PagedList[str]:
        return self.rest_api.list_databases_paged(max_results, page_token, database_name_pattern)

    def create_database(self, name: str, ignore_if_exists: bool, properties: Dict[str, str] = None):
        try:
            self.rest_api.create_database(name, properties)
        except AlreadyExistsException as e:
            if not ignore_if_exists:
                # Convert REST API exception to catalog exception
                raise DatabaseAlreadyExistException(name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(name) from e

    def get_database(self, name: str) -> Database:
        try:
            response = self.rest_api.get_database(name)
            options = response.options
            options[Catalog.DB_LOCATION_PROP] = response.location
            response.put_audit_options_to(options)
            if response is not None:
                return Database(name, options)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(name) from e

    def drop_database(self, name: str, ignore_if_not_exists: bool = False, cascade: bool = False):
        if not cascade:
            try:
                tables = self.list_tables(name)
                if tables:
                    raise ValueError(
                        f"Database {name} is not empty. "
                        f"Use cascade=True to drop all tables first."
                    )
            except DatabaseNotExistException:
                if not ignore_if_not_exists:
                    raise
                return

        try:
            self.rest_api.drop_database(name)
        except NoSuchResourceException as e:
            if not ignore_if_not_exists:
                raise DatabaseNotExistException(name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(name) from e

    def alter_database(self, name: str, changes: List[PropertyChange]):
        try:
            set_properties, remove_keys = PropertyChange.get_set_properties_to_remove_keys(changes)
            self.rest_api.alter_database(name, list(remove_keys), set_properties)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(name) from e

    def list_tables(self, database_name: str) -> List[str]:
        try:
            return self.rest_api.list_tables(database_name)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(database_name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(database_name) from e

    def list_tables_paged(
            self,
            database_name: str,
            max_results: Optional[int] = None,
            page_token: Optional[str] = None,
            table_name_pattern: Optional[str] = None,
            table_type: Optional[str] = None
    ) -> PagedList[str]:
        try:
            return self.rest_api.list_tables_paged(
                database_name,
                max_results,
                page_token,
                table_name_pattern,
                table_type
            )
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(database_name) from e
        except ForbiddenException as e:
            raise DatabaseNoPermissionException(database_name) from e

    def get_table(self, identifier: Union[str, Identifier]):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        if identifier.is_system_table():
            return self._load_system_table(identifier)
        return self._load_data_table(identifier)

    def _load_data_table(self, identifier: Identifier):
        return self.load_table(
            identifier,
            lambda path: self.file_io_for_data(path, identifier),
            self.file_io_from_options,
            self.load_table_metadata,
        )

    def _load_system_table(self, identifier: Identifier):
        from pypaimon.table.system import system_table_loader

        base_identifier = Identifier.create(
            identifier.get_database_name(),
            identifier.get_table_name(),
            branch=identifier.get_branch_name(),
        )
        base_table = self._load_data_table(base_identifier)
        sys_table = system_table_loader.load(
            identifier.get_system_table_name(), base_table
        )
        if sys_table is None:
            raise TableNotExistException(identifier)
        return sys_table

    def create_table(self, identifier: Union[str, Identifier], schema: Schema, ignore_if_exists: bool):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.create_table(identifier, schema)
        except AlreadyExistsException as e:
            if not ignore_if_exists:
                raise TableAlreadyExistException(identifier) from e

    def rename_table(self, source_identifier: Union[str, Identifier], target_identifier: Union[str, Identifier]):
        if not isinstance(source_identifier, Identifier):
            source_identifier = Identifier.from_string(source_identifier)
        if not isinstance(target_identifier, Identifier):
            target_identifier = Identifier.from_string(target_identifier)
        try:
            self.rest_api.rename_table(source_identifier, target_identifier)
        except NoSuchResourceException as e:
            raise TableNotExistException(source_identifier) from e
        except AlreadyExistsException as e:
            raise TableAlreadyExistException(target_identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(source_identifier) from e

    def drop_table(self, identifier: Union[str, Identifier], ignore_if_not_exists: bool = False):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.drop_table(identifier)
        except NoSuchResourceException as e:
            if not ignore_if_not_exists:
                raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def drop_partitions(
        self,
        identifier: Union[str, Identifier],
        partitions: List[Dict[str, str]],
    ) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        if not partitions:
            raise ValueError("Partitions list cannot be empty.")
        table = self.get_table(identifier)
        if isinstance(table, (FormatTable, IcebergTable, ObjectTable)):
            unsupported_type = type(table).__name__
            raise ValueError(
                f"drop_partitions is not supported for table type '{unsupported_type}'. "
                "Only Paimon (FileStore) tables support this operation."
            )
        commit = table.new_batch_write_builder().new_commit()
        try:
            commit.truncate_partitions(partitions)
        finally:
            commit.close()

    def list_partitions_paged(
            self,
            identifier: Union[str, Identifier],
            max_results: Optional[int] = None,
            page_token: Optional[str] = None,
            partition_name_pattern: Optional[str] = None,
    ):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            return self.rest_api.list_partitions_paged(
                identifier,
                max_results,
                page_token,
                partition_name_pattern
            )
        except NoSuchResourceException as e:
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def alter_table(
        self,
        identifier: Union[str, Identifier],
        changes: List[SchemaChange],
        ignore_if_not_exists: bool = False
    ):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.alter_table(identifier, changes)
        except NoSuchResourceException as e:
            if not ignore_if_not_exists:
                raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def rollback_to(self, identifier, instant, from_snapshot=None):
        """Rollback table by the given identifier and instant.

        Args:
            identifier: Path of the table (Identifier or string).
            instant: The Instant (SnapshotInstant or TagInstant) to rollback to.
            from_snapshot: Optional snapshot ID. Success only occurs when the
                latest snapshot is this snapshot.

        Raises:
            TableNotExistException: If the table does not exist.
            TableNoPermissionException: If no permission to access this table.
        """
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.rollback_to(identifier, instant, from_snapshot)
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_SNAPSHOT:
                raise ValueError(
                    "Rollback snapshot '{}' doesn't exist.".format(e.resource_name)) from e
            elif e.resource_type == ErrorResponse.RESOURCE_TYPE_TAG:
                raise ValueError(
                    "Rollback tag '{}' doesn't exist.".format(e.resource_name)) from e
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def load_snapshot(self, identifier: Union[str, Identifier]) -> Optional['TableSnapshot']:
        """Load the latest snapshot for table.

        Args:
            identifier: Path of the table (Identifier or string).

        Returns:
            TableSnapshot instance or None if snapshot not found.

        Raises:
            TableNotExistException: If the table does not exist.
            TableNoPermissionException: If no permission to access this table.
        """
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            return self.rest_api.load_snapshot(identifier)
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_SNAPSHOT:
                return None
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def list_functions(self, database_name: str) -> List[str]:
        try:
            return self.rest_api.list_functions(database_name)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(database_name) from e

    def get_function(self, identifier: Union[str, Identifier]) -> 'Function':
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            response = self.rest_api.get_function(identifier)
            return response.to_function(identifier)
        except NoSuchResourceException as e:
            raise FunctionNotExistException(identifier) from e

    def create_function(self, identifier: Union[str, Identifier],
                        function: 'Function', ignore_if_exists: bool = False) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.create_function(identifier, function)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(identifier.get_database_name()) from e
        except AlreadyExistsException as e:
            if ignore_if_exists:
                return
            raise FunctionAlreadyExistException(identifier) from e

    def drop_function(self, identifier: Union[str, Identifier],
                      ignore_if_not_exists: bool = False) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.drop_function(identifier)
        except NoSuchResourceException as e:
            if ignore_if_not_exists:
                return
            raise FunctionNotExistException(identifier) from e

    def alter_function(self, identifier: Union[str, Identifier],
                       changes: List['FunctionChange'],
                       ignore_if_not_exists: bool = False) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.alter_function(identifier, changes)
        except AlreadyExistsException as e:
            raise DefinitionAlreadyExistException(identifier, e.resource_name) from e
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_DEFINITION:
                raise DefinitionNotExistException(identifier, e.resource_name) from e
            if not ignore_if_not_exists:
                raise FunctionNotExistException(identifier) from e
        except BadRequestException as e:
            raise IllegalArgumentError(str(e)) from e

    def list_functions_paged(
            self,
            database_name: str,
            max_results: Optional[int] = None,
            page_token: Optional[str] = None,
            function_name_pattern: Optional[str] = None,
    ) -> PagedList[str]:
        try:
            return self.rest_api.list_functions_paged(
                database_name, max_results, page_token, function_name_pattern)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(database_name) from e

    def list_functions_paged_globally(
            self,
            database_name_pattern: Optional[str] = None,
            function_name_pattern: Optional[str] = None,
            max_results: Optional[int] = None,
            page_token: Optional[str] = None,
    ) -> PagedList[Identifier]:
        result = self.rest_api.list_functions_paged_globally(
            database_name_pattern, function_name_pattern, max_results, page_token)
        functions = result.elements if result.elements else []
        return PagedList(functions, result.next_page_token)

    def list_function_details_paged(
            self,
            database_name: str,
            max_results: Optional[int] = None,
            page_token: Optional[str] = None,
            function_name_pattern: Optional[str] = None,
    ) -> PagedList['Function']:
        try:
            result = self.rest_api.list_function_details_paged(
                database_name, max_results, page_token, function_name_pattern)
            functions = [
                resp.to_function(Identifier.create(database_name, resp.name))
                for resp in result.elements
            ]
            return PagedList(functions, result.next_page_token)
        except NoSuchResourceException as e:
            raise DatabaseNotExistException(database_name) from e

    # Tag CRUD: mirrors Java RESTCatalog tag handlers.
    def create_tag(self, identifier: Union[str, Identifier], tag_name: str,
                   snapshot_id: Optional[int] = None,
                   time_retained: Optional[str] = None,
                   ignore_if_exists: bool = False) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.create_tag(identifier, tag_name, snapshot_id, time_retained)
        except AlreadyExistsException as e:
            if not ignore_if_exists:
                raise TagAlreadyExistException(tag_name) from e
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_SNAPSHOT:
                raise ValueError(
                    "Snapshot {} in table {} doesn't exist.".format(
                        e.resource_name, identifier.get_full_name())) from e
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e
        except BadRequestException as e:
            raise IllegalArgumentError(str(e)) from e

    def get_tag(self, identifier: Union[str, Identifier], tag_name: str):
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            return self.rest_api.get_tag(identifier, tag_name)
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_TAG:
                raise TagNotExistException(tag_name) from e
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def list_tags_paged(self, identifier: Union[str, Identifier],
                        max_results: Optional[int] = None,
                        page_token: Optional[str] = None,
                        tag_name_prefix: Optional[str] = None) -> PagedList[str]:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            return self.rest_api.list_tags_paged(
                identifier, max_results, page_token, tag_name_prefix)
        except NoSuchResourceException as e:
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def delete_tag(self, identifier: Union[str, Identifier], tag_name: str) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.delete_tag(identifier, tag_name)
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_TAG:
                raise TagNotExistException(tag_name) from e
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    # Branch CRUD: mirrors Java RESTCatalog branch handlers.
    def create_branch(self, identifier: Union[str, Identifier], branch_name: str,
                      tag_name: Optional[str] = None) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.create_branch(identifier, branch_name, tag_name)
        except NoSuchResourceException as e:
            if e.resource_type == ErrorResponse.RESOURCE_TYPE_TAG:
                raise TagNotExistException(tag_name) from e
            raise TableNotExistException(identifier) from e
        except AlreadyExistsException as e:
            raise BranchAlreadyExistException(branch_name) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e
        except BadRequestException as e:
            raise IllegalArgumentError(str(e)) from e

    def drop_branch(self, identifier: Union[str, Identifier], branch_name: str) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.drop_branch(identifier, branch_name)
        except NoSuchResourceException as e:
            raise BranchNotExistException(branch_name) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def rename_branch(self, identifier: Union[str, Identifier], from_branch: str,
                      to_branch: str) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.rename_branch(identifier, from_branch, to_branch)
        except NoSuchResourceException as e:
            raise BranchNotExistException(from_branch) from e
        except AlreadyExistsException as e:
            raise BranchAlreadyExistException(to_branch) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def fast_forward(self, identifier: Union[str, Identifier], branch_name: str) -> None:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            self.rest_api.fast_forward(identifier, branch_name)
        except NoSuchResourceException as e:
            raise BranchNotExistException(branch_name) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def list_branches(self, identifier: Union[str, Identifier]) -> List[str]:
        if not isinstance(identifier, Identifier):
            identifier = Identifier.from_string(identifier)
        try:
            return self.rest_api.list_branches(identifier)
        except NoSuchResourceException as e:
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def load_table_metadata(self, identifier: Identifier) -> TableMetadata:
        try:
            response = self.rest_api.get_table(identifier)
            return self.to_table_metadata(identifier.get_database_name(), response)
        except NoSuchResourceException as e:
            raise TableNotExistException(identifier) from e
        except ForbiddenException as e:
            raise TableNoPermissionException(identifier) from e

    def to_table_metadata(self, db: str, response: GetTableResponse) -> TableMetadata:
        schema = TableSchema.from_schema(response.schema_id, response.get_schema())
        options: Dict[str, str] = dict(schema.options)
        options[CoreOptions.PATH.key()] = response.get_path()
        response.put_audit_options_to(options)

        return TableMetadata(
            schema=schema.copy(options),
            is_external=response.get_is_external(),
            uuid=response.get_id()
        )

    def file_io_from_options(self, table_path: str) -> FileIO:
        return CachingFileIO.wrap_with_caching_if_needed(
            FileIO.get(table_path, self.context.options), self.context.options,
            self._cache_manager)

    def file_io_for_data(self, table_path: str, identifier: Identifier):
        """
        Get FileIO for data access, supporting FUSE local path mapping.
        """
        if self._fuse_resolver is not None:
            file_io = self._fuse_resolver.get_file_io(
                table_path, identifier, self.data_token_enabled,
                rest_token_file_io_factory=lambda: RESTTokenFileIO(
                    identifier, table_path, self.context.options),
                default_file_io_factory=lambda: self.file_io_from_options(table_path),
            )
        elif self.data_token_enabled:
            file_io = RESTTokenFileIO(identifier, table_path, self.context.options)
        else:
            file_io = self.file_io_from_options(table_path)
        return CachingFileIO.wrap_with_caching_if_needed(
            file_io, self.context.options, self._cache_manager)

    def load_table(self,
                   identifier: Identifier,
                   internal_file_io: Callable[[str], Any],
                   external_file_io: Callable[[str], Any],
                   metadata_loader: Callable[[Identifier], TableMetadata],
                   ):
        metadata = metadata_loader(identifier)
        schema = metadata.schema
        table_type = schema.options.get(CoreOptions.TYPE.key(), "").strip().lower()
        if table_type == FORMAT_TABLE_TYPE:
            return self._create_format_table(identifier, metadata, internal_file_io, external_file_io)
        data_file_io = external_file_io if metadata.is_external else internal_file_io
        if table_type == ICEBERG_TABLE_TYPE:
            return self._create_iceberg_table(identifier, metadata, data_file_io)
        if table_type == OBJECT_TABLE_TYPE:
            return self._create_object_table(identifier, metadata, data_file_io)
        catalog_env = CatalogEnvironment(
            identifier=identifier,
            uuid=metadata.uuid,
            catalog_loader=self.catalog_loader(),
            supports_version_management=True  # REST catalogs support version management
        )
        # Use the path from server response directly (do not trim scheme)
        table_path = schema.options.get(CoreOptions.PATH.key())
        table = self.create(data_file_io(table_path),
                            table_path,
                            schema,
                            catalog_env)
        return table

    def _create_format_table(self,
                             identifier: Identifier,
                             metadata: TableMetadata,
                             internal_file_io: Callable[[str], Any],
                             external_file_io: Callable[[str], Any],
                             ) -> FormatTable:
        schema = metadata.schema
        location = schema.options.get(CoreOptions.PATH.key())
        if not location:
            raise ValueError("Format table schema must have path option")
        data_file_io = external_file_io if metadata.is_external else internal_file_io
        file_io = data_file_io(location)
        file_format = schema.options.get(CoreOptions.FILE_FORMAT.key(), "parquet")
        fmt = Format.parse(file_format)
        return FormatTable(
            file_io=file_io,
            identifier=identifier,
            table_schema=schema,
            location=location,
            format=fmt,
            options=dict(schema.options),
            comment=schema.comment,
        )

    def _create_iceberg_table(self,
                              identifier: Identifier,
                              metadata: TableMetadata,
                              file_io: Callable[[str], Any],
                              ) -> IcebergTable:
        schema = metadata.schema
        location = schema.options.get(CoreOptions.PATH.key())
        file_io = file_io(location)
        return IcebergTable(
            file_io=file_io,
            identifier=identifier,
            table_schema=schema,
            location=location,
            options=dict(schema.options),
            comment=schema.comment,
            uuid=metadata.uuid,
        )

    def _create_object_table(self,
                             identifier: Identifier,
                             metadata: TableMetadata,
                             file_io: Callable[[str], Any],
                             ) -> ObjectTable:
        schema = metadata.schema
        location = schema.options.get(CoreOptions.PATH.key())
        file_io = file_io(location)
        return ObjectTable(
            file_io=file_io,
            identifier=identifier,
            table_schema=schema,
            location=location,
            options=dict(schema.options),
            comment=schema.comment,
        )

    @staticmethod
    def create(file_io: FileIO,
               table_path: str,
               table_schema: TableSchema,
               catalog_environment: CatalogEnvironment
               ) -> FileStoreTable:
        """Create FileStoreTable with dynamic options and catalog environment"""
        return FileStoreTable(file_io, catalog_environment.identifier, table_path, table_schema, catalog_environment)
