#!/usr/bin/env python3
"""Verify that every Python file in the repository is documented in LLM.txt.

Behavior:
- Scans the repository for .py files (excluding common virtualenv/cache/build dirs)
- Parses LLM.txt sections of the form:
  "### /abs/path/to/dir/" followed by lines "filename.py - Description..."
- Reports:
  1) Python files missing from LLM.txt
  2) Entries documented in LLM.txt where the file does not exist
  3) Entries whose summary appears to contain too few sentences (optional enforcement)
  4) Repeated section headers (same directory appears multiple times)
  5) Duplicate file entries (same file documented multiple times)

Exit code:
- 0 if no missing files and no broken entries
- 1 if any missing files or any broken entries (and optionally if enforcing sentence count)

Options:
- Use --fix-sorting to automatically sort section headers and filenames alphabetically
- Use --enforce-two-sentences to make the sentence-count rule fail the check
- Use --enforce-exact-two-sentences to require exactly two sentences (recommended)
- Use --warn-unsorted to check if section headers and filenames are alphabetically sorted
- Use --enforce-sorted to fail if section headers or filenames are not sorted

Notes:
- Sentence counting is heuristic: looks for occurrences of ., !, or ? followed by whitespace or end-of-line
"""

from __future__ import annotations

import argparse
import logging
import os
import re
import sys

from collections import defaultdict
from collections.abc import Iterable, Sequence
from dataclasses import dataclass

from kestrel_llm.logging_utils import setup_logging
from kestrel_llm.repo_utils import normalize_path
    
DEFAULT_EXCLUDED_DIRS: set[str] = {
    ".git",
    "__pycache__",
    ".pytest_cache",
    ".mypy_cache",
    ".idea",
    ".vscode",
    "node_modules",
    "dist",
    "build",
    ".venv",
    "venv",
    ".tox",
    ".eggs",
    "other",
    "torch-env",
}


HEADER_PATTERN = re.compile(r"^\s*###\s+(?P<dir>\S+)\s*$")
FILE_LINE_PATTERN = re.compile(
    r"^\s*(?P<name>[A-Za-z0-9_]+\.py)\s*-\s*(?P<desc>.+?)\s*$"
)
SENTENCE_BOUNDARY_PATTERN = re.compile(r"[.!?](?:\s|$)")

# Sentinel line used by update_llm_symbols.py to append autogenerated symbol info.
AUTOGEN_SENTINEL = "Symbols:"

REQUIRED_SENTENCE_COUNT = 2


@dataclass
class LLMSection:
    """A `### <dir>` section in an LLM.txt file."""

    header_line: str
    header_dir_raw: str
    header_dir_abs: str
    header_line_no: int
    entries: list[LLMEntry]


@dataclass
class LLMEntry:
    """A single `filename.py - ...` entry within an LLM.txt section."""

    section_dir_abs: str
    file_name: str
    abs_path: str
    desc_lines: list[str]
    start_line_no: int

    def summary_lines(self) -> list[str]:
        """Return only the human summary lines (excluding autogenerated Symbols block)."""
        out: list[str] = []
        for line in self.desc_lines:
            if line.strip() == AUTOGEN_SENTINEL:
                break
            out.append(line)
        return out

    def summary_text(self) -> str:
        """Return the human summary as a single string (autogenerated Symbols ignored)."""
        return " ".join(s.strip() for s in self.summary_lines() if s.strip())


@dataclass
class DocumentedFile:
    """A documented file entry in LLM.txt (flattened representation)."""

    absolute_path: str
    description: str


@dataclass(frozen=True, slots=True)
class VerifyOptions:
    """Options for verifying LLM.txt coverage and hygiene."""

    enforce_two_sentences: bool = False
    enforce_exact_two_sentences: bool = False
    extra_excluded_dirs: Sequence[str] = ()
    warn_unsorted: bool = False
    enforce_sorted: bool = False
    fix_sorting: bool = False


