"""
textfsmgen.engine.diff
=================

Module for computing and representing differences between TextFSM‑generated
templates, parsed outputs, or line snippets.
"""

import re
from typing import Tuple, Optional

from typing import List
from difflib import ndiff
from itertools import combinations

from textfsmgen.core.patterns import TextPattern
from textfsmgen.core.patterns import ElementPattern
from textfsmgen.core.patterns import LinePattern

from textfsmgen.libs import PATTERN
from textfsmgen.libs import text

from textfsmgen.engine.translate import PatternTranslator
from textfsmgen.exceptions import RuntimeException


class NDiffBaseText:
    """
    Base class for representing normalized diff text nodes.
    """
    def __init__(self, txt: str):
        self._pattern: str = ""
        self._snippet: str = ""
        self._lst: List[str] = []
        self._lst_other: List[str] = []
        self._is_common: bool = False
        self._is_changed: bool = False

        # Detect common lines
        if txt.startswith("  "):
            self._lst.append(txt.lstrip(" "))
            self._is_common = True

        # Detect changed lines
        if txt.startswith("- ") or txt.startswith("+ "):
            if txt.startswith("- "):
                self._lst.append(txt.lstrip("- "))
            if txt.startswith("+ "):
                self._lst_other.append(txt.lstrip("+ "))
            self._is_changed = True

    def __bool__(self) -> bool:
        return bool(self._lst or self._lst_other)

    def __len__(self) -> int:
        return len(self._lst) + len(self._lst_other)

    @property
    def is_common(self) -> bool:
        """bool: Whether this node represents a common (unchanged) line."""
        return self._is_common

    @property
    def is_changed(self) -> bool:
        """bool: Whether this node represents a changed (added/removed) line."""
        return self._is_changed

    @property
    def name(self) -> str:
        """str: Name identifier for the node type (default empty)."""
        return ""

    @property
    def lst(self) -> List[str]:
        """list of str: Text fragments associated with this node."""
        return self._lst

    @property
    def lst_other(self) -> List[str]:
        """list of str: Text fragments from the other side of the diff."""
        return self._lst_other

    def is_same_type(self, other: "NDiffBaseText") -> bool:
        """Check whether another node is of the same type."""
        return bool(self.name and self.name == other.name)

    def extend(self, other: "NDiffBaseText") -> None:
        """
        Extend this node's fragments with those from another node.
        """
        if self.name and self.name == other.name:
            self._lst.extend(other.lst)
            self._lst_other.extend(other.lst_other)

    def readjust_lst(self, *lst_of_txt: str) -> None:
        """
        Replace `_lst` with a new set of text fragments.
        """
        if lst_of_txt:
            self._lst.clear()
            self._lst.extend(txt for txt in lst_of_txt if txt)

    def readjust_lst_other(self, *lst_of_other_txt: str) -> None:
        """
        Replace `_lst_other` with a new set of text fragments.
        """
        if lst_of_other_txt:
            self._lst_other.clear()
            self._lst_other.extend(txt for txt in lst_of_other_txt if txt)

    @classmethod
    def do_factory_create(cls, txt: str) -> "NDiffBaseText":
        """Factory method to create the appropriate diff node."""
        if txt.startswith("  "):
            return NDiffCommonText(txt)
        changed_node = NDiffChangedText(txt)
        return changed_node if changed_node else None


class NDiffCommonText(NDiffBaseText):
    """
    Diff node representing common (unchanged) text lines.
    """

    @property
    def name(self) -> str:
        """Identifier for this diff node type."""
        return "ndiff_common_text" if bool(self) else ""

    def get_pattern(self, whitespace: str = " ") -> str:
        """Generate a normalized regex pattern for the common line."""
        txt = "  ".join(self.lst)
        pattern = TextPattern(txt) if txt else ""
        self._pattern = pattern.replace(" ", whitespace) if pattern else ""
        return self._pattern

    def get_snippet(self, whitespace: str = " ") -> str:
        """Generate a snippet representation of the common line."""
        is_ws = whitespace == PATTERN.WS
        spacer = "\t " if is_ws else "  "
        snippet = spacer.join(self.lst)
        self._snippet = snippet
        return snippet


