#!/usr/bin/env python3
"""Simple delimiter and quote balance checker."""
# Note: This helper is intended for on-demand use and is not wired into CI workflows.
from __future__ import annotations

import argparse
import pathlib
import re
from dataclasses import dataclass
from typing import Iterable, List, Optional, Sequence, Tuple


TARGET_SUFFIXES = {
    ".bat",
    ".cmd",
    ".ps1",
    ".py",
    ".yml",
    ".yaml",
    ".json",
}

# derived requirement (CodeRabbit review, PR #449): cmd.exe treats a TAB exactly like a
# space as the word separator after "rem" -- `rem\tsomething` is just as much a real
# comment as `rem something`. A literal `"REM "` (space only) prefix check misses it,
# silently leaving such a line's parens untracked by the cross-line-paren hazard check.
# Shared by BOTH .bat/.cmd rem-detection call sites in this file so they cannot drift
# apart -- do not inline a second, differently-spelled check.
REM_LINE_RE = re.compile(r"rem(?:[ \t]|$)", re.IGNORECASE)

# derived requirement (CLAUDE.md Item 61, found while fixing the same-line-paren gap
# below): a line like `>> "%LOG%" echo unexpected internal error (exit 3); ...` -- the
# EXACT real shape that broke real cmd.exe parsing in PR #445 -- does not start with
# "echo" after lstrip(), so the plain `echo\b` match below never recognized it as an
# echo line, meaning its own paren pair was never tracked as prose at all. Matches an
# optional leading redirection clause (`>`/`>>`, an optional file descriptor digit, a
# quoted or bare target) before "echo", so a redirected echo statement is detected the
# same way a plain one already is. Also matches an optional leading `@` (CodeRabbit
# review, PR #464): `@echo` -- command-echo suppressed, cmd.exe's own well-documented
# convention, used at this file's own top of file -- is still the echo command as far
# as this hazard is concerned; missing it would leave a nested `@echo` line's own
# prose parens untracked the same way the redirected-echo gap above did.
ECHO_LINE_RE = re.compile(r'@?\s*(?:\d*>>?\s*(?:"[^"]*"|\S+)\s+)?echo\b', re.IGNORECASE)


@dataclass
class Issue:
    path: pathlib.Path
    line: int
    column: int
    message: str

    def format(self) -> str:
        return f"{self.path}:{self.line}:{self.column}: {self.message}"


@dataclass
class StackItem:
    char: str
    line: int
    column: int
    # None = not a hazardous echo/rem prose paren; "echo" / "rem" = which command's
    # text it was opened on AND a genuine structural bracket was already open at that
    # point (see the cross-line-close check in pop() below). Distinct from is_prose
    # below -- prose_kind is the "should this be flagged" verdict, is_prose is the
    # raw "was this opened on echo/rem prose text at all" fact used to compute it.
    prose_kind: Optional[str] = None
    # derived requirement (CodeRabbit review, PR #464): purely line-based -- True
    # whenever this bracket was opened on an echo/rem line, regardless of what else
    # is on the stack. Needed because prose_kind=None is otherwise ambiguous between
    # "this is a genuine structural if/for bracket" and "this is a top-level prose
    # paren judged not hazardous" -- see the push-site comment for why that ambiguity
    # produced a real false positive.
    is_prose: bool = False


class LineCursor:
    def __init__(self, line: str, number: int) -> None:
        self.line = line
        self.number = number
        self.index = 0

    def remaining(self) -> str:
        return self.line[self.index :]

    def advance(self, count: int = 1) -> None:
        self.index += count

    def current(self) -> Optional[str]:
        if self.index >= len(self.line):
            return None
        return self.line[self.index]

    def column(self) -> int:
        return self.index + 1


# derived requirement (CodeRabbit review, PR #470): a PowerShell DOUBLE-quoted string
# interpolates an embedded $variable reference at runtime -- "$IsWindows" or
# "$script:someVar" inside a "..." string is a genuine LIVE reference, not inert text,
# unlike a single-quoted string (PowerShell never interpolates those, no exceptions).
# Stripping the variable token along with the rest of the quoted text (the original
# behavior) silently hid this class of live reference from find_live_ps1_matches --
# sanitize_ps1_line now preserves just the variable token itself (bare $name, an
# optional :scope-style suffix, or braced ${name}) into the sanitized output/mapping;
# everything else inside the string is still stripped exactly as before.
VAR_INTERP_RE = re.compile(r"\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_:]*")


def sanitize_ps1_line(line: str) -> Tuple[str, List[int]]:
    """Strip comments/strings for heuristic scanning while tracking raw indexes."""

    sanitized: List[str] = []
    mapping: List[int] = []
    quote: Optional[str] = None
    i = 0
    length = len(line)
    while i < length:
        ch = line[i]
        if quote:
            if quote == '"' and ch == '`' and i + 1 < length:
                i += 2
                continue
            if quote == '"' and ch == '$':
                match = VAR_INTERP_RE.match(line, i)
                if match:
                    for j in range(match.start(), match.end()):
                        sanitized.append(line[j])
                        mapping.append(j)
                    i = match.end()
                    continue
            if ch == quote:
                quote = None
            i += 1
            continue
        if ch in {'"', "'"}:
            quote = ch
            i += 1
            continue
        if ch == '#':
            break
        sanitized.append(ch)
        mapping.append(i)
        i += 1
    return ("".join(sanitized), mapping)