def _resolve_header_dir_abs(header_dir: str, *, repo_root_abs: str) -> str:
    """Resolve a header directory string to a normalized absolute path."""
    if os.path.isabs(header_dir):
        return normalize_path(header_dir)
    return normalize_path(os.path.join(repo_root_abs, header_dir))


def parse_llm_file_detailed(llm_file_path: str, repo_root: str) -> list[LLMSection]:
    """Parse LLM.txt into structured sections and file entries (preserving duplicates)."""
    repo_root_abs = normalize_path(repo_root)

    sections: list[LLMSection] = []
    current_section: LLMSection | None = None

    active_file_name: str | None = None
    active_abs_path: str | None = None
    active_desc_lines: list[str] = []
    active_start_line_no: int | None = None

    def flush_active_entry() -> None:
        nonlocal \
            active_file_name, \
            active_abs_path, \
            active_desc_lines, \
            active_start_line_no
        if (
            current_section is None
            or active_file_name is None
            or active_abs_path is None
            or active_start_line_no is None
        ):
            active_file_name = None
            active_abs_path = None
            active_desc_lines = []
            active_start_line_no = None
            return

        current_section.entries.append(
            LLMEntry(
                section_dir_abs=current_section.header_dir_abs,
                file_name=active_file_name,
                abs_path=active_abs_path,
                desc_lines=active_desc_lines[:],
                start_line_no=active_start_line_no,
            )
        )
        active_file_name = None
        active_abs_path = None
        active_desc_lines = []
        active_start_line_no = None

    with open(llm_file_path, encoding="utf-8") as f:
        for line_no, raw_line in enumerate(f, start=1):
            line = raw_line.rstrip("\n")

            header_match = HEADER_PATTERN.match(line)
            if header_match:
                flush_active_entry()
                header_dir_raw = header_match.group("dir")
                header_dir_abs = _resolve_header_dir_abs(
                    header_dir_raw, repo_root_abs=repo_root_abs
                )
                current_section = LLMSection(
                    header_line=line,
                    header_dir_raw=header_dir_raw,
                    header_dir_abs=header_dir_abs,
                    header_line_no=line_no,
                    entries=[],
                )
                sections.append(current_section)
                continue

            file_match = FILE_LINE_PATTERN.match(line)
            if file_match and current_section is not None:
                flush_active_entry()
                file_name = file_match.group("name")
                first_desc = file_match.group("desc").strip()
                abs_path = normalize_path(
                    os.path.join(current_section.header_dir_abs, file_name)
                )
                active_file_name = file_name
                active_abs_path = abs_path
                active_desc_lines = [first_desc]
                active_start_line_no = line_no
                continue

            if active_file_name is not None:
                if line.strip() == "":
                    flush_active_entry()
                else:
                    active_desc_lines.append(line)

    flush_active_entry()
    return sections


def parse_llm_file(llm_file_path: str, repo_root: str) -> dict[str, DocumentedFile]:
    """Parse LLM.txt into a mapping from absolute file paths to descriptions."""
    documented: dict[str, DocumentedFile] = {}
    current_dir: str | None = None
    active_file_path: str | None = None
    active_desc_lines: list[str] = []

    def flush_active() -> None:
        nonlocal active_file_path, active_desc_lines
        if active_file_path is not None:
            description = " ".join(s.strip() for s in active_desc_lines if s.strip())
            documented[active_file_path] = DocumentedFile(
                absolute_path=active_file_path,
                description=description,
            )
        active_file_path = None
        active_desc_lines = []

    repo_root_abs = normalize_path(repo_root)

    with open(llm_file_path, encoding="utf-8") as f:
        for raw_line in f:
            line = raw_line.rstrip("\n")

            header_match = HEADER_PATTERN.match(line)
            if header_match:
                flush_active()
                header_dir = header_match.group("dir")
                # Allow absolute or repo-relative headers
                if os.path.isabs(header_dir):
                    current_dir = normalize_path(header_dir)
                else:
                    current_dir = normalize_path(
                        os.path.join(repo_root_abs, header_dir)
                    )
                continue

            file_match = FILE_LINE_PATTERN.match(line)
            if file_match and current_dir is not None:
                # Starting a new file entry; flush the previous
                flush_active()
                file_name = file_match.group("name")
                first_desc = file_match.group("desc").strip()
                abs_path = normalize_path(os.path.join(current_dir, file_name))
                active_file_path = abs_path
                active_desc_lines = [first_desc]
                continue

            # Continuation lines for the active file description
            if active_file_path is not None:
                if line.strip() == "":
                    # Blank line ends the current description block
                    flush_active()
                else:
                    active_desc_lines.append(line)

        # End of file
        flush_active()

    return documented


