# 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.

from dataclasses import dataclass
from typing import Optional

from pypaimon.common.json_util import json_field

SYSTEM_TABLE_SPLITTER = '$'
SYSTEM_BRANCH_PREFIX = 'branch_'
DEFAULT_MAIN_BRANCH = 'main'
UNKNOWN_DATABASE = 'unknown'


@dataclass(init=False)
class Identifier:
    """Identifies a database object (table, view, etc.).

    1:1 port of ``org.apache.paimon.catalog.Identifier``: the on-the-wire
    shape is exactly two fields, ``database`` and ``object``. Any branch /
    system-table portion is encoded into the ``object`` field using the
    ``$`` separator and the ``branch_`` prefix, so JSON written by Python
    is round-trippable through the Java REST server (and vice versa).

    Mirrors Java's three public constructors via a single signature:
      * ``Identifier(database, object)`` — JSON-create form. ``object``
        is the final, possibly-encoded string.
      * ``Identifier(database, table, branch=...)`` — encodes ``branch``
        into ``object``.
      * ``Identifier(database, table, branch=..., system_table=...)`` —
        encodes both.

    ``branch == "main"`` (case-insensitive) is treated as the default
    branch and is not encoded into the object name, matching Java.
    """

    database: str = json_field("database", default=None)
    object: str = json_field("object", default=None)

    def __init__(self, database: str, object: Optional[str] = None,
                 branch: Optional[str] = None,
                 system_table: Optional[str] = None):
        self.database = database
        if branch is None and system_table is None:
            # @JsonCreator form: ``object`` is already the final, encoded
            # string. Components are decoded lazily by _split_object_name().
            self.object = object
            self._table: Optional[str] = None
            self._branch: Optional[str] = None
            self._system_table: Optional[str] = None
        else:
            # Encoding form: ``object`` is the bare table name; encode
            # branch / system_table into the on-wire ``object``.
            builder = object
            if branch is not None and branch.lower() != DEFAULT_MAIN_BRANCH:
                builder = (builder + SYSTEM_TABLE_SPLITTER
                           + SYSTEM_BRANCH_PREFIX + branch)
            if system_table is not None:
                builder = builder + SYSTEM_TABLE_SPLITTER + system_table
            self.object = builder
            self._table = object
            self._branch = branch
            self._system_table = system_table

    @classmethod
    def create(cls, database: str, table: str,
               branch: Optional[str] = None,
               system_table: Optional[str] = None) -> "Identifier":
        """Create an Identifier.

        Two-arg form ``create(database, object)`` mirrors Java's
        ``Identifier.create``: the second argument is treated as the final
        ``object`` string (may already carry encoded branch / system_table
        segments).

        Multi-arg form ``create(database, table, branch=..., system_table=...)``
        is a Python convenience that encodes the components into ``object``
        for you, equivalent to ``Identifier(database, table, branch=...,
        system_table=...)``.
        """
        return cls(database, table, branch=branch, system_table=system_table)

    @classmethod
    def from_string(cls, full_name: str) -> "Identifier":
        """Parse a ``database.object`` identifier, with optional backtick quoting."""
        if not full_name or not full_name.strip():
            raise ValueError("fullName cannot be null or empty")

        if '`' in full_name:
            return cls._parse_with_backticks(full_name)

        parts = full_name.split(".", 1)

        if len(parts) != 2:
            raise ValueError(
                "Cannot get splits from '{}' to get database and object".format(full_name)
            )

        return cls(parts[0], parts[1])

    @classmethod
    def _parse_with_backticks(cls, full_name: str) -> "Identifier":
        parts = []
        current = ""
        in_backticks = False

        for char in full_name:
            if char == '`':
                in_backticks = not in_backticks
            elif char == '.' and not in_backticks:
                parts.append(current)
                current = ""
            else:
                current += char

        if current:
            parts.append(current)

        if in_backticks:
            raise ValueError("Unclosed backtick in identifier: {}".format(full_name))

        if len(parts) != 2:
            raise ValueError("Invalid identifier format: {}".format(full_name))

        return cls(parts[0], parts[1])

    def _split_object_name(self) -> None:
        if self._table is not None:
            return

        splits = self.object.split(SYSTEM_TABLE_SPLITTER)
        if len(splits) == 1:
            self._table = self.object
            self._branch = None
            self._system_table = None
        elif len(splits) == 2:
            self._table = splits[0]
            if splits[1].startswith(SYSTEM_BRANCH_PREFIX):
                self._branch = splits[1][len(SYSTEM_BRANCH_PREFIX):]
                self._system_table = None
            else:
                self._branch = None
                self._system_table = splits[1]
        elif len(splits) == 3:
            if not splits[1].startswith(SYSTEM_BRANCH_PREFIX):
                raise ValueError(
                    "System table can only contain one '$' separator, "
                    "but this is: " + self.object
                )
            self._table = splits[0]
            self._branch = splits[1][len(SYSTEM_BRANCH_PREFIX):]
            self._system_table = splits[2]
        else:
            raise ValueError("Invalid object name: " + self.object)

    def get_full_name(self) -> str:
        # Match Java: tables created without an explicit database (e.g. some
        # ad-hoc query paths) land in the special "unknown" database, in which
        # case the database segment is dropped from the rendered name.
        if UNKNOWN_DATABASE == self.database:
            return self.object
        return "{}.{}".format(self.database, self.object)

    def get_database_name(self) -> str:
        return self.database

    def get_table_name(self) -> str:
        self._split_object_name()
        return self._table

    def get_object_name(self) -> str:
        return self.object

    def get_branch_name(self) -> Optional[str]:
        self._split_object_name()
        return self._branch

    def get_branch_name_or_default(self) -> str:
        """Get branch name or return ``DEFAULT_MAIN_BRANCH`` if no branch is encoded."""
        branch = self.get_branch_name()
        return branch if branch is not None else DEFAULT_MAIN_BRANCH

    def get_system_table_name(self) -> Optional[str]:
        self._split_object_name()
        return self._system_table

    def is_system_table(self) -> bool:
        return self.get_system_table_name() is not None

    @property
    def branch(self) -> Optional[str]:
        # Read/write alias for callers that previously accessed the
        # ``Identifier.branch`` dataclass field directly. Java's
        # ``branch`` is transient/private and not exposed; Python kept
        # it public, so this property tides external code over.
        return self.get_branch_name()

    @branch.setter
    def branch(self, value: Optional[str]) -> None:
        # Re-encode ``object`` so the wire shape stays consistent with
        # the new value (equivalent to Identifier(db, table, branch=value,
        # system_table=current_system_table)).
        table = self.get_table_name()
        system_table = self.get_system_table_name()
        rebuilt = Identifier(
            self.database, table, branch=value, system_table=system_table
        )
        self.object = rebuilt.object
        self._table = table
        self._branch = value
        self._system_table = system_table

    def __hash__(self):
        return hash((self.database, self.object))

    def __eq__(self, other):
        if not isinstance(other, Identifier):
            return NotImplemented
        return self.database == other.database and self.object == other.object