class NDiffChangedText(NDiffBaseText):
    """Diff node representing changed text lines."""

    @property
    def name(self) -> str:
        return "ndiff_changed_text" if bool(self) else ""

    @property
    def is_containing_empty_changed(self) -> bool:
        """Whether this node represents a change that may be empty."""
        if self.lst and self.lst_other:
            return False
        elif self.lst or self.lst_other:
            return True
        return False

    def get_pattern(
        self,
        var: str = "",
        label: Optional[str | None] = None,
        is_root: bool = False,
    ) -> str:
        """Generate a normalized regex pattern for the changed line."""
        if label:
            var = var.replace("v", f"v{label}", 1)

        txt1 = "  ".join(self.lst)
        txt2 = "  ".join(self.lst_other)

        if txt1 or txt2:    # noqa
            args = [txt1, txt2] if txt1 and txt2 else [txt1] if txt1 else [txt2]
            factory = PatternTranslator.do_factory_create(*args)
            pattern = factory.root_pattern if is_root else factory.pattern
        else:
            pattern = ""

        if var:
            fmt = "(?P<%s>(%s)|)" if self.is_containing_empty_changed else "(?P<%s>%s)"
            pattern = fmt % (var, pattern)
        elif pattern:
            pattern = f"(({pattern})|)" if self.is_containing_empty_changed else pattern

        return pattern

    def get_snippet(
        self,
        var: str = "",
        label: Optional[str | None] = None,
        is_root: bool = False,
    ) -> str:
        """Generate a snippet representation of the changed line."""
        if label:
            var = var.replace("v", f"v{label}", 1)

        txt1 = "  ".join(self.lst)
        txt2 = "  ".join(self.lst_other)

        if txt1 or txt2:
            args = [txt1, txt2] if txt1 and txt2 else [txt1] if txt1 else [txt2]
            factory = PatternTranslator.do_factory_create(*args)
            kwargs = dict(var=var, is_root=is_root)
            self._snippet = factory.get_template_snippet(**kwargs)
            if self.is_containing_empty_changed:
                self._snippet = f"{self._snippet[:-1]}, or_empty)"

        return self._snippet