def collect_section_file_order(
    llm_file_path: str, repo_root: str
) -> dict[str, list[str]]:
    """Return mapping of absolute section directories to the list of filenames in the
    order they appear in LLM.txt. Used to check alphabetical ordering hygiene.

    Section keys are absolute, normalized paths to match parse_llm_file behavior.
    """
    section_to_files: dict[str, list[str]] = {}
    current_dir: str | None = None

    repo_root_abs = normalize_path(repo_root)

    with open(llm_file_path, encoding="utf-8") as f:
        for raw_line in f:
            line = raw_line.rstrip("\n")

            header_match = HEADER_PATTERN.match(line)
            if header_match:
                header_dir = header_match.group("dir")
                if os.path.isabs(header_dir):
                    current_dir = normalize_path(header_dir)
                else:
                    current_dir = normalize_path(
                        os.path.join(repo_root_abs, header_dir)
                    )
                section_to_files.setdefault(current_dir, [])
                continue

            file_match = FILE_LINE_PATTERN.match(line)
            if file_match and current_dir is not None:
                file_name = file_match.group("name")
                section_to_files[current_dir].append(file_name)

    return section_to_files


def iter_python_files(repo_root: str, excluded_dirs: set[str]) -> Iterable[str]:
    """Yield normalized absolute paths to Python files under the repo root.

    Notes:
    - Prunes common excluded directories (venvs, caches, etc.)
    - Always skips hidden directories (starting with '.')
    - Skips any *nested* git repositories (directories that contain a `.git` marker)
      so submodules and embedded repos are not treated as part of this repo.
    """
    repo_root_abs = normalize_path(repo_root)
    for root, dirs, files in os.walk(repo_root_abs, topdown=True):
        # If this directory looks like the root of a nested git repo (submodule, vendor repo,
        # worktree checkout, etc.), skip it entirely. We allow the main repo root even if it
        # contains a `.git` marker.
        if root != repo_root_abs and (".git" in dirs or ".git" in files):
            dirs[:] = []
            continue

        # Prune excluded directories by name. Always skip hidden directories (starting with '.')
        dirs[:] = [
            d for d in dirs if (d not in excluded_dirs) and (not d.startswith("."))
        ]
        for fname in files:
            if not fname.endswith(".py"):
                continue
            yield normalize_path(os.path.join(root, fname))


def count_sentences(text: str) -> int:
    """Heuristically count sentences in a text string."""
    return len(SENTENCE_BOUNDARY_PATTERN.findall(text))


def _read_preamble_lines(llm_file_path: str) -> list[str]:
    preamble_lines: list[str] = []
    with open(llm_file_path, encoding="utf-8") as f:
        for raw_line in f:
            line = raw_line.rstrip("\n")
            if HEADER_PATTERN.match(line):
                break
            preamble_lines.append(line)
    return preamble_lines


def _merge_sections_by_dir(
    sections: Sequence[LLMSection],
) -> tuple[list[str], dict[str, str], dict[str, list[LLMEntry]]]:
    section_order: list[str] = []
    header_line_by_dir: dict[str, str] = {}
    entries_by_dir: dict[str, list[LLMEntry]] = defaultdict(list)

    for section in sections:
        dir_abs = section.header_dir_abs
        if dir_abs not in header_line_by_dir:
            section_order.append(dir_abs)
            header_line_by_dir[dir_abs] = section.header_line
        entries_by_dir[dir_abs].extend(section.entries)

    return section_order, header_line_by_dir, dict(entries_by_dir)