def find_live_ps1_matches(
    lines: Sequence[str], pattern: re.Pattern[str]
) -> Iterable[Tuple[int, int, str]]:
    """Yield (1-based line, 1-based column, matched text) for PATTERN matches in the LIVE
    (non-string, non-#-comment) portion of each PowerShell line.

    Built on sanitize_ps1_line's quote/comment stripping so a match sitting inside a quoted
    string literal is never reported, and a '#' that is itself inside a quoted string can
    never suppress a later live match on the same line -- both were real gaps in an earlier
    version of this scan that searched raw, un-sanitized line text directly (CodeRabbit
    review, PR #470). Does not track here-strings/block comments across lines -- callers
    scanning a whole file before the main per-line loop starts already accepted that
    limitation.
    """
    for line_no, raw_line in enumerate(lines, start=1):
        sanitized, mapping = sanitize_ps1_line(raw_line)
        for match in pattern.finditer(sanitized):
            raw_column = mapping[match.start()] + 1
            yield line_no, raw_column, match.group(0)


def iter_files(paths: Sequence[pathlib.Path]) -> Iterable[pathlib.Path]:
    for path in paths:
        if path.is_file():
            if path.suffix.lower() in TARGET_SUFFIXES:
                yield path
        elif path.is_dir():
            for sub in path.rglob("*"):
                if sub.is_file() and sub.suffix.lower() in TARGET_SUFFIXES:
                    # Skip files inside .git folders.
                    if any(part.startswith(".git") for part in sub.parts):
                        continue
                    yield sub


def is_python_triple_quote(line: str, idx: int, quote: str) -> bool:
    segment = line[idx : idx + 3]
    return segment == quote * 3


def count_preceding(line: str, idx: int, char: str) -> int:
    count = 0
    j = idx - 1
    while j >= 0 and line[j] == char:
        count += 1
        j -= 1
    return count


def yaml_is_doubled_quote(line: str, idx: int, quote: str) -> bool:
    # For YAML single/double quoted scalars, repeated quotes escape themselves.
    return idx + 1 < len(line) and line[idx + 1] == quote