class NDiffLinePattern:
    """
    Represents a normalized diff pattern between two text lines.
    """

    def __init__(
        self,
        line_a: str,
        line_b: str,
        whitespace: str = None,
        label: str = None,
        is_root: bool = False,
    ):
        self.whitespace = whitespace
        if not self.whitespace:
            is_ws = any(text.Line.has_whitespace_in_line(line) for line in [line_a, line_b])
            self.whitespace = PATTERN.WS if is_ws else PATTERN.SPACE

        self.label = label
        self.is_root = is_root

        # Leading whitespace detection
        self.is_leading = text.Line.has_leading(line_a) or text.Line.has_leading(line_b)
        self.are_leading = text.Line.has_leading(line_a) and text.Line.has_leading(line_b)

        # trailing whitespace detection
        self.is_trailing = text.Line.has_trailing(line_a) or text.Line.has_trailing(line_b)
        self.are_trailing = text.Line.has_trailing(line_a) and text.Line.has_trailing(line_b)

        multi = '+' if self.are_leading else '*'
        ws = self.whitespace
        self.leading_whitespace = f'{ws}{multi}' if self.is_leading else ""

        multi = '+' if self.are_trailing else '*'
        self.trailing_whitespace = f'{ws}{multi}' if self.is_trailing else ""

        self.line_a = line_a
        self.line_b = line_b

        self._line_a = self.line_a.strip()
        self._line_b = self.line_b.strip()

        self._is_diff = False
        self._pattern = ""
        self._snippet = ""
        self.process()

    def __len__(self) -> int:
        """Return nonzero if a pattern has been generated."""
        return int(self._pattern != "")

    def __call__(self, *args, **kwargs) -> "NDiffLinePattern":
        """Create a new instance of `NDiffLinePattern` with given arguments."""
        return self.__class__(*args, **kwargs)

    @property
    def is_diff(self) -> bool:
        """bool: Whether the two lines differ."""
        return self._is_diff

    @property
    def pattern(self) -> str:
        """str: Full regex pattern including leading/trailing whitespace."""
        return f"{self.leading_whitespace}{self._pattern}{self.trailing_whitespace}"

    @property
    def snippet(self) -> str:
        """Snippet representation of the line comparison."""
        if self.is_diff:
            leading_snippet = "start()"
            trailing_snippet = "end()"
            whitespace = "space"
            if self.whitespace == r"\s":
                whitespace = "whitespaces"
            if self.is_leading:
                new_ws = f"{whitespace}s" if self.are_leading else whitespace
                leading_snippet = leading_snippet.replace("()", f"({new_ws})")
            if self.is_trailing:
                new_ws = f"{whitespace}s" if self.are_trailing else whitespace
                trailing_snippet = trailing_snippet.replace("()", f"({new_ws})")
            return f"{leading_snippet} {self._snippet} {trailing_snippet}"
        return self._snippet

    # --- Analysis methods ---

    def analyze_and_parse_empty_case(self) -> bool:
        """Handle the case where both lines are empty and equal."""
        is_equal = self._line_a == self._line_b
        is_empty = self._line_a == ""

        if is_empty and is_equal:
            if self.is_leading or self.is_trailing:
                self._pattern = f"{self.whitespace}+"
            self._snippet = self.line_a
            return True
        return False

    def analyze_and_parse_identical_case(self) -> bool:
        """
        Handle the case where both lines are identical or whitespace-equivalent.
        """
        if self._line_a == self._line_b:
            self._pattern = TextPattern(self._line_a)
            self._snippet = self.line_a
            return True

        lst_a = re.split(f"{self.whitespace}+", self._line_a)
        lst_b = re.split(f"{self.whitespace}+", self._line_b)
        if lst_a == lst_b:
            new_lst = [TextPattern(item) for item in lst_a]
            self._pattern = f"{self.whitespace}+".join(new_lst)
            return True
        return False

    def build_list_of_diff(self) -> List[NDiffBaseText]:
        """Tokenize both lines and build a list of diff nodes."""
        # Tokenize both lines by whitespace
        lst_a = re.split(PATTERN.WSS, self._line_a)
        lst_b = re.split(PATTERN.WSS, self._line_b)

        # Compute diff between token lists
        diff = ndiff(lst_a, lst_b)

        # Filter out alignment hints
        tokens = [item for item in diff if not item.startswith("? ")]

        result: List[NDiffBaseText] = []
        for item in tokens:
            node = NDiffBaseText.do_factory_create(item)
            if result and result[-1].is_same_type(node):
                # Merge with previous node if same type
                result[-1].extend(node)
            else:
                result.append(node)

        return result

    def build_pattern_from_diff_list(self, lst) -> str:    # noqa
        """Construct a regex pattern from a list of diff nodes."""
        kwargs = dict(label=self.label, is_root=self.is_root)

        total = len(lst)
        if total == 1:
            item = lst[0]
            if item.is_changed:
                return item.get_pattern(var='v0', **kwargs)
            return item.get_pattern(whitespace=self.whitespace)

        result = []
        count = 0
        spacer = PATTERN.WSS if self.whitespace == PATTERN.WS else PATTERN.SPACES
        for index, item in enumerate(lst):
            is_last = index == total - 1
            if not is_last:
                if item.is_changed:
                    pat = item.get_pattern(var=f"v{count}", **kwargs)
                    count += 1
                    if item.is_containing_empty_changed:
                        result.extend([pat, f"({spacer})?"])
                    else:
                        result.extend([pat, spacer])
                else:
                    result.extend([item.get_pattern(whitespace=self.whitespace), spacer])
            else:
                if item.is_changed:
                    pat = item.get_pattern(var=f"v{count}", **kwargs)
                    if item.is_containing_empty_changed:
                        # Adjust previous spacer to optional
                        if result:
                            result.pop()
                        result.extend([f"({spacer})?", pat])
                    else:
                        result.append(pat)
                else:
                    result.append(item.get_pattern(whitespace=self.whitespace))
        return "".join(result)

    def build_snippet_from_diff_list(self, lst) -> str:  # noqa
        """Construct a snippet string from a list of diff nodes."""
        result: List[str] = []
        count = 0

        for item in lst:
            if item.is_changed:
                count += 1
                kwargs = dict(
                    var=f"v{count}",
                    label=self.label,
                    is_root=self.is_root,
                )
                snippet_ = item.get_snippet(**kwargs)   # noqa
            else:
                snippet_ = item.get_snippet(whitespace=self.whitespace) # noqa
            result.append(snippet_)

        snippet = "  ".join(result)
        return snippet

    def analyze_and_parse_diff_case(self) -> bool:
        """Analyze and parse the case where the two lines differ."""
        if self.analyze_and_parse_empty_case():
            return False
        if self.analyze_and_parse_identical_case():
            return False

        self._is_diff = True
        lst = self.build_list_of_diff()
        self._pattern = self.build_pattern_from_diff_list(lst)
        self._snippet = self.build_snippet_from_diff_list(lst)
        return True

    def process(self) -> None:
        """Perform full analysis of the two lines."""
        is_empty = self.analyze_and_parse_empty_case()
        is_similar = not is_empty and self.analyze_and_parse_identical_case()
        if not is_similar:
            self.analyze_and_parse_diff_case()


