#!/usr/bin/env python3
"""Static legacy UI modernization inspector with no third-party dependencies."""

from __future__ import annotations

import argparse
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Iterable

SCANNED_EXTENSIONS = {
    ".html", ".htm", ".xhtml", ".css", ".scss", ".sass", ".less",
    ".js", ".jsx", ".ts", ".tsx", ".vue", ".svelte", ".php", ".cfm",
    ".cfc", ".jsp", ".jspx", ".asp", ".aspx", ".cshtml", ".vbhtml",
    ".cs", ".vb", ".xml", ".json", ".yml", ".yaml", ".properties",
}

IGNORE_DIRS = {
    ".git", ".svn", ".hg", "node_modules", "vendor", "dist", "build",
    "coverage", ".next", ".nuxt", ".cache", "target", "bin", "obj",
    "packages", "bower_components", "wwwroot/lib", "public/vendor",
}

SEVERITY_ORDER = {"info": 0, "low": 1, "medium": 2, "high": 3, "critical": 4}

FRAMEWORK_PACKAGES = {
    "jquery": "jQuery",
    "angular": "AngularJS",
    "@angular/core": "Angular",
    "react": "React",
    "react-dom": "React",
    "vue": "Vue",
    "svelte": "Svelte",
    "backbone": "Backbone",
    "ember-source": "Ember",
    "bootstrap": "Bootstrap",
    "foundation-sites": "Foundation",
}

LEGACY_PACKAGE_RULES = {
    "angular": ("high", "AngularJS package detected; plan containment and migration because it is a distinct legacy framework."),
    "backbone": ("medium", "Backbone detected; confirm ownership, plugin dependencies, and supported runtime."),
    "bower": ("high", "Bower detected; preserve reproducible output, then migrate package acquisition incrementally."),
    "grunt": ("medium", "Grunt detected; document generated assets before replacing the build pipeline."),
    "gulp": ("medium", "Gulp detected; verify task behavior and generated files before changing the build."),
}


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Inspect a project for legacy UI modernization risks.")
    parser.add_argument("path", help="Project directory to inspect")
    parser.add_argument("--format", choices=("markdown", "json"), default="markdown")
    parser.add_argument("--output", help="Write the report to this file")
    parser.add_argument("--fail-on", choices=("low", "medium", "high", "critical"))
    parser.add_argument("--max-file-bytes", type=int, default=2_000_000)
    return parser.parse_args()


def is_ignored(path: Path, root: Path) -> bool:
    try:
        parts = path.relative_to(root).parts
    except ValueError:
        return True
    joined = "/".join(parts)
    simple_names = {value for value in IGNORE_DIRS if "/" not in value}
    if any(part in simple_names for part in parts):
        return True
    return any(joined == value or joined.startswith(value + "/") for value in IGNORE_DIRS if "/" in value)


def iter_files(root: Path) -> Iterable[Path]:
    for path in root.rglob("*"):
        if not path.is_file() or is_ignored(path, root):
            continue
        if path.name in {
            "package.json", "bower.json", "composer.json", "packages.config",
            "Gemfile", "pom.xml", "build.gradle", "webpack.config.js",
            "vite.config.js", "vite.config.ts", "gulpfile.js", "Gruntfile.js",
        } or path.suffix.lower() in SCANNED_EXTENSIONS:
            yield path


def read_text(path: Path, max_bytes: int) -> str | None:
    try:
        if path.stat().st_size > max_bytes:
            return None
        return path.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return None


def line_number(text: str, index: int) -> int:
    return text.count("\n", 0, max(index, 0)) + 1


def parse_major(version: str) -> int | None:
    match = re.search(r"(\d+)", version or "")
    return int(match.group(1)) if match else None


def compact_evidence(value: str, limit: int = 180) -> str:
    value = " ".join(value.strip().split())
    return value if len(value) <= limit else value[: limit - 3] + "..."