def _section_sort_key(section_dir_abs: str, *, repo_root_abs: str) -> tuple[str, str]:
    """Return a deterministic sort key for section directories.

    We primarily sort by repo-relative path so the ordering is stable across machines,
    but fall back to the absolute path when relpath cannot be computed (e.g. different
    drives on Windows).
    """
    try:
        rel = os.path.relpath(section_dir_abs, repo_root_abs)
    except ValueError:
        rel = section_dir_abs
    return (rel, section_dir_abs)


def _write_sorted_llm_file(
    llm_file_path: str,
    *,
    preamble_lines: Sequence[str],
    section_order: Sequence[str],
    header_line_by_dir: dict[str, str],
    entries_by_dir: dict[str, list[LLMEntry]],
) -> None:
    with open(llm_file_path, "w", encoding="utf-8") as f:
        for line in preamble_lines:
            f.write(line + "\n")

        for section_dir_abs in section_order:
            header_line = header_line_by_dir.get(section_dir_abs)
            if header_line is None:
                continue
            f.write(header_line + "\n\n")

            entries = entries_by_dir.get(section_dir_abs, [])
            for entry in sorted(entries, key=lambda e: (e.file_name, e.start_line_no)):
                if not entry.desc_lines:
                    continue
                f.write(f"{entry.file_name} - {entry.desc_lines[0]}\n")
                for cont_line in entry.desc_lines[1:]:
                    f.write(cont_line + "\n")
                f.write("\n")


def sort_llm_file(llm_file_path: str, repo_root: str) -> None:
    """Read LLM.txt, sort section headers and filenames alphabetically, and rewrite.

    If the same section header directory appears multiple times, merge all file entries
    into the first occurrence and drop repeated headers.
    """
    repo_root_abs = normalize_path(repo_root)

    preamble_lines = _read_preamble_lines(llm_file_path)
    sections = parse_llm_file_detailed(llm_file_path, repo_root_abs)
    section_order, header_line_by_dir, entries_by_dir = _merge_sections_by_dir(sections)
    section_order = sorted(
        section_order, key=lambda d: _section_sort_key(d, repo_root_abs=repo_root_abs)
    )
    _write_sorted_llm_file(
        llm_file_path,
        preamble_lines=preamble_lines,
        section_order=section_order,
        header_line_by_dir=header_line_by_dir,
        entries_by_dir=entries_by_dir,
    )


def _build_excluded_dirs(extra_excluded_dirs: Sequence[str]) -> set[str]:
    excluded = set(DEFAULT_EXCLUDED_DIRS)
    excluded.update(extra_excluded_dirs)
    return excluded


def _find_repeated_headers(
    sections: Sequence[LLMSection],
) -> dict[str, list[LLMSection]]:
    sections_by_dir: dict[str, list[LLMSection]] = defaultdict(list)
    for section in sections:
        sections_by_dir[section.header_dir_abs].append(section)
    return {d: ss for d, ss in sections_by_dir.items() if len(ss) > 1}


def _group_entries_by_path(entries: Sequence[LLMEntry]) -> dict[str, list[LLMEntry]]:
    entries_by_path: dict[str, list[LLMEntry]] = defaultdict(list)
    for entry in entries:
        entries_by_path[entry.abs_path].append(entry)
    return dict(entries_by_path)


def _find_duplicate_entries(
    entries_by_path: dict[str, list[LLMEntry]],
) -> dict[str, list[LLMEntry]]:
    return {p: es for p, es in entries_by_path.items() if len(es) > 1}


def _find_sentence_issues(
    entries: Sequence[LLMEntry],
    *,
    enforce_exact_two_sentences: bool,
) -> list[tuple[str, int, int]]:
    issues: list[tuple[str, int, int]] = []
    for entry in entries:
        summary = entry.summary_text()
        num = count_sentences(summary)
        if enforce_exact_two_sentences:
            if num != REQUIRED_SENTENCE_COUNT:
                issues.append((entry.abs_path, entry.start_line_no, num))
        elif num < REQUIRED_SENTENCE_COUNT:
            issues.append((entry.abs_path, entry.start_line_no, num))
    return issues