class DiffLineTranslator(RuntimeException):
    """
    Represents a normalized diff pattern between multiple text lines.
    """

    def __init__(
        self, line1: str, line2: str, *other_lines: str,
        label: Optional[str | None] = None
    ):
        self.label = label
        self.raw_lines: List[str] = []
        self.lines: List[str] = []
        self._is_diff: bool = False
        self._pattern: str = ""
        self._snippet: str = ""

        self.prepare(line1, line2, *other_lines)
        self.process()

    def __bool__(self): return True if self._pattern else False

    def __len__(self) -> int: return int(bool(self._pattern))

    @property
    def is_diff(self) -> bool: return self._is_diff

    @property
    def pattern(self) -> str:
        """
        str: Full regex pattern including leading/trailing whitespace.
        """
        return f"{self.leading_whitespace}{self._pattern}{self.trailing_whitespace}"

    @property
    def snippet(self) -> str:
        """Snippet representation of the line comparison."""
        if not self.is_diff:
            return self._snippet

        leading_snippet, trailing_snippet = "start()", "end()"
        whitespace_type = "whitespace" if self.whitespace == r"\s" else "space"

        if self.is_leading:
            new_ws = f"{whitespace_type}s" if self.are_leading else whitespace_type
            leading_snippet = leading_snippet.replace("()", f"({new_ws})")

        if self.is_trailing:
            new_ws = f"{whitespace_type}s" if self.are_trailing else whitespace_type
            trailing_snippet = trailing_snippet.replace("()", f"({new_ws})")

        # Regex to detect existing start/end markers in the snippet
        pattern = r"^(start\(\w*\)) (?P<snippet>.+) (end\(\w*\))$"
        match = re.match(pattern, self._snippet)

        if match:
            snippet_body = match.group("snippet")
            return f"{leading_snippet} {snippet_body} {trailing_snippet}"

        return f"{leading_snippet} {self._snippet} {trailing_snippet}"

    @property
    def are_leading(self) -> bool:
        """Whether all lines have leading whitespace."""
        return all(text.Line.has_leading(line) for line in self.raw_lines)

    @property
    def are_trailing(self) -> bool:
        """Whether all lines have trailing whitespace."""
        return all(text.Line.has_trailing(line) for line in self.raw_lines)

    @property
    def is_leading(self) -> bool:
        """Whether any line has leading whitespace."""
        return any(text.Line.has_leading(line) for line in self.raw_lines)

    @property
    def is_trailing(self) -> bool:
        """Whether any line has trailing whitespace."""
        return any(text.Line.has_trailing(line) for line in self.raw_lines)

    @property
    def is_whitespace_in_line(self) -> bool:
        """Whether any line contains whitespace."""
        return any(text.Line.has_whitespace_in_line(line) for line in self.raw_lines)

    @property
    def whitespace(self) -> str:
        """Regex pattern for whitespace handling."""
        return PATTERN.WS if self.is_whitespace_in_line else PATTERN.SPACE

    @property
    def leading_whitespace(self) -> str:
        """Regex fragment for leading whitespace."""
        multi = "+" if self.are_leading else "*"
        return f"{self.whitespace}{multi}" if self.is_leading else ""

    @property
    def trailing_whitespace(self) -> str:
        """Regex fragment for trailing whitespace."""
        multi = "+" if self.are_trailing else "*"
        return f"{self.whitespace}{multi}" if self.is_trailing else ""

    def reset(self) -> None:
        """Reset the internal state of the object."""
        self.lines.clear()
        self._pattern = ""

    def prepare(self, line1: str, line2: str, *other_lines: str) -> None:
        """Prepare and normalize input lines for diff analysis."""
        lst = [line1, line2] + list(other_lines)

        raw_lines: List[str] = []
        lines: List[str] = []

        for line in lst:
            trim_line = line.strip()
            if trim_line:
                if line not in raw_lines:
                    raw_lines.append(line)
                if trim_line not in lines:
                    lines.append(trim_line)

        if len(lines) < 2:
            fmt = "Cannot form pattern: fewer than two lines provided.\n%s"
            details = [f"Line 1: {line1!r}", f"Line 2: {line2!r}"]
            if other_lines:
                details.append(f"Other Lines: {other_lines!r}")
            self.raise_runtime_error(msg=fmt % "\n".join(details))
        else:
            self.reset()
            self.lines.extend(lines)
            self.raw_lines.extend(raw_lines)

    def get_pattern_btw_two_lines(
        self, line_a: str, line_b: str,
        is_root: bool = False
    ) -> str:
        """Generate a regex pattern between two lines."""
        diff_line_obj = NDiffLinePattern(
            line_a,
            line_b,
            label=self.label,
            whitespace=f"{self.whitespace}",
            is_root=is_root,
        )
        self._is_diff = diff_line_obj.is_diff
        return diff_line_obj.pattern

    def get_snippet_btw_two_lines(
        self, line_a: str, line_b: str,
        is_root: bool = False
    ) -> str:
        """Generate a snippet representation between two lines."""
        diff_line_obj = NDiffLinePattern(
            line_a,
            line_b,
            label=self.label,
            whitespace=f"{self.whitespace}",
            is_root=is_root,
        )
        return diff_line_obj.snippet

    def is_matched_all(self, pattern: str) -> bool:
        """Check whether all stored lines fully match a given regex pattern."""
        for line in self.lines:
            match = re.match(pattern, line)
            if not match or match.group() != line:
                return False
        return True

    def reconstruct_pattern_and_snippet(self) -> None:
        """
        Reconstruct the regex pattern and snippet representation
        from the stored lines.
        """
        lst: List[object] = []

        for line in self.lines:
            # Match the current pattern against the line
            match = re.match(self._pattern, line)
            if not match:
                continue

            other_lst = ["(?P<c0>.*)"]
            key = ""

            # Build generic pattern with escaped values and extra captures
            for key, val in match.groupdict().items():
                val = re.escape(val)
                other_lst.append(f"(?P<{key}>{val})")
                other_lst.append(f"(?P<c{key}>.+)")
            else:
                # Replace last greedy capture with a non-greedy fallback
                if other_lst:
                    other_lst.pop()
                other_lst.append(f"(?P<c{key}>.*)")

            generic_pattern = text.join_string(*other_lst)
            match = re.match(generic_pattern, line)

            if lst:
                # Merge values into existing nodes
                for index, (key, val) in enumerate(match.groupdict().items()):
                    lst[index].add(val)     # noqa
            else:
                # Initialize nodes from first match
                for key, val in match.groupdict().items():
                    if key.startswith("c"):
                        lst.append(DText(val))
                    else:
                        lst.append(DChange(val, var=key))

        # Build snippet string from nodes
        snippet = "".join(item.get_snippet() for item in lst)       # noqa
        self._snippet = f"start() {snippet} end()"
        self._pattern = LinePattern(snippet)

    def process(self) -> None:
        """
        Attempt to construct a regex pattern and snippet from the stored lines.

        This method compares all pairs of lines in `self.lines` and tries
        to generate a valid pattern/snippet using three passes:

        1. **First pass**: Standard pattern generation.
        2. **Second pass**: Pattern generation with `is_root=True`.

        If a matching pattern is found that validates against all lines,
        the internal `_pattern` and `_snippet` are updated and
        `reconstruct_pattern_and_snippet()` is called. If no valid pattern
        is found after all passes, a `RuntimeException` is raised.

        Raises
        ------
        RuntimeException
            If no constructed pattern matches all lines.
        """
        lines_count = len(self.lines)
        pairs = list(combinations(range(lines_count), 2))
        attempted_patterns: List[str] = []

        def try_pass(**kwargs) -> bool:
            """Helper to attempt pattern/snippet generation with given flags."""
            for i, j in pairs:
                line_a, line_b = self.lines[i], self.lines[j]
                pattern = self.get_pattern_btw_two_lines(line_a, line_b,
                                                         **kwargs)
                snippet = self.get_snippet_btw_two_lines(line_a, line_b,
                                                         **kwargs)
                attempted_patterns.append(pattern)

                if self.is_matched_all(pattern):
                    self._pattern = pattern
                    self._snippet = snippet
                    self.reconstruct_pattern_and_snippet()
                    return True
            return False

        # Try passes in order: default, root
        if try_pass():
            return
        if try_pass(is_root=True):
            return

        # If all passes failed, raise error with diagnostic info
        fmt = "Failed to build a matching pattern. Attempted patterns:\n  %s"
        details = "\n  ".join(repr(item) for item in attempted_patterns)
        self.raise_runtime_error(msg=fmt % details)


