#!/usr/bin/env python3
"""Static analysis for dangerous SwiftUI modifier patterns.

Catches three classes of regressions that caused vault-notes-vanish and
terminal-empty bugs (Plan D incident, May 2026):

  1. .animation() on views wrapped in AnyView() — broad animation contexts
     on type-erased views cause rendering pipeline issues during
     NSHostingView.rootView replacement.

  2. .arrivalHighlight() (or any .background-adding modifier) sandwiched
     between .tag() and .listRowBackground() in a List — interferes with
     SwiftUI's internal row selection management.

  3. .animation() on ScrollView/LazyVStack containing SSE/streaming data —
     the animation modifier can block incremental rendering of rapidly-
     updating lazy content.

Usage:
    python scripts/check_swiftui_modifiers.py [--diff]
        Without --diff: scans all .swift files under MugiApp/.
        With --diff: scans only files touched in the staged diff.

Exit codes:
    0 — no violations found
    1 — violations found
    2 — script error
"""
from __future__ import annotations

import argparse
import os
import re
import subprocess
import sys
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parent.parent
SWIFT_DIR = REPO_ROOT / "MugiApp"

# Modifiers that add .background() layers — dangerous between .tag() and
# .listRowBackground() in List contexts.
BACKGROUND_MODIFIERS = {
    ".arrivalHighlight(",
    ".directionalEdgeGlow(",
}


def staged_swift_files() -> list[Path]:
    result = subprocess.run(
        ["git", "diff", "--cached", "--name-only", "--diff-filter=ACMR"],
        capture_output=True, text=True, cwd=REPO_ROOT,
    )
    return [
        REPO_ROOT / f
        for f in result.stdout.strip().splitlines()
        if f.endswith(".swift") and f.startswith("MugiApp/")
    ]


def all_swift_files() -> list[Path]:
    files: list[Path] = []
    for root, _dirs, names in os.walk(SWIFT_DIR):
        for name in names:
            if name.endswith(".swift") and "Tests" not in root:
                files.append(Path(root) / name)
    return files


# ── Check 1: .animation() on AnyView-wrapped content ──────────────────

_ANYVIEW_ANIMATION_RE = re.compile(
    r"""
    AnyView\(            # AnyView wrapping
    \s*\n?\s*            # optional whitespace/newline
    (?!content)          # NOT ViewModifier's `content` (that's a safe pattern)
    [\s\S]{0,800}?       # up to 800 chars of content (non-greedy)
    \.animation\(        # .animation modifier inside
    """,
    re.VERBOSE,
)


def check_anyview_animation(path: Path, source: str) -> list[str]:
    """Warn if .animation() is applied inside AnyView() on a complex view.

    Excludes ViewModifier.body patterns where AnyView is just a return-type
    wrapper around a single `content` view — those are safe because the
    animation is scoped to exactly one child, not a broad container.
    """
    violations: list[str] = []
    for m in _ANYVIEW_ANIMATION_RE.finditer(source):
        start = source[:m.start()].count("\n") + 1
        violations.append(
            f"  {path.relative_to(REPO_ROOT)}:{start}: "
            f".animation() inside AnyView() — broad animation on type-erased "
            f"views causes rendering issues during NSHostingView.rootView "
            f"replacement. Apply animation to specific child views instead."
        )
    return violations


# ── Check 2: background-modifier between .tag() and .listRowBackground()

def check_tag_background_order(path: Path, source: str) -> list[str]:
    """Warn if a background-adding modifier sits between .tag() and .listRowBackground()."""
    violations: list[str] = []
    lines = source.splitlines()
    for i, line in enumerate(lines, start=1):
        stripped = line.strip()
        if not any(stripped.startswith(mod) for mod in BACKGROUND_MODIFIERS):
            continue
        # Look backward for .tag( within 5 lines
        has_tag_above = False
        for j in range(max(0, i - 6), i - 1):
            if ".tag(" in lines[j]:
                has_tag_above = True
                break
        # Look forward for .listRowBackground( within 5 lines
        has_listrow_below = False
        for j in range(i, min(len(lines), i + 5)):
            if ".listRowBackground(" in lines[j]:
                has_listrow_below = True
                break
        if has_tag_above and has_listrow_below:
            mod_name = stripped.split("(")[0]
            violations.append(
                f"  {path.relative_to(REPO_ROOT)}:{i}: "
                f"{mod_name} between .tag() and .listRowBackground() — "
                f"background-adding modifiers between these List row "
                f"modifiers can break selection propagation. "
                f"Move it after .listRowBackground()."
            )
    return violations


# ── Check 3: .animation() on ScrollView containing LazyVStack ─────────

def check_scrollview_animation(path: Path, source: str) -> list[str]:
    """Warn if .animation() is applied to a ScrollView that contains LazyVStack."""
    violations: list[str] = []
    lines = source.splitlines()
    in_scrollview_block = False
    has_lazyvstack = False
    scrollview_start = 0
    brace_depth = 0

    for i, line in enumerate(lines, start=1):
        stripped = line.strip()
        if "ScrollView" in stripped and "{" in stripped:
            in_scrollview_block = True
            has_lazyvstack = False
            scrollview_start = i
            brace_depth = 1
            continue

        if in_scrollview_block:
            brace_depth += stripped.count("{") - stripped.count("}")
            if "LazyVStack" in stripped:
                has_lazyvstack = True
            if brace_depth <= 0:
                in_scrollview_block = False
                # Check the next few lines after the closing brace for .animation(
                for j in range(i, min(len(lines), i + 3)):
                    if ".animation(" in lines[j]:
                        if has_lazyvstack:
                            violations.append(
                                f"  {path.relative_to(REPO_ROOT)}:{j + 1}: "
                                f".animation() on ScrollView containing "
                                f"LazyVStack (started line {scrollview_start}) — "
                                f"can block incremental rendering of streaming "
                                f"data. Use withAnimation() on specific state "
                                f"changes instead."
                            )
                        break
    return violations


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--diff", action="store_true",
                        help="Only check staged files")
    args = parser.parse_args()

    files = staged_swift_files() if args.diff else all_swift_files()
    if not files:
        print("No Swift files to check.")
        return 0

    all_violations: list[str] = []
    for path in sorted(files):
        if not path.exists():
            continue
        source = path.read_text(encoding="utf-8", errors="replace")
        all_violations.extend(check_anyview_animation(path, source))
        all_violations.extend(check_tag_background_order(path, source))
        all_violations.extend(check_scrollview_animation(path, source))

    if all_violations:
        print(f"SwiftUI modifier lint: {len(all_violations)} violation(s)\n")
        for v in all_violations:
            print(v)
        print(
            "\nThese patterns caused the Plan D vault-notes-vanish and "
            "terminal-empty regressions. See scripts/check_swiftui_modifiers.py "
            "for details."
        )
        return 1
    else:
        print("SwiftUI modifier lint: OK")
        return 0


if __name__ == "__main__":
    try:
        sys.exit(main())
    except Exception as exc:
        print(f"Script error: {exc}", file=sys.stderr)
        sys.exit(2)