def _log_overview(
    logger: logging.Logger,
    *,
    repo_root_abs: str,
    llm_file_abs: str,
    python_files_found: int,
    documented_files_found: int,
) -> None:
    logger.info("Repo root: %s", repo_root_abs)
    logger.info("LLM file:  %s", llm_file_abs)
    logger.info("")
    logger.info("Python files found: %d", python_files_found)
    logger.info("Documented in LLM:  %d", documented_files_found)
    logger.info("")


def _report_repeated_headers(
    logger: logging.Logger,
    repeated_headers: dict[str, list[LLMSection]],
    *,
    repo_root_abs: str,
) -> bool:
    if not repeated_headers:
        return False

    logger.error(
        "Repeated section headers in LLM.txt (directory appears multiple times):"
    )
    for dir_abs, secs in sorted(repeated_headers.items(), key=lambda x: x[0]):
        rel_dir = os.path.relpath(dir_abs, repo_root_abs)
        line_nos = ", ".join(str(s.header_line_no) for s in secs)
        logger.error("  - %s (lines %s)", rel_dir, line_nos)
    logger.info("")
    return True


def _report_duplicate_entries(
    logger: logging.Logger,
    duplicate_entries: dict[str, list[LLMEntry]],
    *,
    repo_root_abs: str,
) -> bool:
    if not duplicate_entries:
        return False

    logger.error(
        "Duplicate file entries in LLM.txt (same file documented multiple times):"
    )
    for path, es in sorted(duplicate_entries.items(), key=lambda x: x[0]):
        rel = os.path.relpath(path, repo_root_abs)
        line_nos = ", ".join(str(e.start_line_no) for e in es)
        logger.error("  - %s (lines %s)", rel, line_nos)
    logger.info("")
    return True


def _report_missing_in_llm(
    logger: logging.Logger,
    missing_in_llm: Sequence[str],
    *,
    repo_root_abs: str,
) -> bool:
    if not missing_in_llm:
        return False

    logger.error("Missing from LLM.txt (files exist on disk):")
    for path in missing_in_llm:
        rel = os.path.relpath(path, repo_root_abs)
        logger.error("  - %s", rel)
    logger.info("")
    return True


def _report_documented_missing_on_disk(
    logger: logging.Logger,
    documented_missing_on_disk: Sequence[str],
    *,
    repo_root_abs: str,
) -> bool:
    if not documented_missing_on_disk:
        return False

    logger.error("Documented in LLM.txt but file not found on disk:")
    for path in documented_missing_on_disk:
        rel = os.path.relpath(path, repo_root_abs)
        logger.error("  - %s", rel)
    logger.info("")
    return True


def _report_sentence_issues(
    logger: logging.Logger,
    sentence_issues: Sequence[tuple[str, int, int]],
    *,
    repo_root_abs: str,
    enforce_two_sentences: bool,
    enforce_exact_two_sentences: bool,
) -> bool:
    if not sentence_issues:
        return False

    if enforce_exact_two_sentences:
        logger.warning(
            "Summaries not exactly two sentences (heuristic count, Symbols ignored):"
        )
    else:
        logger.warning(
            "Summaries with fewer than two sentences (heuristic count, Symbols ignored):"
        )

    for path, line_no, num in sentence_issues:
        rel = os.path.relpath(path, repo_root_abs)
        logger.warning("  - %s (line %d, found %d)", rel, line_no, num)
    logger.info("")

    return enforce_exact_two_sentences or enforce_two_sentences