class CommonDiffLinePattern(RuntimeException):
    """
    Represents a common diff pattern across multiple lines.
    """

    def __init__(self, *lines: str, label: Optional[str | None] = None):
        self.raw_lines: Tuple[str, ...] = lines
        self.lines: List[str] = [line.strip() for line in lines if line.strip()]
        self.label: Optional[str | None] = label
        self._is_diff: bool = False
        self._pattern: str = ""
        self._snippet: str = ""
        self.process()

    @property
    def are_leading(self) -> bool:
        """Whether all lines have leading whitespace."""
        return all(text.Line.has_leading(line) for line in self.raw_lines)

    @property
    def are_trailing(self) -> bool:
        """Whether all lines have trailing whitespace."""
        return all(text.Line.has_trailing(line) for line in self.raw_lines)

    @property
    def is_leading(self) -> bool:
        """Whether any line has leading whitespace."""
        return any(text.Line.has_leading(line) for line in self.raw_lines)

    @property
    def is_trailing(self) -> bool:
        """Whether any line has trailing whitespace."""
        return any(text.Line.has_trailing(line) for line in self.raw_lines)

    @property
    def is_whitespace_in_line(self) -> bool:
        """Whether any line contains whitespace."""
        return any(text.Line.has_whitespace_in_line(line) for line in self.raw_lines)

    @property
    def whitespace(self) -> str:
        """Regex pattern for whitespace handling."""
        return PATTERN.WS if self.is_whitespace_in_line else PATTERN.SPACE

    @property
    def leading_whitespace(self) -> str:
        """Regex fragment for leading whitespace."""
        multi = "+" if self.are_leading else "*"
        return f"{self.whitespace}{multi}" if self.is_leading else ""

    @property
    def trailing_whitespace(self) -> str:
        """Regex fragment for trailing whitespace."""
        multi = "+" if self.are_trailing else "*"
        return f"{self.whitespace}{multi}" if self.is_trailing else ""

    @property
    def has_data(self) -> bool:
        """Whether the class contains any non-empty lines."""
        return len(self.lines) > 0

    @property
    def are_identical_lines(self) -> bool:
        """Whether all normalized lines are identical."""
        if not self.has_data:
            return False
        normalized = [re.sub(PATTERN.WSS, "", line) for line in self.lines]
        return len(set(normalized)) == 1

    @property
    def is_diff(self) -> bool:
        """Whether the lines differ."""
        return self._is_diff

    @property
    def pattern(self) -> str:
        """Regex pattern representing the lines."""
        return self._pattern

    @property
    def snippet(self) -> str:
        """Snippet representation of the lines."""
        return self._snippet

    def get_common_pattern(self) -> str:
        """Construct a common regex pattern across identical lines."""
        if not self.are_identical_lines:
            return ""

        # Case 1: All lines are exactly identical
        if len(set(self.lines)) == 1:
            pattern = TextPattern(self.lines[0])
            return f"{self.leading_whitespace}{pattern}{self.trailing_whitespace}"

        # Case 2: Lines are structurally identical but differ in whitespace
        lst_of_groups = list(
            zip(*[text.Text(line).do_finditer_split(r"\S+") for line in self.lines]))
        result: List[str] = []

        for grp in lst_of_groups[1:-1]:
            if len(set(grp)) == 1:
                result.append(TextPattern(grp[0]))
            else:
                is_space_only = re.match(r" +$", "".join(grp))
                result.append(
                    PATTERN.SPACES if is_space_only else PATTERN.WSS)

        pattern = "".join(result)
        return f"{self.leading_whitespace}{pattern}{self.trailing_whitespace}"

    def get_common_snippet(self) -> str:
        """Construct a common snippet across identical lines."""
        if not self.are_identical_lines:
            return ""

        tbl = {
            " +": "(spaces)",
            " *": "(space)",
            r"\s+": "(whitespaces)",
            r"\s*": "(whitespace)",
            "": "",
        }

        case = tbl.get(self.leading_whitespace)
        leading_snippet = f"start({case})" if self.is_leading else ""
        case = tbl.get(self.trailing_whitespace)
        trailing_snippet = f"end({case})" if self.is_trailing else ""

        # Case 1: All lines are exactly identical
        if len(set(self.lines)) == 1:
            snippet = self.lines[0]
            return f"{leading_snippet} {snippet} {trailing_snippet}".strip()

        # Case 2: Lines are structurally identical but differ in whitespace
        lst_of_groups = list(
            zip(*[text.Text(line).do_finditer_split(r"\S+") for line in self.lines]))
        result: List[str] = []

        for grp in lst_of_groups[1:-1]:
            if len(set(grp)) == 1:
                result.append(TextPattern(grp[0]))
            else:
                result.append(list(set(grp))[-1])

        snippet = "".join(result)
        return f"{self.leading_whitespace}{snippet}{self.trailing_whitespace}"

    def process(self) -> None:
        """Process the stored lines to generate a pattern and snippet."""
        if not self.has_data:
            return

        if self.are_identical_lines:
            self._pattern = self.get_common_pattern()
            self._snippet = self.get_common_snippet()
        else:
            node = DiffLineTranslator(*self.lines, label=self.label)
            self._is_diff = node.is_diff
            self._pattern = node.pattern
            self._snippet = node.snippet

            if node.is_diff:
                tbl = {
                    " +": "spaces",
                    " *": "space",
                    r"\s+": "whitespaces",
                    r"\s*": "whitespace",
                    "": "",
                }
                case = tbl.get(self.leading_whitespace)
                leading_snippet = f"start({case})" if self.is_leading else ""
                case = tbl.get(self.trailing_whitespace)
                trailing_snippet = f"end({case})" if self.is_trailing else ""

                # Update pattern and snippet with whitespace context
                self._pattern = f"{self.leading_whitespace}{self._pattern}{self.trailing_whitespace}"
                if leading_snippet:
                    self._snippet = self._snippet.replace("start()",
                                                          leading_snippet)
                if trailing_snippet:
                    self._snippet = self._snippet.replace("end()",
                                                          trailing_snippet)