class DelimiterChecker:
    def __init__(self, path: pathlib.Path) -> None:
        self.path = path
        self.issues: List[Issue] = []
        self.stack: List[StackItem] = []
        self.string_state: Optional[Tuple[str, bool, int, int]] = None
        self.here_string: Optional[str] = None
        self.in_block_comment = False
        self.prev_ps1_backtick = False
        # derived requirement: a PowerShell statement can also continue naturally (no backtick
        # needed) when a line ends in a trailing binary operator like -and/-or -- the next
        # physical line is still part of the SAME logical statement. Tracked alongside
        # prev_ps1_backtick so _check_ps1_boolean_operators can tell "is this line a
        # continuation of the previous one" regardless of which continuation style was used.
        self.ps1_natural_continues = False
        # Carried "was the logical statement this line belongs to already judged safe"
        # verdict -- True once an assignment ('='), a control keyword, an already-open
        # bracket, or a genuinely safe continuation established it; reset to a fresh
        # per-line computation whenever the line is NOT a continuation of the previous one.
        self.ps1_stmt_safe_context = False
        self.yaml_shell_by_indent: dict[int, str] = {}
        self.in_yaml_pwsh_block = False
        self.yaml_pwsh_block_indent = 0
        self.yaml_prev_pwsh_command = False
        self._bat_in_backtick = False

    def add_issue(self, line: int, column: int, message: str) -> None:
        self.issues.append(Issue(self.path, line, column, message))

    def push(
        self,
        char: str,
        line: int,
        column: int,
        prose_kind: Optional[str] = None,
        is_prose: bool = False,
    ) -> None:
        self.stack.append(StackItem(char, line, column, prose_kind, is_prose))

    def pop(self, expected: str, line: int, column: int, actual: str) -> None:
        if not self.stack:
            self.add_issue(line, column, f"Unexpected '{actual}' without matching opening")
            return
        last = self.stack.pop()
        if last.char != expected:
            self.add_issue(
                line,
                column,
                f"Mismatched '{actual}' (expected to close '{last.char}' from line {last.line}, column {last.column})",
            )
            return
        if last.char == "(" and last.prose_kind:
            # derived requirement: cmd.exe's parenthesized-block parser counts '(' / ')'
            # characters inside plain "echo"/"rem" text too -- it has no concept of "this
            # paren is just prose." Originally this check only fired for a CROSS-line split
            # (line != last.line) on the theory that a SAME-line, individually-balanced pair
            # was safe -- CLAUDE.md Item 61's own dedicated cmd.exe probe
            # (tools/probe_paren_hazard.ps1, dispatched on a real Windows runner via
            # .github/workflows/batch-paren-hazard-probe.yml, 2026-08-23) disproved that:
            # EVERY same-line matrix fixture corrupted ("X was unexpected at this time."),
            # including the shallowest case tested -- a plain, non-redirected pair nested
            # just ONE level inside a single if(...) block, with no ">>" redirection
            # involved. Nesting depth and redirection do not matter; only whether the pair
            # is nested inside a real open bracket at all does. See docs/agent-lessons-
            # learned.md's "A literal (/) inside echo text is NOT invisible..." entry for
            # the full incident history (PR #408, PR #445) and the probe's own confirmation.
            same_line = line == last.line
            where = "on this same line" if same_line else f"does not close until line {line}"
            self.add_issue(
                last.line,
                last.column,
                f"Batch: '(' opened on this '{last.prose_kind}' line {where}; cmd.exe's "
                f"parenthesized-block parser counts parens in {last.prose_kind} text too, "
                "so a pair nested inside an enclosing if/for block can corrupt that block's "
                "structure -- confirmed on real cmd.exe even for a same-line, individually-"
                "balanced pair nested only one level deep. Keep parens like this OUTSIDE "
                "any if/for block, avoid literal parens in wrapped prose (prefer ' -- ' or "
                "','), or escape both as '^(' / '^)' if they are structurally necessary.",
            )

    def check(self) -> List[Issue]:
        try:
            text = self.path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            text = self.path.read_text(encoding="utf-8", errors="replace")

        lines = text.splitlines()
        lower_suffix = self.path.suffix.lower()
        if lower_suffix == ".ps1":
            # derived requirement (CodeRabbit review, PR #470): both scans below use
            # find_live_ps1_matches (built on sanitize_ps1_line) rather than a whole-text
            # regex, so a match sitting inside a quoted string literal (e.g.
            # `Write-Host '$IsWindows'`) is never reported, and a '#' that is itself inside a
            # quoted string earlier on the same line can never suppress a later genuine live
            # match -- both were real gaps in an earlier version of these two checks that
            # searched raw `text` directly and approximated comment-skipping with a bare
            # `line_text.find("#")`.
            bad = re.compile(r"\$[A-Za-z_][A-Za-z0-9_]*:")
            allow = re.compile(r"\$(?:script|global|local|private|env|using):", re.IGNORECASE)
            for line_no, column, token in find_live_ps1_matches(lines, bad):
                if allow.fullmatch(token):
                    continue
                # derived requirement: catching `$var:` early prevents the Windows PowerShell parser
                # from treating it as a scoped lookup and crashing the gate pipeline again.
                self.add_issue(
                    line_no,
                    column,
                    f'PowerShell scoped variable token "{token}" (wrap with ${{...}} or use -f formatting)',
                )

            # derived requirement (this exact bug independently rediscovered and fixed one file
            # at a time across at least 4 separate PRs -- #434, #436, and others -- before this
            # check existed, per docs/agent-lessons-learned.md's "$IsWindows undefined under
            # Windows PowerShell 5.1" entry): $IsWindows is a PowerShell 6+ automatic variable,
            # undefined (reads as $null, which is falsy) under Windows PowerShell 5.1 -- real
            # CI dispatches these scripts via pwsh (where it IS defined), but a maintainer's own
            # local double-click/console session defaults to powershell.exe, where "-not
            # $IsWindows" silently reads true and skips real Windows execution. This repo has
            # zero legitimate uses of the bare $IsWindows token; every occurrence found so far
            # has been this exact bug. Blunt on purpose (no attempt to distinguish a
            # version-guarded reference from a bare one) -- flag any live, non-commented
            # reference and point at the one correct replacement.
            #
            # derived requirement (CodeRabbit review, PR #470, third round): PowerShell variable
            # names are case-insensitive ($ISWINDOWS/$iswindows are the SAME variable as
            # $IsWindows) and a braced reference (${IsWindows}) is equally live, valid PowerShell
            # syntax anywhere a bare variable reference is -- not only inside an interpolated
            # string. re.IGNORECASE plus an explicit braced alternative closes both gaps; the
            # trailing \b on the bare form still rejects a longer identifier like
            # "IsWindowsFoo", and the braced form's own closing "}" already bounds the match
            # without needing \b (which would incorrectly require a following word character).
            iswindows_re = re.compile(r"\$(?:IsWindows\b|\{IsWindows\})", re.IGNORECASE)
            for line_no, column, _token in find_live_ps1_matches(lines, iswindows_re):
                self.add_issue(
                    line_no,
                    column,
                    "PowerShell automatic variable $IsWindows is undefined under Windows "
                    "PowerShell 5.1 (a maintainer's local powershell.exe session, not real CI's "
                    "pwsh dispatch) -- an undefined variable is $null, which is falsy, so "
                    "'-not $IsWindows' silently reads as true and skips real Windows execution. "
                    "Use [System.Environment]::OSVersion.Platform -ne "
                    "[System.PlatformID]::Win32NT instead.",
                )

        for line_no, raw_line in enumerate(lines, start=1):
            line = raw_line.rstrip("\n\r")

            if lower_suffix in {".yml", ".yaml"}:
                self._check_yaml_pwsh_block(line_no, line)

            if lower_suffix == ".ps1":
                self._check_ps1_boolean_operators(line_no, line)

            if self.here_string:
                terminator = self.here_string
                stripped_terminator = line.strip()
                if stripped_terminator.startswith(terminator):
                    remainder = stripped_terminator[len(terminator) :]
                    if not remainder or remainder[0].isspace() or remainder.startswith('-'):
                        self.here_string = None
                continue

            cursor = LineCursor(line, line_no)

            if self.in_block_comment:
                idx = line.find("#>")
                if idx == -1:
                    continue
                self.in_block_comment = False
                cursor.advance(idx + 2)

            stripped = line.lstrip()
            is_bat_echo_line = False
            is_bat_rem_line = False
            if lower_suffix in {".bat", ".cmd"}:
                if stripped.startswith("::"):
                    continue
                if REM_LINE_RE.match(stripped):
                    # derived requirement (CLAUDE.md Item 61, PR #445 Item 52 incident): a
                    # "rem" line is NOT opaque to cmd.exe's own parenthesized-block parser --
                    # it counts '(' / ')' characters inside rem comment text exactly the same
                    # way it does inside echo text (see the cross-line-close check in pop()
                    # above). Route rem lines through the same character scan echo lines
                    # already get instead of skipping them outright, so a cross-line paren
                    # pair inside rem prose, nested inside a real enclosing if/for block, is
                    # caught the same way an echo-text one already is. "::" stays fully
                    # skipped -- a real label token, not prose text, and out of this item's
                    # scope.
                    is_bat_rem_line = True
                else:
                    # derived requirement: matches "echo", "echo.", "echo(", "echo message",
                    # a redirected form like '>> "%LOG%" echo ...', and "@echo" -- anything
                    # cmd.exe itself treats as the echo command -- but not "echofoo".
                    is_bat_echo_line = ECHO_LINE_RE.match(stripped) is not None

            while True:
                ch = cursor.current()
                if ch is None:
                    break

                if self.string_state:
                    quote, triple, _, _ = self.string_state
                    if triple:
                        segment = cursor.remaining()
                        if segment.startswith(quote * 3):
                            self.string_state = None
                            cursor.advance(3)
                            continue
                        else:
                            cursor.advance()
                            continue
                    else:
                        escape = None
                        if lower_suffix in {".py", ".json"}:
                            escape = "\\"
                        elif lower_suffix in {".bat", ".cmd"}:
                            escape = "^"
                        elif lower_suffix == ".ps1" and quote == '"':
                            escape = "`"
                        if (
                            ch == quote
                            and lower_suffix in {".yml", ".yaml"}
                            and quote == "'"
                            and yaml_is_doubled_quote(line, cursor.index, "'")
                        ):
                            cursor.advance(2)
                            continue
                        if (
                            ch == quote
                            and lower_suffix == ".ps1"
                            and quote == "'"
                            and yaml_is_doubled_quote(line, cursor.index, "'")
                        ):
                            cursor.advance(2)
                            continue
                        if escape and count_preceding(line, cursor.index, escape) % 2 == 1:
                            cursor.advance()
                            continue
                        if ch == quote:
                            self.string_state = None
                            cursor.advance()
                            continue
                        cursor.advance()
                        continue

                # Not currently inside a string
                if lower_suffix in {".py", ".ps1", ".yml", ".yaml"} and ch == "#":
                    break

                if lower_suffix == ".ps1" and not self.in_block_comment:
                    segment = cursor.remaining()
                    if segment.startswith("<#"):
                        end = line.find("#>", cursor.index + 2)
                        if end == -1:
                            # Multiline comment: skip the remainder of this line
                            # and mark the parser as inside a block comment so
                            # subsequent lines get ignored until the terminator.
                            self.in_block_comment = True
                            break
                        # Comment closes on the same line; advance past the
                        # terminator so the rest of the line can be parsed.
                        cursor.advance(end - cursor.index + 2)
                        continue

                if lower_suffix == ".ps1":
                    trimmed = line.strip()
                    if trimmed.endswith("@\""):
                        self.here_string = '"@'
                        break
                    if trimmed.endswith("@'"):
                        self.here_string = "'@"
                        break

                if (
                    lower_suffix in {".bat", ".cmd"}
                    and ch in "(){}[]"
                    and count_preceding(line, cursor.index, "^") % 2 == 1
                ):
                    # derived requirement (found while extending the rem-line cross-line-paren
                    # check, CLAUDE.md Item 61): cmd.exe's own escape character ('^') in front
                    # of a bracket makes it a literal character there, not a real block
                    # delimiter -- and this repo's own established convention for defusing the
                    # cross-line-paren hazard is exactly to write it as '^(' / '^)' (see
                    # docs/agent-lessons-learned.md). Failing to recognize the escape here would
                    # make the checker flag the very construct that fixes the hazard -- confirmed
                    # directly: run_setup.bat's own file-header rem block (lines ~43-58) uses
                    # "CRLF ^)" / "^(no goto/call...breaks^)" style escaping extensively, and
                    # without this check every one of those was mis-tracked as a real,
                    # structurally significant bracket, corrupting the stack for the rest of the
                    # file. Applies to all four bracket characters (not just parens) and to every
                    # .bat/.cmd line (not only echo/rem text), since '^' escaping is general
                    # cmd.exe syntax, not a prose-specific convention.
                    cursor.advance()
                    continue

                if ch in "({[":
                    # derived requirement: only the case where this paren is ALREADY nested
                    # inside another open bracket (a real enclosing if/for block) is actually
                    # hazardous -- a top-level echo/rem line with a self-contained paren pair
                    # split across two otherwise-independent lines (no enclosing block for
                    # cmd.exe to misparse) is harmless, confirmed against real instances in
                    # this file (:print_fastpath_ambiguous_note for echo; a top-level rem
                    # header block for rem) that would otherwise false-flag.
                    #
                    # derived requirement (CodeRabbit review, PR #464, real bug -- confirmed
                    # via `echo outer (inner (detail))` at genuine top level): the ORIGINAL
                    # "already nested" test was `bool(self.stack)` -- true the moment ANY
                    # bracket is already open, including a PRIOR prose paren from this exact
                    # same echo/rem line's own text. That misclassified the line's own SECOND
                    # paren as hazardous even with no real enclosing if/for block anywhere.
                    # "Already nested inside a real block" must mean a genuine STRUCTURAL
                    # bracket (one not itself opened on echo/rem prose) is on the stack --
                    # not merely that the stack is non-empty. is_prose is a pure per-line
                    # fact (independent of stack state); a bracket is hazardous prose only
                    # when some ALREADY-OPEN stack item has is_prose=False.
                    is_prose = ch == "(" and (is_bat_echo_line or is_bat_rem_line)
                    nested_in_structural = any(not item.is_prose for item in self.stack)
                    prose_kind: Optional[str] = None
                    if is_prose and nested_in_structural:
                        prose_kind = "echo" if is_bat_echo_line else "rem"
                    self.push(ch, line_no, cursor.column(), prose_kind=prose_kind, is_prose=is_prose)
                    cursor.advance()
                    continue

                if ch in ")}]":
                    matching = {')': '(', ']': '[', '}': '{'}[ch]
                    self.pop(matching, line_no, cursor.column(), ch)
                    cursor.advance()
                    continue

                if lower_suffix in {".bat", ".cmd"} and ch in "'\"":
                    if ch == "'":
                        # derived requirement (found while extending the rem-line
                        # cross-line-paren check, CLAUDE.md Item 61): cmd.exe has no concept
                        # of a single-quote string delimiter at all -- only '"' is meaningful
                        # to it, in real code. The generic quote-tracking below previously
                        # treated a bare apostrophe in .bat/.cmd text as opening a string,
                        # which was harmless for the small amount of real .bat CODE this
                        # scanner used to see (code rarely contains a stray apostrophe) but
                        # corrupts everything once rem/echo PROSE routes through here too --
                        # an ordinary contraction like "doesn't" or a possessive like
                        # "user's" would silently swallow every character (including real
                        # parens) up to the NEXT apostrophe as fake "string content".
                        # Confirmed directly: run_setup.bat's own file-header rem block hits
                        # this via "gitattributes'", "GitHub's", "user's", "cmd.exe's", etc.
                        # Always inert, on every .bat/.cmd line -- matches real cmd.exe
                        # semantics, where a bare "'" is never special anywhere in the file.
                        cursor.advance()
                        continue
                    if is_bat_echo_line or is_bat_rem_line:
                        # derived requirement: unlike a real command line (where '"' groups
                        # an argument), an echo/rem line's text has no "quoted argument"
                        # concept at all to cmd.exe -- the whole remainder of the line is
                        # just text. Prose can legitimately contain an ODD count of '"'
                        # (documentation describing the quote character itself), which would
                        # otherwise open a persistent "string" that incorrectly swallows
                        # every following character -- including real parens on LATER lines
                        # -- until some unrelated, later '"' happens to "close" it. Confirmed
                        # directly: run_setup.bat's own rem text (line ~1036: "...(a literal
                        # \" would close the cmd-level quote)."). '"' stays fully meaningful
                        # on a real (non-prose) .bat/.cmd code line, e.g. `set "VAR=..."`.
                        cursor.advance()
                        continue

                if ch in "'\"":
                    triple = False
                    if lower_suffix == ".py" and is_python_triple_quote(line, cursor.index, ch):
                        triple = True
                        self.string_state = (ch, True, line_no, cursor.column())
                        cursor.advance(3)
                        continue
                    self.string_state = (ch, False, line_no, cursor.column())
                    cursor.advance()
                    continue

                cursor.advance()

        if self.string_state:
            quote, triple, line_no, col = self.string_state
            kind = "triple" if triple else "string"
            self.add_issue(line_no, col, f"Unterminated {kind} starting with {quote!r}")

        if self.here_string:
            self.add_issue(len(lines), max(1, len(lines[-1]) if lines else 1), f"Unterminated here-string expecting {self.here_string}")

        if self.stack:
            for item in self.stack:
                self.add_issue(item.line, item.column, f"Unclosed '{item.char}'")

        if lower_suffix in {".bat", ".cmd"}:
            self._bat_in_backtick = False
            for line_no, raw_line in enumerate(lines, start=1):
                line = raw_line.rstrip("\n\r")
                stripped = line.lstrip()
                if REM_LINE_RE.match(stripped) or stripped.startswith("::"):
                    continue
                scan_from = None
                if not self._bat_in_backtick:
                    if re.search(r"\bfor\s+/f\b", line, re.IGNORECASE):
                        if line.count("`") % 2 == 1:
                            self._bat_in_backtick = True
                            scan_from = line.index("`") + 1
                if self._bat_in_backtick:
                    segment = line[scan_from:] if scan_from is not None else line
                    self._check_bat_forloop_pipes(line_no, segment)
                    if scan_from is None and not line.rstrip().endswith("^"):
                        self._bat_in_backtick = False
                self._check_bat_ps_like_brackets(line_no, line)
                self._check_bat_set_quoting(line_no, line)
                self._check_bat_unquoted_path_var(line_no, line)
                self._check_bat_rem_comment_spacing(line_no, line)

        return self.issues

    def _check_bat_forloop_pipes(self, line_no: int, segment: str) -> None:
        """Flag unescaped '|' inside a for /f backtick block in .bat/.cmd files.

        CMD interprets '|' as a pipe before passing content to the subshell,
        even inside double-quoted strings. Bare '|' causes 'The syntax of the
        command is incorrect.' and the for /f yields no iterations.
        All such pipes must be written as '^|'.
        """
        i = 0
        length = len(segment)
        while i < length:
            ch = segment[i]
            if ch == "^":
                i += 2
                continue
            if ch == "|":
                self.add_issue(
                    line_no,
                    i + 1,
                    "Unescaped '|' inside for /f backtick block; CMD interprets '|' as a "
                    "pipe before the subshell runs, causing 'The syntax of the command is "
                    "incorrect.' Use '^|' instead.",
                )
            i += 1

    def _check_bat_ps_like_brackets(self, line_no: int, line: str) -> None:
        """Flag unmatched '[' inside PowerShell -like pattern strings embedded in .bat files.

        PS wildcard patterns treat '[' as a character-class opener (like regex). An
        unmatched '[' (no closing ']') causes the -like operator to silently return
        False or throw, depending on the PS version. This is invisible to the outer
        delimiter checker because the pattern sits inside a quoted batch string.
        """
        # derived requirement: the -like '#dependencies=*[' bug (unmatched bracket in
        # PS wildcard pattern inside a batch double-quoted string) was undetectable by the
        # standard delimiter pass. This targeted scan prevents the same class of regression.
        for match in re.finditer(r"-like\s+'([^']*)'", line, re.IGNORECASE):
            pattern = match.group(1)
            depth = 0
            for ch in pattern:
                if ch == "[":
                    depth += 1
                elif ch == "]":
                    depth -= 1
            if depth != 0:
                col = match.start(1) + 1
                self.add_issue(
                    line_no,
                    col,
                    f"Unmatched '[' in PowerShell -like pattern '{pattern}'; "
                    "PS wildcard treats '[' as a character-class opener -- "
                    "use .StartsWith()/.EndsWith() or escape as '`[' instead.",
                )

    def _check_bat_set_quoting(self, line_no: int, line: str) -> None:
        """Flag set assignments that store quotes inside the variable value.

        Correct: set "VAR=value"  -- quotes wrap the entire assignment
        Wrong:   set VAR="value"  -- quotes become part of the variable string
        """
        # derived requirement: set VAR="value" causes double-quoting on %VAR% expansion;
        # enforce set "VAR=value" uniformly. Skip /a and /p variants (different syntax).
        if re.match(
            r'^\s*set\s+(?!/[aApP]\b)(?!")\w+\s*=\s*"',
            line,
            re.IGNORECASE,
        ):
            col = line.lower().find("set") + 1
            self.add_issue(
                line_no,
                col,
                'Batch: use set "VAR=value" not set VAR="value"; '
                "quotes stored inside the variable cause double-quoting on expansion.",
            )

    def _check_bat_unquoted_path_var(self, line_no: int, line: str) -> None:
        """Flag path-variable references in file-system commands without double-quote wrapping."""
        # derived requirement: unquoted %VAR% in file-system commands breaks on paths with spaces.
        FS_UNQUOTED = re.compile(
            r'\b(?:del|if\s+(?:not\s+)?exist|mkdir|copy|move|pushd)\s+%[A-Za-z_]',
            re.IGNORECASE,
        )
        for match in FS_UNQUOTED.finditer(line):
            self.add_issue(
                line_no,
                match.start() + 1,
                'Batch: path variable appears unquoted in a file-system command; '
                'use "%VAR%" to handle paths containing spaces.',
            )

    def _check_bat_rem_comment_spacing(self, line_no: int, line: str) -> None:
        """Flag a line whose first token starts with "rem" but has no space after it.

        cmd.exe only treats "rem" as a comment when it is followed by whitespace
        (or is the entire line) -- "rem-nested" or "rem;foo" is parsed as an
        attempt to run a literal (almost always nonexistent) command named
        "rem-nested"/"rem;foo", NOT as a comment. This is easy to introduce by
        accident when a multi-line prose comment wraps a hyphenated (or otherwise
        glued-together) word across two "rem" lines.
        """
        # derived requirement: a real regression (caught only on real Windows CI,
        # not by any static check that existed at the time) split "already-nested"
        # across two rem lines, leaving the second line reading
        # "rem-nested failure path." cmd.exe printed
        # "'rem-nested' is not recognized as an internal or external command..."
        # and left a stray nonzero errorlevel sitting in front of the very next
        # "if errorlevel 1" check, silently corrupting its result. See
        # docs/agent-lessons-learned.md's "rem needs a space after it" entry for
        # the full trace. This check is intentionally blunt (no attempt to
        # distinguish "meant to be a comment" from "a real command that happens
        # to start with rem") -- no command in this codebase legitimately starts
        # with the bare letters "rem" glued to more characters, so a match here
        # is always worth a human's attention.
        stripped = line.lstrip()
        match = re.match(r"(rem)(\S)", stripped, re.IGNORECASE)
        if not match:
            return
        column = len(line) - len(stripped) + 1
        self.add_issue(
            line_no,
            column,
            f"Batch: \"{match.group(0)}...\" -- \"rem\" must be followed by whitespace "
            "(or be the entire line) to be treated as a comment; cmd.exe will instead "
            f"try to run \"{match.group(1)}{match.group(2)}...\" as a literal command, "
            "leaving a stray nonzero errorlevel behind. Add a space after \"rem\" if "
            "this was meant to be a comment.",
        )

    def _check_ps1_boolean_operators(self, line_no: int, line: str) -> None:
        # derived requirement: Windows runners surfaced "parameter name 'or'" faults whenever -or/-and sat
        # outside a boolean expression.
        # These heuristics stay intentionally simple per the latest CI spec; keep this comment to avoid
        # reintroducing syntax regressions when adjusting the PowerShell parsing rules.
        #
        # derived requirement (full-repo audit, PR #470-adjacent): the space_pattern branch below
        # ("appears without an enclosing if/elseif/while/for context") only ever looked at the
        # CURRENT physical line for an '=' or control keyword -- a real PowerShell statement can
        # legitimately span multiple physical lines (explicit backtick continuation, natural
        # continuation when a line ends in a trailing -and/-or, or simply being nested inside a
        # bracket opened on an earlier line: a hashtable literal, an if-expression's own {} branch,
        # a Where-Object scriptblock). All of those are safe, working, already-shipped code in this
        # repo; the checker just couldn't see far enough back to know it. is_continuation /
        # ps1_stmt_safe_context (set on DelimiterChecker, see __init__) carry the "was this
        # statement's context already established" verdict across such continuations; bracket_open
        # (len(self.stack) > 0, evaluated BEFORE this line's own brackets are pushed by the main
        # scanning loop below) covers the nested-inside-an-open-bracket case. This does not weaken
        # the ORIGINAL hazard this function exists to catch -- a bare command followed by -and/-or
        # being parsed as a parameter name -- since that is caught unconditionally by the separate
        # command_pattern loop further down, which does not depend on any of this.
        if self.here_string or self.in_block_comment:
            self.prev_ps1_backtick = False
            self.ps1_natural_continues = False
            return

        sanitized, index_map = sanitize_ps1_line(line)
        trimmed = sanitized.strip()
        if not trimmed:
            self.prev_ps1_backtick = False
            self.ps1_natural_continues = False
            return

        sanitized_lower = sanitized.lower()
        trimmed_lower = trimmed.lower()
        keyword_re = re.compile(r"\b(if|elseif|while|for|switch|return|until)\b", re.IGNORECASE)
        # Keep the assignment heuristic broad so any '=' counts as assignment; a prior pattern regressed
        # with an invalid character class, so this intentionally simple regex avoids compilation failures.
        assignment_re = re.compile(r"=")
        flagged: set[int] = set()

        def note_issue(op: str, sanitized_index: int, detail: str) -> None:
            if sanitized_index < 0 or sanitized_index >= len(index_map):
                return
            raw_index = index_map[sanitized_index]
            if raw_index in flagged:
                return
            flagged.add(raw_index)
            self.add_issue(
                line_no,
                raw_index + 1,
                f"PowerShell boolean operator '{op}' {detail}; wrap it inside an explicit conditional expression.",
            )

        # A leading -or/-and can only be valid PowerShell as an explicit backtick continuation of
        # the PREVIOUS line (natural trailing-operator continuation never produces a line that
        # itself starts with another operator) -- so only that specific case can be a carried-safe
        # continuation here.
        backtick_continuation_safe = self.prev_ps1_backtick and self.ps1_stmt_safe_context
        if trimmed_lower.startswith("-or") or trimmed_lower.startswith("-and"):
            if not backtick_continuation_safe:
                op = "-or" if trimmed_lower.startswith("-or") else "-and"
                idx = sanitized_lower.find(op)
                if idx != -1:
                    detail = "cannot begin a statement"
                    if self.prev_ps1_backtick:
                        detail = "cannot follow a line ending with '`'; add parentheses around the condition"
                    note_issue(op, idx, detail)

        has_assignment = bool(assignment_re.search(sanitized))
        has_control_keyword = bool(keyword_re.search(sanitized_lower))
        bracket_open = len(self.stack) > 0
        is_continuation = self.prev_ps1_backtick or self.ps1_natural_continues
        fresh_context = has_assignment or has_control_keyword or bracket_open
        effective_context = fresh_context or (is_continuation and self.ps1_stmt_safe_context)
        space_pattern = re.compile(r"\s-(or|and)\s", re.IGNORECASE)
        if not effective_context:
            for match in space_pattern.finditer(sanitized_lower):
                start_index = match.start()
                segment_before = sanitized[:start_index]
                if "{" in segment_before:
                    continue
                op = f"-{match.group(1).lower()}"
                note_issue(op, match.start() + 1, "appears without an enclosing if/elseif/while/for context")

        command_pattern = re.compile(r"-(or|and)\b", re.IGNORECASE)
        for match in command_pattern.finditer(sanitized_lower):
            op = f"-{match.group(1).lower()}"
            idx = match.start()
            if idx < 0 or idx >= len(index_map):
                continue
            before_segment = sanitized[:idx]
            stripped_before = before_segment.rstrip()
            if not stripped_before:
                continue

            if assignment_re.search(before_segment) or keyword_re.search(before_segment):
                continue

            command_before = False
            if "|" in stripped_before:
                command_before = True
            elif stripped_before.lstrip().startswith("&"):
                command_before = True
            else:
                tokens = stripped_before.split()
                if tokens:
                    last_token = tokens[-1]
                    if not last_token.endswith(")") and not last_token.startswith(("$", "!", "-", "{", "(", "[")):
                        if not keyword_re.search(last_token):
                            command_before = True

            if command_before:
                note_issue(op, idx, "appears after a command invocation; PowerShell treats it as a parameter")

        raw_trimmed = line.rstrip()
        self.prev_ps1_backtick = bool(trimmed) and raw_trimmed.endswith("`")
        # derived requirement: a line ending in a trailing binary operator (-and/-or) continues
        # naturally onto the next line with no backtick needed -- real, valid PowerShell syntax,
        # and the shape every trailing-operator false positive in this repo's own test suite used.
        self.ps1_natural_continues = bool(re.search(r"-(?:and|or)\b\s*$", trimmed_lower))
        self.ps1_stmt_safe_context = effective_context

    def _check_yaml_pwsh_block(self, line_no: int, line: str) -> None:
        if self.here_string:
            return

        raw = line
        content_no_comment = raw.split("#", 1)[0]
        indent = len(raw) - len(raw.lstrip(" "))

        if self.in_yaml_pwsh_block:
            block_indent = self.yaml_pwsh_block_indent
            if indent <= block_indent and content_no_comment.strip():
                self.in_yaml_pwsh_block = False
                self.yaml_prev_pwsh_command = False
            else:
                self._inspect_yaml_pwsh_content(line_no, raw, content_no_comment)
                return

        for depth in list(self.yaml_shell_by_indent.keys()):
            if depth > indent:
                del self.yaml_shell_by_indent[depth]

        stripped = content_no_comment.strip()
        if not stripped:
            return

        shell_match = re.match(r"^(\s*)shell\s*:\s*([^\s]+)", content_no_comment, re.IGNORECASE)
        if shell_match:
            shell_indent = len(shell_match.group(1))
            shell_value = shell_match.group(2).strip().lower()
            self.yaml_shell_by_indent[shell_indent] = shell_value
            return

        run_match = re.match(r"^(\s*)run\s*:\s*\|[-+]?\s*$", content_no_comment, re.IGNORECASE)
        if run_match:
            run_indent = len(run_match.group(1))
            shell_value = self.yaml_shell_by_indent.get(run_indent, "")
            shell_value = shell_value.lower()
            if shell_value in {"pwsh", "powershell"}:
                self.in_yaml_pwsh_block = True
                self.yaml_pwsh_block_indent = run_indent
                self.yaml_prev_pwsh_command = False
            else:
                self.in_yaml_pwsh_block = False
                self.yaml_prev_pwsh_command = False
            return

    def _inspect_yaml_pwsh_content(self, line_no: int, raw: str, content_no_comment: str) -> None:
        block_indent = self.yaml_pwsh_block_indent
        segment = content_no_comment[block_indent + 1 :] if len(content_no_comment) > block_indent else ""
        trimmed = segment.lstrip()
        if not trimmed:
            self.yaml_prev_pwsh_command = False
            return

        lower = trimmed.lower()
        if lower.startswith("-or") or lower.startswith("-and"):
            op = "-or" if lower.startswith("-or") else "-and"
            op_start = raw.lower().find(op, block_indent)
            if op_start != -1:
                detail = "cannot begin a continued pwsh run line"
                if self.yaml_prev_pwsh_command:
                    detail = "cannot follow a command invocation on the previous line"
                self.add_issue(
                    line_no,
                    op_start + 1,
                    f"PowerShell boolean operator '{op}' {detail}; wrap it inside an explicit expression.",
                )
            self.yaml_prev_pwsh_command = False
            return

        if lower.endswith("-or") or lower.endswith("-and"):
            op = "-or" if lower.endswith("-or") else "-and"
            op_start = raw.lower().rfind(op)
            if op_start != -1:
                self.add_issue(
                    line_no,
                    op_start + 1,
                    f"PowerShell boolean operator '{op}' cannot terminate a pwsh run line; complete the expression before wrapping.",
                )

        begins_command = trimmed.startswith("&") or trimmed.startswith("./") or trimmed.startswith(".\\")
        self.yaml_prev_pwsh_command = begins_command


def main(argv: Optional[Sequence[str]] = None) -> int:
    parser = argparse.ArgumentParser(description="Validate paired delimiters and quotes in text files.")
    parser.add_argument("paths", nargs="*", default=["."], help="Files or directories to scan")
    args = parser.parse_args(argv)

    base_paths = [pathlib.Path(p) for p in args.paths]
    issues: List[Issue] = []
    for file_path in iter_files(base_paths):
        checker = DelimiterChecker(file_path)
        issues.extend(checker.check())

    if issues:
        for issue in issues:
            print(issue.format())
        print(f"Found {len(issues)} delimiter issue(s).")
        return 1

    print("No delimiter issues found.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