def main() -> int:
    args = parse_args()
    root = Path(args.path).resolve()
    if not root.exists() or not root.is_dir():
        print(f"Error: project directory not found: {root}", file=sys.stderr)
        return 2

    findings: list[dict[str, Any]] = []
    inventory: dict[str, Any] = {
        "root": str(root),
        "files_scanned": 0,
        "technologies": set(),
        "packages": {},
        "server_templates": set(),
        "build_tools": set(),
        "test_signals": set(),
        "ci_signals": set(),
    }
    file_texts: dict[Path, str] = {}
    rule_counts: Counter[str] = Counter()

    def add(
        rule_id: str,
        severity: str,
        confidence: str,
        area: str,
        message: str,
        impact: str,
        remediation: str,
        manual_check: str,
        path: Path | None = None,
        line: int | None = None,
        evidence: str = "",
    ) -> None:
        key = f"{rule_id}|{path}|{line}|{message}"
        if any(item.get("_key") == key for item in findings):
            return
        findings.append({
            "_key": key,
            "rule_id": rule_id,
            "severity": severity,
            "confidence": confidence,
            "area": area,
            "message": message,
            "file": str(path.relative_to(root)) if path else None,
            "line": line,
            "evidence": compact_evidence(evidence),
            "impact": impact,
            "remediation": remediation,
            "manual_check": manual_check,
        })
        rule_counts[rule_id] += 1

    files = list(iter_files(root))
    inventory["files_scanned"] = len(files)

    package_manifests: list[Path] = []
    for path in files:
        text = read_text(path, args.max_file_bytes)
        if text is None:
            continue
        file_texts[path] = text
        lower_name = path.name.lower()
        suffix = path.suffix.lower()

        if lower_name == "package.json":
            package_manifests.append(path)
            try:
                data = json.loads(text)
            except json.JSONDecodeError as exc:
                add(
                    "LUM001", "high", "high", "build",
                    "package.json is not valid JSON.",
                    "Dependency and build inventory may be unreliable.",
                    "Repair the manifest before changing package or build tooling.",
                    "Confirm whether preprocessing or templating generates this file.",
                    path, exc.lineno, str(exc),
                )
                continue
            deps: dict[str, str] = {}
            for section in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
                value = data.get(section, {})
                if isinstance(value, dict):
                    deps.update({str(k): str(v) for k, v in value.items()})
            inventory["packages"].update(deps)
            scripts = data.get("scripts", {})
            if isinstance(scripts, dict):
                for key, value in scripts.items():
                    combined = f"{key} {value}".lower()
                    if any(token in combined for token in ("test", "jest", "vitest", "playwright", "cypress", "karma")):
                        inventory["test_signals"].add(f"npm:{key}")
                    if "webpack" in combined:
                        inventory["build_tools"].add("webpack")
                    if "vite" in combined:
                        inventory["build_tools"].add("vite")
                    if "gulp" in combined:
                        inventory["build_tools"].add("gulp")
                    if "grunt" in combined:
                        inventory["build_tools"].add("grunt")

        if lower_name == "bower.json":
            inventory["build_tools"].add("bower")
            add(
                "LUM002", "high", "high", "dependencies",
                "Bower manifest detected.",
                "The package pipeline is likely unsupported or difficult to reproduce securely.",
                "Freeze and document current output, then migrate package acquisition without combining it with UI redesign.",
                "Confirm whether bower_components is committed, generated, or deployed from another pipeline.",
                path, 1, "bower.json",
            )

        if lower_name in {"gulpfile.js", "gruntfile.js", "webpack.config.js", "vite.config.js", "vite.config.ts"}:
            inventory["build_tools"].add(path.stem.lower())

        if suffix in {".cfm", ".cfc"}:
            inventory["server_templates"].add("ColdFusion")
        elif suffix == ".php":
            inventory["server_templates"].add("PHP")
        elif suffix in {".jsp", ".jspx"}:
            inventory["server_templates"].add("JSP")
        elif suffix in {".asp", ".aspx", ".cshtml", ".vbhtml"}:
            inventory["server_templates"].add("ASP.NET")

        if ".github/workflows" in str(path).replace("\\", "/"):
            inventory["ci_signals"].add("GitHub Actions")
        if lower_name in {"gitlab-ci.yml", "azure-pipelines.yml", "jenkinsfile", "circle.yml", "config.yml"}:
            inventory["ci_signals"].add(path.name)
        if any(token in lower_name for token in ("test", "spec")) and suffix in {".js", ".jsx", ".ts", ".tsx", ".cs", ".php"}:
            inventory["test_signals"].add(str(path.relative_to(root)))

    packages: dict[str, str] = inventory["packages"]
    frameworks = set()
    for package, label in FRAMEWORK_PACKAGES.items():
        if package in packages:
            frameworks.add(label)
            inventory["technologies"].add(label)

    if len(frameworks) >= 3:
        add(
            "LUM003", "high", "high", "architecture",
            f"Multiple UI frameworks are installed: {', '.join(sorted(frameworks))}.",
            "Shared DOM, routing, CSS, state, and lifecycle ownership may be ambiguous.",
            "Map ownership by route and component, then define explicit interoperability boundaries before migration.",
            "Confirm whether packages are active production dependencies, build-only tools, or unused remnants.",
            evidence=", ".join(sorted(frameworks)),
        )
    elif len(frameworks) == 2:
        add(
            "LUM004", "medium", "medium", "architecture",
            f"Two UI frameworks are installed: {', '.join(sorted(frameworks))}.",
            "Coexistence can create lifecycle and styling conflicts if boundaries are implicit.",
            "Document DOM ownership, routing, global state, and teardown responsibilities.",
            "Confirm whether both frameworks execute on the same route.",
            evidence=", ".join(sorted(frameworks)),
        )

    for package, (severity, note) in LEGACY_PACKAGE_RULES.items():
        if package in packages or package in inventory["build_tools"]:
            add(
                "LUM005", severity, "high", "dependencies",
                note,
                "Unsupported or opaque tooling increases migration and security risk.",
                "Inventory generated output, add characterization tests, and replace the dependency in an isolated tranche.",
                "Verify actual deployed version and organizational support policy.",
                evidence=f"{package}: {packages.get(package, 'detected')}",
            )

    version_rules = [
        ("bootstrap", 5, "high", "Bootstrap version is older than major 5."),
        ("jquery", 3, "medium", "jQuery version is older than major 3."),
        ("react", 16, "medium", "React version is from a substantially older major."),
        ("vue", 3, "medium", "Vue version is older than major 3."),
    ]
    for package, expected_major, severity, message in version_rules:
        if package in packages:
            major = parse_major(packages[package])
            if major is not None and major < expected_major:
                add(
                    "LUM006", severity, "medium", "dependencies",
                    message,
                    "Legacy APIs and ecosystem packages may constrain safe modernization.",
                    "Upgrade to the latest supported patch of the current major first, then follow official major migration guides incrementally.",
                    "Confirm the runtime artifact version; manifest ranges can differ from the deployed lockfile.",
                    evidence=f"{package}: {packages[package]}",
                )

    if package_manifests:
        lockfiles = [
            root / "package-lock.json", root / "npm-shrinkwrap.json", root / "yarn.lock",
            root / "pnpm-lock.yaml", root / "bun.lock", root / "bun.lockb",
        ]
        if not any(path.exists() for path in lockfiles):
            add(
                "LUM007", "high", "high", "build",
                "A JavaScript package manifest exists without a recognized lockfile at the project root.",
                "Builds and dependency audits may not be reproducible.",
                "Establish a supported package manager, generate a reviewed lockfile, and verify equivalent production output.",
                "Check whether the lockfile lives in a workspace subdirectory or is intentionally managed elsewhere.",
                evidence=", ".join(str(p.relative_to(root)) for p in package_manifests),
            )

    all_text = "\n".join(file_texts.values())
    has_data_toggle = bool(re.search(r"\bdata-toggle\s*=", all_text, re.I))
    has_data_bs_toggle = bool(re.search(r"\bdata-bs-toggle\s*=", all_text, re.I))
    if has_data_toggle and has_data_bs_toggle:
        add(
            "LUM008", "high", "high", "framework-coexistence",
            "Bootstrap legacy and Bootstrap 5 data attributes are both present.",
            "Different component runtimes can initialize the same behavior inconsistently.",
            "Map markup by route and Bootstrap major; isolate bundles or add a temporary adapter instead of loading both globally.",
            "Confirm whether generated templates or documentation examples account for one of the attribute families.",
            evidence="data-toggle and data-bs-toggle",
        )

    bootstrap_major = parse_major(packages.get("bootstrap", ""))
    jquery_calls = bool(re.search(r"\.(modal|tooltip|popover|collapse|dropdown|tab|carousel)\s*\(", all_text))
    if bootstrap_major is not None and bootstrap_major >= 5 and jquery_calls:
        add(
            "LUM009", "high", "high", "framework-coexistence",
            "Bootstrap 5 is installed while jQuery-style Bootstrap plugin calls are present.",
            "Legacy calls may fail or indicate mixed Bootstrap runtimes.",
            "Replace calls with supported Bootstrap instance APIs and verify event names, disposal, focus, and Popper behavior.",
            "Confirm whether a compatibility plugin intentionally reintroduces the jQuery API.",
            evidence="Bootstrap 5 package plus .modal/.tooltip/.popover style calls",
        )

    regex_rules = [
        (
            "LUM010", "high", "high", "javascript",
            re.compile(r"\b(document\.write|eval\s*\(|new\s+Function\s*\(|set(?:Timeout|Interval)\s*\(\s*['\"])", re.I),
            "Dynamic code or document injection API detected.",
            "These APIs complicate CSP, testing, and safe component boundaries and may enable injection defects.",
            "Replace with explicit DOM creation, typed callbacks, or constrained parsing; add security regression tests.",
            "Determine whether the input can contain user-controlled or external data.",
        ),
        (
            "LUM011", "high", "high", "javascript",
            re.compile(r"\.live\s*\(|\.andSelf\s*\(|\.size\s*\(|\.success\s*\(|\.error\s*\(", re.I),
            "Deprecated jQuery API detected.",
            "Framework upgrades can fail before business behavior is characterized.",
            "Replace one API family at a time and test delegated events, AJAX errors, and collection semantics.",
            "Confirm the exact jQuery and plugin versions in the deployed bundle.",
        ),
        (
            "LUM012", "high", "high", "javascript",
            re.compile(r"async\s*:\s*false|\.open\s*\([^\n;]*,\s*false\s*\)", re.I),
            "Synchronous network request pattern detected.",
            "The UI can block and migration to modern browsers or APIs may change timing behavior.",
            "Convert to asynchronous control flow and specify pending, timeout, cancellation, retry, and stale-response behavior.",
            "Confirm whether the pattern is active code or a test fixture.",
        ),
        (
            "LUM013", "high", "medium", "security",
            re.compile(r"\b(innerHTML|outerHTML|insertAdjacentHTML|dangerouslySetInnerHTML|v-html)\b", re.I),
            "HTML injection sink detected.",
            "Migrating rendering code can preserve or expand injection risk.",
            "Trace the data source, prefer text or structured rendering, and sanitize only with an approved context-aware mechanism when HTML is required.",
            "Determine whether all values reaching the sink are trusted and correctly encoded.",
        ),
        (
            "LUM014", "medium", "high", "javascript",
            re.compile(r"\bon(?:click|change|submit|load|error|keydown|keyup)\s*=", re.I),
            "Inline event handler detected.",
            "Behavior, CSP, testing, and lifecycle cleanup are tightly coupled to markup.",
            "Move behavior behind an owned module or progressive-enhancement initializer and preserve keyboard semantics.",
            "Confirm whether server templating injects dynamic handler content.",
        ),
        (
            "LUM015", "high", "high", "compatibility",
            re.compile(r"<!--\s*\[if\s+IE|\bActiveXObject\b|document\.all\b", re.I),
            "Internet Explorer-specific implementation detected.",
            "Unsupported browser branches and plugins can block dependency and security upgrades.",
            "Confirm current browser requirements, isolate required compatibility code, and remove branches only after usage is measured.",
            "Check enterprise mode, embedded browser controls, and internal kiosk requirements.",
        ),
        (
            "LUM016", "medium", "high", "markup",
            re.compile(r"<(?:font|center|marquee|frameset|frame)\b", re.I),
            "Deprecated presentational or frame markup detected.",
            "Semantics, accessibility, responsive behavior, and modern styling are constrained.",
            "Replace through route or component slices while preserving navigation, focus, and deep links.",
            "Confirm whether the markup is generated by a report or third-party editor.",
        ),
        (
            "LUM017", "high", "high", "security",
            re.compile(r"(?:src|href)\s*=\s*['\"]http://", re.I),
            "Insecure HTTP asset or link reference detected in markup.",
            "Mixed content can fail in modern browsers and exposes supply-chain or transport risk.",
            "Move controlled assets to HTTPS and verify third-party ownership, integrity, and fallback behavior.",
            "Separate navigational HTTP links from executable or embedded mixed content.",
        ),
        (
            "LUM018", "medium", "medium", "dependencies",
            re.compile(r"(?:cdn|unpkg|jsdelivr)[^'\"\s]*(?:@latest|/latest/|\?latest)", re.I),
            "A CDN dependency appears to use an unpinned latest version.",
            "Production behavior can change without a repository or deployment change.",
            "Pin an exact reviewed version and use supported integrity and caching controls.",
            "Confirm whether an upstream proxy rewrites or pins the asset.",
        ),
    ]

    css_important: defaultdict[Path, int] = defaultdict(int)
    css_fixed_width: defaultdict[Path, int] = defaultdict(int)
    css_animation_files: set[Path] = set()
    react_like_files: set[Path] = set()
    jquery_mutation_files: set[Path] = set()

    for path, text in file_texts.items():
        for rule_id, severity, confidence, area, pattern, message, impact, remediation, manual in regex_rules:
            for match in pattern.finditer(text):
                add(rule_id, severity, confidence, area, message, impact, remediation, manual,
                    path, line_number(text, match.start()), match.group(0))

        suffix = path.suffix.lower()
        if suffix in {".css", ".scss", ".sass", ".less", ".html", ".htm", ".cfm", ".php", ".jsp", ".aspx", ".cshtml"}:
            css_important[path] += len(re.findall(r"!important\b", text, re.I))
            css_fixed_width[path] += len(re.findall(r"\b(?:width|min-width)\s*:\s*(?:[5-9]\d{2}|\d{4,})px\b", text, re.I))
            if re.search(r"@keyframes|\banimation\s*:|\btransition\s*:", text, re.I):
                css_animation_files.add(path)

        if suffix in {".jsx", ".tsx", ".vue", ".svelte"} or re.search(r"\b(?:React|createApp|defineComponent|onMount)\b", text):
            react_like_files.add(path)
        if re.search(r"\$\([^\n]+\)\.(?:html|append|prepend|remove|empty|replaceWith|wrap)\s*\(", text):
            jquery_mutation_files.add(path)

        for match in re.finditer(r"\bwindow\.[A-Za-z_$][\w$]*\s*=", text):
            add(
                "LUM019", "medium", "medium", "javascript",
                "Global window state assignment detected.",
                "Implicit cross-module coupling makes route and component extraction harder.",
                "Map readers and writers, then hide the global behind an adapter before replacing implementations.",
                "Confirm whether the assignment is a deliberate public integration API.",
                path, line_number(text, match.start()), match.group(0),
            )

    if react_like_files and jquery_mutation_files:
        overlap = react_like_files.intersection(jquery_mutation_files)
        if overlap:
            for path in sorted(overlap):
                add(
                    "LUM020", "high", "medium", "framework-coexistence",
                    "Reactive component code and direct jQuery DOM mutation appear in the same file.",
                    "Two systems may compete for DOM ownership and lifecycle cleanup.",
                    "Wrap the legacy plugin behind one component boundary and initialize/dispose it through the framework lifecycle.",
                    "Confirm whether jQuery only targets nodes outside the framework-owned subtree.",
                    path, 1, "framework component plus jQuery DOM mutation",
                )

    for path, count in css_important.items():
        if count >= 20:
            add(
                "LUM021", "medium", "high", "css",
                f"The file contains {count} uses of !important.",
                "Cascade conflicts may hide framework and design-system coupling.",
                "Identify ownership boundaries and replace global overrides with scoped layers, tokens, or component contracts.",
                "Exclude generated or vendor CSS before deciding remediation.",
                path, 1, f"!important count: {count}",
            )
    for path, count in css_fixed_width.items():
        if count >= 3:
            add(
                "LUM022", "medium", "medium", "responsive",
                f"The file contains {count} large fixed pixel width declarations.",
                "Legacy layout assumptions can fail with zoom, narrow windows, translations, and split view.",
                "Characterize layout behavior, then migrate containers to intrinsic sizing and content-driven breakpoints.",
                "Confirm whether declarations target print, canvas, media, or another intrinsically fixed surface.",
                path, 1, f"large fixed widths: {count}",
            )

    if css_animation_files and not re.search(r"prefers-reduced-motion", all_text, re.I):
        add(
            "LUM023", "medium", "high", "accessibility",
            "Animations or transitions are present without a detected reduced-motion strategy.",
            "Modernization can preserve motion that causes discomfort or blocks accessible use.",
            "Define reduced-motion behavior for migrated components and verify that essential state changes remain understandable.",
            "Search generated styles and runtime media-query handling before confirming the finding.",
            evidence=f"animation files: {len(css_animation_files)}",
        )

    if not inventory["test_signals"]:
        add(
            "LUM024", "high", "medium", "testing",
            "No test tooling or test files were detected.",
            "Migration changes cannot be compared reliably against current behavior.",
            "Add characterization tests for critical journeys and contracts before structural replacement.",
            "Tests may live in another repository or external pipeline; verify before concluding coverage is absent.",
        )

    ci_paths = [root / ".github" / "workflows", root / ".gitlab-ci.yml", root / "azure-pipelines.yml", root / "Jenkinsfile"]
    if not any(path.exists() for path in ci_paths):
        add(
            "LUM025", "medium", "medium", "delivery",
            "No common CI configuration was detected.",
            "Repeatable quality gates and migration comparisons may depend on manual execution.",
            "Document the actual pipeline and add automated build, test, accessibility, and security gates where missing.",
            "CI may be configured outside the repository.",
        )

    feature_flag_signals = [
        "openfeature", "launchdarkly", "unleash", "flagsmith", "flipt",
        "featureflag", "feature_flag", "feature-toggle", "featuretoggle",
    ]
    if not any(signal in all_text.lower() or signal in " ".join(packages).lower() for signal in feature_flag_signals):
        add(
            "LUM026", "low", "low", "rollout",
            "No obvious feature-flag or migration-routing mechanism was detected.",
            "Risky slices may lack controlled cohort rollout or rapid fallback.",
            "For active modernization, choose a routing or flag mechanism proportional to risk and define owner, expiry, telemetry, and safe default.",
            "The project may use infrastructure-level routing or a proprietary flag service not visible in source.",
        )

    if not re.search(r"<meta\s+[^>]*name=['\"]viewport['\"]", all_text, re.I) and any(p.suffix.lower() in {".html", ".htm", ".cfm", ".php", ".jsp", ".aspx", ".cshtml"} for p in file_texts):
        add(
            "LUM027", "medium", "medium", "responsive",
            "No viewport meta declaration was detected in scanned templates.",
            "Mobile rendering and zoom behavior may not match modern expectations.",
            "Identify the shared layout template and define a zoom-permitting viewport configuration.",
            "The declaration may be injected by a parent template or framework shell.",
        )

    technology_signals = {
        "jQuery": r"\bjquery\b|\$\(",
        "AngularJS": r"\bng-(?:app|controller|repeat|model)\b|angular\.module",
        "React": r"\bReact\b|createRoot\s*\(|ReactDOM",
        "Vue": r"\bcreateApp\s*\(|new\s+Vue\s*\(|\bv-(?:if|for|model)\b",
        "Bootstrap legacy": r"\bdata-toggle\s*=|\bpanel-(?:default|primary)\b|\bform-group\b",
        "Bootstrap 5": r"\bdata-bs-toggle\s*=|\bvisually-hidden\b",
    }
    for label, pattern in technology_signals.items():
        if re.search(pattern, all_text, re.I):
            inventory["technologies"].add(label)

    findings.sort(key=lambda item: (-SEVERITY_ORDER[item["severity"]], item["rule_id"], item.get("file") or "", item.get("line") or 0))
    for item in findings:
        item.pop("_key", None)

    summary = Counter(item["severity"] for item in findings)
    result = {
        "tool": "inspect_legacy_ui.py",
        "project": str(root),
        "inventory": {
            "files_scanned": inventory["files_scanned"],
            "technologies": sorted(inventory["technologies"]),
            "server_templates": sorted(inventory["server_templates"]),
            "build_tools": sorted(inventory["build_tools"]),
            "packages_detected": len(inventory["packages"]),
            "test_signals": sorted(inventory["test_signals"]),
            "ci_signals": sorted(inventory["ci_signals"]),
        },
        "summary": {severity: summary.get(severity, 0) for severity in ("critical", "high", "medium", "low", "info")},
        "findings": findings,
        "limitations": [
            "Static evidence does not prove runtime use or absence.",
            "Manifest versions can differ from deployed artifacts and lockfiles.",
            "Generated, vendored, or templated code can create false positives.",
            "The report does not establish accessibility, security, or migration readiness by itself.",
        ],
    }

    if args.format == "json":
        report = json.dumps(result, indent=2, ensure_ascii=False)
    else:
        lines = [
            "# Legacy UI Modernization Inspection",
            "",
            f"Project: `{root}`",
            f"Files scanned: **{inventory['files_scanned']}**",
            "",
            "## Inventory",
            "",
            f"- Technologies: {', '.join(result['inventory']['technologies']) or 'none detected'}",
            f"- Server templates: {', '.join(result['inventory']['server_templates']) or 'none detected'}",
            f"- Build tools: {', '.join(result['inventory']['build_tools']) or 'none detected'}",
            f"- Packages detected: {result['inventory']['packages_detected']}",
            f"- Test signals: {', '.join(result['inventory']['test_signals']) or 'none detected'}",
            f"- CI signals: {', '.join(result['inventory']['ci_signals']) or 'none detected'}",
            "",
            "## Summary",
            "",
            "| Severity | Count |",
            "| --- | ---: |",
        ]
        for severity in ("critical", "high", "medium", "low", "info"):
            lines.append(f"| {severity.title()} | {result['summary'][severity]} |")
        lines.extend(["", "## Findings", ""])
        if not findings:
            lines.append("No findings were produced by the static checks.")
        for index, item in enumerate(findings, 1):
            location = item["file"] or "project"
            if item["line"]:
                location += f":{item['line']}"
            lines.extend([
                f"### {index}. [{item['severity'].upper()}] {item['rule_id']} - {item['message']}",
                "",
                f"- Area: `{item['area']}`",
                f"- Confidence: `{item['confidence']}`",
                f"- Location: `{location}`",
                f"- Evidence: `{item['evidence'] or 'project-level signal'}`",
                f"- Impact: {item['impact']}",
                f"- Remediation: {item['remediation']}",
                f"- Manual check: {item['manual_check']}",
                "",
            ])
        lines.extend(["## Limitations", ""] + [f"- {value}" for value in result["limitations"]])
        report = "\n".join(lines) + "\n"

    if args.output:
        output = Path(args.output)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text(report, encoding="utf-8")
    else:
        print(report, end="")

    if args.fail_on:
        threshold = SEVERITY_ORDER[args.fail_on]
        if any(SEVERITY_ORDER[item["severity"]] >= threshold for item in findings):
            return 1
    return 0


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