class DText:
    """
    Represents a diff text node for unchanged fragments.
    """

    def __init__(self, txt: str):
        self.lst: List[str] = [txt]
        self.leading_lst: List[str] = []
        self.trailing_lst: List[str] = []
        self.text: str = txt

        leading = text.Line.get_leading(txt)
        if leading:
            self.leading_lst.append(leading)

        trailing = text.Line.get_trailing(txt)
        if trailing:
            if txt.strip():
                self.trailing_lst.append(trailing)

    @property
    def leading(self) -> str:
        """Leading whitespace fragment."""
        if not self.leading_lst:
            return ""

        if len(set(self.leading_lst)) == 1:
            return self.leading_lst[0]

        ws = " "
        for item in self.leading_lst:
            if item.strip(" "):
                ws = item.strip(" ")
                break
        is_multi = any(len(item) > 1 for item in self.leading_lst)
        return f"{ws} " if is_multi else ws

    @property
    def trailing(self) -> str:
        """Trailing whitespace fragment."""
        if not self.trailing_lst:
            return ""

        if len(set(self.trailing_lst)) == 1:
            return self.trailing_lst[0]

        ws = " "
        for item in self.trailing_lst:
            if item.strip(" "):
                ws = item.strip(" ")
                break
        is_multi = any(len(item) > 1 for item in self.trailing_lst)
        return f"{ws} " if is_multi else ws

    @property
    def first_text(self) -> str:
        """First text fragment."""
        return self.lst[0] if self.lst else ""

    @property
    def is_identical(self) -> bool:
        """Whether all text fragments are identical."""
        return len(set(self.lst)) == 1

    @property
    def is_closed_to_identical(self) -> bool:
        """Whether all text fragments are identical after trimming."""
        clean_lst = [item.strip() for item in self.lst if item.strip()]
        return len(set(clean_lst)) == 1

    def concatenate(self, txt: str) -> None:
        """Concatenate text to the last fragment."""
        if self.lst:
            self.lst[-1] = self.lst[-1] + txt
        else:
            self.lst.append(txt)

    def add(self, txt: str) -> None:
        """Add a new text fragment."""
        self.lst.append(txt)

    def to_group(self) -> List[List[str]]:
        """Group text fragments by whitespace splits."""
        lst: List[List[str]] = []
        for line in self.lst:
            line = line.strip()
            if line:
                sub_lst = text.Text(line).do_finditer_split(PATTERN.WSS)
                lst.append(sub_lst)

        group = list(zip(*lst))
        return [sorted(set(sub_grp)) for sub_grp in group]

    def to_general_text(self) -> str:
        """Generate generalized text representation."""
        result: List[str] = []
        group = self.to_group()     # noqa

        for sub_grp in group:
            if len(sub_grp) == 1:
                result.append(sub_grp[0])
            else:
                ws = " "
                for item in sub_grp:
                    if item.strip(" "):
                        ws = item.strip(" ")
                        break
                is_multi = any(len(item) > 1 for item in sub_grp)
                spacer = f"{ws} " if is_multi else ws
                result.append(spacer)

        return self.leading + "".join(result) + self.trailing

    def get_pattern(self) -> "TextPattern":
        """Generate regex pattern for the text node."""
        if self.is_identical:
            return TextPattern(self.first_text)
        elif self.is_closed_to_identical:
            clean_lst = [item.strip() for item in self.lst if item.strip()]
            txt = clean_lst[0]
            return TextPattern(self.leading + txt + self.trailing)
        return TextPattern(self.to_general_text())

    def get_snippet(self) -> str:
        """Generate snippet representation for the text node."""
        if self.is_identical:
            return self.first_text
        elif self.is_closed_to_identical:
            clean_lst = [item.strip() for item in self.lst if item.strip()]
            txt = clean_lst[0]
            return self.leading + txt + self.trailing
        return self.to_general_text()


class DChange:
    """
    Represents a diff change node for text fragments.
    """

    def __init__(self, txt: str, var: str):
        self.var: str = var
        self.lst: List[str] = []
        self.text: str = txt
        self.is_empty: bool = txt.strip() == ""

        if not self.is_empty:
            self.lst.append(txt)

    def add(self, txt: str):
        """Add a new text fragment to the change node."""
        if txt.strip() == "":
            self.is_empty = True
        elif txt not in self.lst:
            self.lst.append(txt)

    def get_pattern(self) -> "ElementPattern":
        """Generate a regex pattern for the change node."""
        snippet = self.get_snippet()
        return ElementPattern(snippet)

    def get_snippet(self) -> str:
        """Generate a snippet representation of the change node."""
        factory = PatternTranslator.do_factory_create(*self.lst)
        snippet = factory.get_template_snippet(var=self.var)

        if self.is_empty:
            snippet = f"{snippet[:-1]}, or_empty)"

        return snippet