def _report_unsorted_sections(
    logger: logging.Logger,
    sections: Sequence[LLMSection],
    *,
    repo_root_abs: str,
    warn_unsorted: bool,
    enforce_sorted: bool,
) -> bool:
    if not (warn_unsorted or enforce_sorted):
        return False

    any_unsorted = False

    # 1) Section header order (### <dir>)
    section_dirs_in_order = [s.header_dir_abs for s in sections]
    sorted_section_dirs = sorted(
        section_dirs_in_order,
        key=lambda d: _section_sort_key(d, repo_root_abs=repo_root_abs),
    )
    if section_dirs_in_order != sorted_section_dirs:
        any_unsorted = True
        current_order = ", ".join(
            os.path.relpath(d, repo_root_abs) for d in section_dirs_in_order
        )
        expected_order = ", ".join(
            os.path.relpath(d, repo_root_abs) for d in sorted_section_dirs
        )
        if enforce_sorted:
            logger.error("Unsorted section headers in LLM.txt (### <dir>)")
            logger.error("  Current order: %s", current_order)
            logger.error("  Sorted order:  %s", expected_order)
        else:
            logger.warning("Unsorted section headers in LLM.txt (### <dir>)")
            logger.warning("  Current order: %s", current_order)
            logger.warning("  Sorted order:  %s", expected_order)
        logger.info("")

    # 2) Filenames within each section
    for section in sections:
        files_in_order = [e.file_name for e in section.entries]
        sorted_files = sorted(files_in_order)
        if files_in_order == sorted_files:
            continue

        any_unsorted = True
        rel_section = os.path.relpath(section.header_dir_abs, repo_root_abs)

        if enforce_sorted:
            logger.error("Unsorted filenames in section: %s", rel_section)
            logger.error("  Current order: %s", ", ".join(files_in_order))
            logger.error("  Sorted order:  %s", ", ".join(sorted_files))
        else:
            logger.warning("Unsorted filenames in section: %s", rel_section)
            logger.warning("  Current order: %s", ", ".join(files_in_order))
            logger.warning("  Sorted order:  %s", ", ".join(sorted_files))
        logger.info("")

    if not any_unsorted:
        logger.info("All section headers and filenames are in alphabetical order.")

    return any_unsorted and enforce_sorted


def verify(
    repo_root: str,
    llm_file_path: str,
    options: VerifyOptions,
) -> int:
    """Verify LLM.txt coverage and hygiene.

    Returns:
        Exit code (0 on success, 1 on any failure conditions).
    """
    logger = logging.getLogger("verify_llm_docs")
    repo_root_abs = normalize_path(repo_root)
    llm_file_abs = normalize_path(llm_file_path)
    excluded = _build_excluded_dirs(options.extra_excluded_dirs)

    # Apply sorting fix if requested
    if options.fix_sorting:
        logger.info("Sorting section headers and filenames alphabetically...")
        sort_llm_file(llm_file_abs, repo_root_abs)
        logger.info(
            "LLM.txt has been updated with sorted section headers and filenames."
        )
        logger.info("")

    sections = parse_llm_file_detailed(llm_file_abs, repo_root_abs)
    entries: list[LLMEntry] = [e for s in sections for e in s.entries]

    # Repeated header detection (directory appears multiple times)
    sections_by_dir: dict[str, list[LLMSection]] = defaultdict(list)
    for section in sections:
        sections_by_dir[section.header_dir_abs].append(section)
    repeated_headers: dict[str, list[LLMSection]] = {
        d: ss for d, ss in sections_by_dir.items() if len(ss) > 1
    }

    # Duplicate file entry detection (same file documented multiple times)
    entries_by_path = _group_entries_by_path(entries)
    duplicate_entries = _find_duplicate_entries(entries_by_path)

    all_py_files = set(iter_python_files(repo_root_abs, excluded))

    documented_paths = set(entries_by_path.keys())

    missing_in_llm = sorted(all_py_files - documented_paths)
    documented_missing_on_disk = sorted(
        p for p in documented_paths if not os.path.exists(p)
    )

    sentence_issues = _find_sentence_issues(
        entries, enforce_exact_two_sentences=options.enforce_exact_two_sentences
    )

    # Reporting
    _log_overview(
        logger,
        repo_root_abs=repo_root_abs,
        llm_file_abs=llm_file_abs,
        python_files_found=len(all_py_files),
        documented_files_found=len(documented_paths),
    )

    failed = False
    failed |= _report_repeated_headers(
        logger, repeated_headers, repo_root_abs=repo_root_abs
    )
    failed |= _report_duplicate_entries(
        logger, duplicate_entries, repo_root_abs=repo_root_abs
    )
    failed |= _report_missing_in_llm(
        logger, missing_in_llm, repo_root_abs=repo_root_abs
    )
    failed |= _report_documented_missing_on_disk(
        logger, documented_missing_on_disk, repo_root_abs=repo_root_abs
    )
    failed |= _report_sentence_issues(
        logger,
        sentence_issues,
        repo_root_abs=repo_root_abs,
        enforce_two_sentences=options.enforce_two_sentences,
        enforce_exact_two_sentences=options.enforce_exact_two_sentences,
    )
    if not repeated_headers:
        failed |= _report_unsorted_sections(
            logger,
            sections,
            repo_root_abs=repo_root_abs,
            warn_unsorted=options.warn_unsorted,
            enforce_sorted=options.enforce_sorted,
        )

    if not failed:
        logger.info("All checks passed.")

    return 1 if failed else 0


