"""Raw segment definitions.

This is designed to be the root segment, without
any children, and the output of the lexer.
"""

import functools
from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast

import regex as re

from sqlfluff.core.helpers.identity import get_next_id
from sqlfluff.core.parser.markers import PositionMarker
from sqlfluff.core.parser.segments.base import BaseSegment, SourceFix

if TYPE_CHECKING:  # pragma: no cover
    from sqlfluff.core.templaters import TemplatedFile
    from sqlfluffrs import RsToken


class RawSegment(BaseSegment):
    """This is a segment without any subsegments."""

    type = "raw"
    _is_code = True
    _is_comment = False
    _is_whitespace = False
    # Classes inheriting from RawSegment may provide a _default_raw
    # to enable simple initialisation.
    _default_raw = ""

    # NOTE: These are declared here (rather than solely via `self.x = value`
    # in __init__) because __init__ writes most fields straight into
    # self.__dict__ for performance (see the NOTE in __init__), which mypy
    # can't use to infer instance attribute types the way it can a plain
    # assignment.
    _raw: str
    _raw_upper: str
    _raw_value: str
    instance_types: tuple[str, ...]
    trim_start: Optional[tuple[str, ...]]
    trim_chars: Optional[tuple[str, ...]]
    _source_fixes: Optional[list[SourceFix]]
    quoted_value: Optional[tuple[str, Union[int, str]]]
    escape_replacements: Optional[list[tuple[str, str]]]
    casefold: Optional[Callable[[str], str]]

    def __init__(
        self,
        raw: Optional[str] = None,
        pos_marker: Optional[PositionMarker] = None,
        # For legacy and syntactic sugar we allow the simple
        # `type` argument here, but for more precise inheritance
        # we suggest using the `instance_types` option.
        type: Optional[str] = None,
        instance_types: tuple[str, ...] = (),
        trim_start: Optional[tuple[str, ...]] = None,
        trim_chars: Optional[tuple[str, ...]] = None,
        source_fixes: Optional[list[SourceFix]] = None,
        uuid: Optional[int] = None,
        quoted_value: Optional[tuple[str, Union[int, str]]] = None,
        escape_replacements: Optional[list[tuple[str, str]]] = None,
        casefold: Optional[Callable[[str], str]] = None,
    ):
        """Initialise raw segment.

        If raw is not provided, we default to _default_raw if present.
        If pos_marker is not provided, it is assume that this will be
        inserted later as part of a reposition phase.

        NOTE: This writes straight into ``self.__dict__`` rather than via
        plain attribute assignment. RawSegment has no children, so none of
        BaseSegment.__setattr__'s cache-invalidation machinery applies here,
        but *any* class-level ``__setattr__`` override still forces every
        ``self.x = y`` through a full Python-level call. This class is on
        the lexer's hot path (one instantiation per raw token), so bypassing
        the attribute protocol entirely is a meaningful win at that volume.
        """
        if type:
            assert not instance_types, "Cannot set `type` and `instance_types`."
            instance_types = (type,)

        _raw = raw if raw is not None else self._default_raw

        # pos marker is required here. We ignore the typing initially
        # because it might *initially* be unset, but it will be reset
        # later. NOTE: The annotated declaration below tells mypy that,
        # for a RawSegment specifically, pos_marker is non-Optional.
        # The actual assignment goes through __dict__ (not __setattr__)
        # to avoid a wasted _recalculate_caches() call on construction.
        self.pos_marker: PositionMarker
        d = self.__dict__
        d["pos_marker"] = pos_marker
        d["_raw"] = _raw
        d["_raw_upper"] = _raw.upper()
        # Set the segments attribute to be an empty tuple.
        d["segments"] = ()
        d["instance_types"] = instance_types
        # What should we trim off the ends to get to content
        d["trim_start"] = trim_start
        d["trim_chars"] = trim_chars
        # Keep track of any source fixes
        d["_source_fixes"] = source_fixes
        # Identifier for matching (a plain int, swift for comparisons).
        d["uuid"] = uuid or get_next_id()
        d["quoted_value"] = quoted_value
        d["escape_replacements"] = escape_replacements
        d["casefold"] = casefold
        d["_raw_value"] = self.normalize()

    @functools.cached_property
    def representation(self) -> str:
        """Lazy string representation, computed once on first access."""
        return "<{}: ({}) {!r}>".format(
            self.__class__.__name__, self.pos_marker, self.raw
        )

    def __repr__(self) -> str:
        # This is calculated at __init__, because all elements are immutable
        # and this was previously recalculating the pos marker,
        # and became very expensive
        return self.representation

    def __setattr__(self, key: str, value: Any) -> None:
        """Overwrite BaseSegment's __setattr__ with BaseSegment's superclass."""
        if key == "pos_marker":
            self._recalculate_caches()
        super(BaseSegment, self).__setattr__(key, value)

    # ################ PUBLIC PROPERTIES

    @property
    def is_code(self) -> bool:
        """Return True if this segment is code."""
        return self._is_code

    @property
    def is_comment(self) -> bool:
        """Return True if this segment is a comment."""
        return self._is_comment

    @property
    def is_whitespace(self) -> bool:
        """Return True if this segment is whitespace."""
        return self._is_whitespace

    @property
    def raw(self) -> str:
        """Returns the raw segment."""
        return self._raw

    @property
    def raw_upper(self) -> str:
        """Returns the raw segment in uppercase."""
        return self._raw_upper

    @property
    def raw_segments(self) -> list["RawSegment"]:
        """Returns self to be compatible with calls to its superclass."""
        return [self]

    @property
    def class_types(self) -> frozenset[str]:
        """The set of full types for this segment, including inherited.

        Add the surrogate type for raw segments.
        """
        return frozenset(self.instance_types) | super().class_types

    @property
    def source_fixes(self) -> list[SourceFix]:
        """Return any source fixes as list."""
        return self._source_fixes or []

    # ################ INSTANCE METHODS

    def invalidate_caches(self) -> None:
        """Overwrite superclass functionality."""
        pass

    def get_type(self) -> str:
        """Returns the type of this segment as a string."""
        if self.instance_types:
            return self.instance_types[0]
        return super().get_type()

    def is_type(self, *seg_type: str) -> bool:
        """Extend the parent class method with the surrogate types."""
        if set(self.instance_types).intersection(seg_type):
            return True
        return self.class_is_type(*seg_type)

    def get_raw_segments(self) -> list["RawSegment"]:
        """Iterate raw segments, mostly for searching."""
        return [self]

    def raw_trimmed(self) -> str:
        """Return a trimmed version of the raw content.

        Returns:
            str: The trimmed version of the raw content.
        """
        raw_buff = self.raw
        if self.trim_start:
            for seq in self.trim_start:
                if raw_buff.startswith(seq):
                    raw_buff = raw_buff[len(seq) :]
        if self.trim_chars:
            raw_buff = self.raw
            # for each thing to trim
            for seq in self.trim_chars:
                # trim start
                while raw_buff.startswith(seq):
                    raw_buff = raw_buff[len(seq) :]
                # trim end
                while raw_buff.endswith(seq):
                    raw_buff = raw_buff[: -len(seq)]
            return raw_buff
        return raw_buff

    def normalize(self, value: Optional[str] = None) -> str:
        """Returns the normalized version of a string using the segment's rules.

        By default this uses the raw value of the segment.
        E.g. This removes leading and trailing quote characters, removes escapes

        Return:
        str: The normalized value
        """
        raw_buff = value or self.raw
        if self.quoted_value:
            _match = re.match(self.quoted_value[0], raw_buff)
            if _match:
                _group_match = _match.group(self.quoted_value[1])
                if isinstance(_group_match, str):
                    raw_buff = _group_match
        if self.escape_replacements:
            for old, new in self.escape_replacements:
                raw_buff = re.sub(old, new, raw_buff)
        return raw_buff

    def raw_normalized(self, casefold: bool = True) -> str:
        """Returns a normalized string of the raw content.

        E.g. This removes leading and trailing quote characters, removes escapes,
        optionally casefolds to the dialect's casing

        Return:
        str: The normalized version of the raw content
        """
        raw_buff = self._raw_value
        if self.casefold and casefold:
            raw_buff = self.casefold(raw_buff)
        return raw_buff

    def stringify(
        self, ident: int = 0, tabsize: int = 4, code_only: bool = False
    ) -> str:
        """Use indentation to render this segment and its children as a string.

        Args:
            ident (int, optional): The indentation level. Defaults to 0.
            tabsize (int, optional): The size of each tab. Defaults to 4.
            code_only (bool, optional): Whether to render only the code.
                Defaults to False.

        Returns:
            str: The rendered string.
        """
        preface = self._preface(ident=ident, tabsize=tabsize)
        return preface + "\n"

    def _suffix(self) -> str:
        """Return any extra output required at the end when logging.

        NB Override this for specific subclasses if we want extra output.

        Returns:
            str: The extra output.
        """
        return f"{self.raw!r}"

    def edit(
        self, raw: Optional[str] = None, source_fixes: Optional[list[SourceFix]] = None
    ) -> "RawSegment":
        """Create a new segment, with exactly the same position but different content.

        Args:
            raw (Optional[str]): The new content for the segment.
            source_fixes (Optional[list[SourceFix]]): A list of fixes to be applied to
                the segment.

        Returns:
            RawSegment: A copy of this object with new contents.

        Used mostly by fixes.

        NOTE: This *doesn't* copy the uuid. The edited segment is a new
        segment.

        """
        return self.__class__(
            raw=raw or self.raw,
            pos_marker=self.pos_marker,
            instance_types=self.instance_types,
            trim_start=self.trim_start,
            trim_chars=self.trim_chars,
            quoted_value=self.quoted_value,
            escape_replacements=self.escape_replacements,
            casefold=self.casefold,
            source_fixes=source_fixes or self.source_fixes,
        )

    def _get_raw_segment_kwargs(self) -> dict[str, Any]:
        return {
            "quoted_value": self.quoted_value,
            "escape_replacements": self.escape_replacements,
            "casefold": self.casefold,
        }

    # ################ CLASS METHODS

    @classmethod
    def from_result_segments(
        cls,
        result_segments: tuple[BaseSegment, ...],
        segment_kwargs: dict[str, Any],
    ) -> "RawSegment":
        """Create a RawSegment from result segments."""
        assert len(result_segments) == 1
        raw_seg = cast("RawSegment", result_segments[0])
        new_segment_kwargs = raw_seg._get_raw_segment_kwargs()
        new_segment_kwargs.update(segment_kwargs)
        return cls(
            raw=raw_seg.raw,
            pos_marker=raw_seg.pos_marker,
            **new_segment_kwargs,
        )

    @classmethod
    def from_rstoken(
        cls,
        token: "RsToken",
        tf: "TemplatedFile",
    ) -> "RawSegment":
        """Create a RawSegment from an RSQL token.

        Args:
            token: The Rust token to create the segment from
            tf: The TemplatedFile for position marker reconstruction
        """
        # Build instance_types from token
        instance_types = tuple(token.instance_types)

        # NOTE: The pyo3 getters return lists (Vec<String>), but the Python
        # lexer configures these kwargs as tuples and downstream code (and
        # byte-level parity with Python-lexed segments) expects tuples.
        trim_start = tuple(token.trim_start) if token.trim_start else None
        trim_chars = tuple(token.trim_chars) if token.trim_chars else None

        segment = cls(
            raw=token.raw,
            pos_marker=PositionMarker.from_rs_position_marker(token.pos_marker, tf),
            instance_types=instance_types,
            trim_start=trim_start,
            trim_chars=trim_chars,
            source_fixes=token.source_fixes,
            uuid=token.uuid,
            quoted_value=token.quoted_value,
            escape_replacements=token.escape_replacements,
        )
        # Cache the original RsToken for efficient round-trip to Rust parser
        segment._rstoken = token
        return segment


__all__ = [
    "PositionMarker",
    "RawSegment",
    "SourceFix",
]