def build_arg_parser() -> argparse.ArgumentParser:
    """Build the CLI argument parser for the standalone script entry point."""
    parser = argparse.ArgumentParser(
        description="Verify every Python file is documented in LLM.txt"
    )
    parser.add_argument(
        "--repo-root",
        default=os.environ.get("CONDOR_REPO_ROOT", os.getcwd()),
        help="Absolute path to the repository root (default: CWD or $CONDOR_REPO_ROOT)",
    )
    parser.add_argument(
        "--llm-file",
        default=os.path.join(
            os.environ.get("CONDOR_REPO_ROOT", os.getcwd()), "LLM.txt"
        ),
        help="Absolute path to LLM.txt (default: <repo-root>/LLM.txt)",
    )
    parser.add_argument(
        "--enforce-two-sentences",
        action="store_true",
        help="Fail if any summary appears to contain fewer than two sentences (Symbols ignored)",
    )
    parser.add_argument(
        "--enforce-exact-two-sentences",
        action="store_true",
        help="Fail if any summary does not contain exactly two sentences (Symbols ignored)",
    )
    parser.add_argument(
        "--warn-unsorted",
        action="store_true",
        help="Warn if filenames are not alphabetically sorted within each section",
    )
    parser.add_argument(
        "--enforce-sorted",
        action="store_true",
        help="Fail if filenames are not sorted within any section",
    )
    parser.add_argument(
        "--fix-sorting",
        action="store_true",
        help="Automatically sort filenames alphabetically within each section in LLM.txt",
    )
    parser.add_argument(
        "--exclude-dir",
        action="append",
        default=[],
        help="Additional directory names to exclude during scanning (can be repeated)",
    )
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    """Standalone CLI entry point for verifying LLM.txt coverage and rules."""
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    parser = build_arg_parser()
    args = parser.parse_args(argv)

    # Resolve defaults relative to provided repo root if the user left --llm-file at default
    repo_root = normalize_path(args.repo_root)
    llm_file = args.llm_file
    if llm_file == os.path.join(
        os.environ.get("CONDOR_REPO_ROOT", os.getcwd()), "LLM.txt"
    ):
        llm_file = os.path.join(repo_root, "LLM.txt")

    return verify(
        repo_root=repo_root,
        llm_file_path=llm_file,
        options=VerifyOptions(
            enforce_two_sentences=args.enforce_two_sentences,
            enforce_exact_two_sentences=args.enforce_exact_two_sentences,
            extra_excluded_dirs=tuple(args.exclude_dir),
            warn_unsorted=args.warn_unsorted,
            enforce_sorted=args.enforce_sorted,
            fix_sorting=args.fix_sorting,
        ),
    )


if __name__ == "__main__":
    setup_logging()
    sys.exit(main())
