"""Code reader for LLM-guided security analysis."""

import os
import re
from pathlib import Path

# SKIPLIST APPROACH: Scan all files EXCEPT those in the skip lists below.
# This ensures we don't miss unusual config files or framework-specific extensions.

# File extensions to SKIP (binary files, media, compiled/generated output)
SKIP_EXTENSIONS = {
    # Binary executables & libraries
    ".exe", ".dll", ".so", ".dylib", ".a", ".lib", ".bin", ".app",
    # Compiled code
    ".pyc", ".pyo", ".pyd", ".o", ".obj", ".class", ".jar", ".war", ".ear",
    # Archives & compressed files
    ".zip", ".tar", ".gz", ".bz2", ".xz", ".7z", ".rar", ".iso", ".dmg",
    # Images
    ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".ico", ".webp", ".tiff", ".psd",
    # Video & audio
    ".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv", ".webm",
    ".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a",
    # Fonts
    ".ttf", ".otf", ".woff", ".woff2", ".eot",
    # Documents (typically not source code)
    ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
    # Database files
    ".db", ".sqlite", ".sqlite3", ".mdb",
    # Minified/bundled output (too large, not source)
    ".min.js", ".min.css", ".bundle.js", ".chunk.js",
    # Maps
    ".map", ".js.map", ".css.map",
    # Lockfiles for package managers (scanned separately by dep_scanner)
    # We keep package.json but skip large lockfiles since dep_scanner handles them
    ".lock",  # General lockfiles - be careful, this might be too broad
}

# Directories to skip.
# NOTE: .github is intentionally NOT listed here — workflow files under
# .github/workflows/ are a common attack surface (CI/CD injection, secret
# exfiltration) and must be scanned.  Only the actual git object store (.git)
# is excluded.
SKIP_DIRS = {
    "node_modules", "venv", ".venv", "env", "__pycache__",
    ".git", ".svn", ".hg", "dist", "build", ".next", "coverage",
    ".tox", ".mypy_cache", ".pytest_cache", "eggs", ".eggs",
    "vendor", "third_party", "external", ".gradle", "target",
    # IDE / tooling dotdirs — not security-relevant
    ".vscode", ".idea", ".husky", ".turbo",
}

# Sensitive file patterns to NEVER read (simple substring/suffix checks)
SENSITIVE_SUBSTRINGS = [
    ".env", ".envrc", "config.local", "settings.local", "local_settings",
    "credentials", "secrets", ".secret", "password", "api_key", "api-key",
    "auth_token", "auth-token", "aws_credentials", ".aws/", "gcloud",
    "service_account", "database.yml", "db.json", "docker-compose.override",
]

SENSITIVE_SUFFIXES = [
    ".pem", ".key", ".p12", ".pfx", ".jks",
    "id_rsa", "id_dsa", "id_ecdsa", "id_ed25519",
    ".htpasswd", "shadow", "passwd", "known_hosts", "authorized_keys",
]

MAX_FILE_SIZE = 100 * 1024  # 100KB

_API_SIGNAL_RE = re.compile(
    r'fetch\s*\('
    r'|axios\.'
    r'|requests\.(get|post|put|patch|delete|head|options)\b'
    r'|urllib\.request\b'
    r'|httpx\.'
    r'|aiohttp\.'
    r'|http\.(get|post|put|patch|delete|request)\s*\('
    r'|\$\.ajax\s*\('
    r'|\$http\.'
    r'|superagent\b'
    r'|got\s*\('
    r'|node-fetch'
    r'|@(Get|Post|Put|Patch|Delete|Head|Options)\s*\('
    r'|router\.(get|post|put|patch|delete|all)\s*\('
    r'|app\.(get|post|put|patch|delete)\s*\('
    r'|Route\s*\('
    r'|https?://'
    r'|BASE_URL\b|API_URL\b|baseURL\b|api_base\b',
    re.IGNORECASE,
)


def _has_api_signals(path: Path) -> bool:
    """Return True only if the file contains HTTP API call patterns."""
    try:
        content = path.read_text(encoding="utf-8", errors="ignore")
        return bool(_API_SIGNAL_RE.search(content))
    except Exception:
        return False


def is_sensitive_file(file_path: Path) -> bool:
    """Check if a file path matches sensitive patterns."""
    path_str = str(file_path).lower()
    name = file_path.name.lower()
    # Catch all .env variants: .env, .env.local, .env.production, env.local, etc.
    if name.startswith(".env") or name.startswith("env."):
        return True
    for token in SENSITIVE_SUBSTRINGS:
        if token in path_str or token in name:
            return True
    for suffix in SENSITIVE_SUFFIXES:
        if name.endswith(suffix) or path_str.endswith(suffix):
            return True
    return False


def _is_candidate_file(file_path: Path) -> bool:
    """Return True if a file should be included in the scan (skiplist approach)."""
    name = file_path.name.lower()
    suffix = file_path.suffix.lower()

    # Skip binary/media/generated file extensions
    if suffix in SKIP_EXTENSIONS:
        return False

    # Skip minified files (special case for double extensions like .min.js)
    if ".min." in name or ".bundle." in name or ".chunk." in name:
        return False

    # Skip lockfiles - they're handled by dep_scanner
    if name in {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock",
                "gemfile.lock", "cargo.lock", "go.sum", "composer.lock", "mix.lock"}:
        return False

    # Accept everything else (text-based files, source code, config, etc.)
    return True


def collect_scan_files(
    target_path: str,
    api_signals_only: bool = False,
) -> tuple[list[str], list[str]]:
    """
    Collect files for LLM-guided analysis.

    Args:
        target_path: Directory or file to scan.
        api_signals_only: When True, skip files that contain no HTTP API call
            patterns. Only enable this for API-extraction scans; leave False
            for security scans and test-generation scans that need all files.

    Returns:
        - files: list of absolute file paths
        - info: list of info/error messages
    """
    files: list[str] = []
    info: list[str] = []
    skipped_sensitive = 0
    skipped_large = 0
    skipped_no_signals = 0

    path = Path(target_path)

    if not path.exists():
        info.append(f"Path does not exist: {target_path}")
        return files, info

    def _maybe_add_file(file_path: Path) -> None:
        nonlocal skipped_sensitive, skipped_large, skipped_no_signals
        if is_sensitive_file(file_path):
            skipped_sensitive += 1
            return
        if file_path.is_file():
            try:
                if file_path.stat().st_size > MAX_FILE_SIZE:
                    skipped_large += 1
                    return
            except Exception:
                return
            if api_signals_only and not _has_api_signals(file_path):
                skipped_no_signals += 1
                return
            files.append(str(file_path))

    if path.is_file():
        _maybe_add_file(path)
    else:
        for root, dirs, filenames in os.walk(path):
            # Filter using SKIP_DIRS only — the blanket ".startswith('.')" check
            # was removed so that .github/workflows/ gets scanned.
            dirs[:] = [d for d in dirs if d not in SKIP_DIRS]

            for filename in filenames:
                file_path = Path(root) / filename
                if not _is_candidate_file(file_path):
                    continue
                _maybe_add_file(file_path)

    if skipped_sensitive > 0:
        info.append(f"Skipped {skipped_sensitive} sensitive file(s)")
    if skipped_large > 0:
        info.append(f"Skipped {skipped_large} large file(s) > {MAX_FILE_SIZE} bytes")
    if skipped_no_signals > 0:
        info.append(f"Skipped {skipped_no_signals} file(s) with no API signals")

    info.append(f"Collected {len(files)} files for analysis")

    files = sorted(set(files))
    return files, info


# ---------------------------------------------------------------------------
# Smart file ordering for test generation
# ---------------------------------------------------------------------------

# Tier 0: Package manifests — tell the LLM what framework the app uses.
_TIER_0_NAMES = {
    "package.json", "composer.json", "cargo.toml", "pyproject.toml",
    "gemfile", "build.gradle", "pom.xml",
}

# Tier 1: Router / routing files
_TIER_1_PATTERNS = [
    re.compile(r"(^|/)routes?\.(ts|tsx|js|jsx|py|rb)$", re.I),
    re.compile(r"(^|/)router\.(ts|tsx|js|jsx)$", re.I),
    re.compile(r"(^|/)router/(index|routes)\.(ts|tsx|js|jsx)$", re.I),
    re.compile(r"(^|/)app\.(tsx|jsx)$", re.I),
    re.compile(r"(^|/)_app\.(tsx|jsx|ts|js)$", re.I),
    re.compile(r"(^|/)app/layout\.(tsx|jsx|ts|js)$", re.I),
    re.compile(r"(^|/)middleware\.(ts|js)$", re.I),
    re.compile(r"(^|/)urls\.py$", re.I),
    re.compile(r"(^|/)web\.php$", re.I),
    re.compile(r"(^|/)nuxt\.config\.(ts|js)$", re.I),
    re.compile(r"(^|/)next\.config\.(ts|js|mjs)$", re.I),
    re.compile(r"(^|/)angular\.json$", re.I),
]

# Tier 2: Entry points and layouts
_TIER_2_PATTERNS = [
    re.compile(r"(^|/)(index|main)\.(ts|tsx|js|jsx)$", re.I),
    re.compile(r"(^|/)layout\.(ts|tsx|js|jsx)$", re.I),
    re.compile(r"(^|/)_document\.(ts|tsx|js|jsx)$", re.I),
    re.compile(r"(^|/)root\.(tsx|jsx)$", re.I),
    re.compile(r"(^|/)App\.(vue|svelte)$", re.I),
]

# Tier 3: Navigation components
_TIER_3_PATTERNS = [
    re.compile(
        r"(navbar|nav[-_]?bar|sidebar|side[-_]?bar|header|footer|menu"
        r"|navigation|breadcrumb|topbar|top[-_]?bar|drawer)",
        re.I,
    ),
]

# Tier 4: Auth / guards / middleware
_TIER_4_PATTERNS = [
    re.compile(
        r"(auth|login|signin|sign[-_]?in|sign[-_]?up|register|logout"
        r"|session|guard|private[-_]?route|protected[-_]?route"
        r"|require[-_]?auth|with[-_]?auth|use[-_]?auth|use[-_]?session)",
        re.I,
    ),
]

# Tier 5: Page / view components (identified by directory name)
_TIER_5_PATTERNS = [
    re.compile(r"(^|/)(pages?|views?|screens?|routes?)/", re.I),
]


def _file_priority_tier(filepath: str) -> int:
    """Return a numeric priority tier for a file (lower = higher priority)."""
    name = Path(filepath).name.lower()

    if name in _TIER_0_NAMES:
        return 0
    rel = filepath.lower()
    for tier, patterns in [
        (1, _TIER_1_PATTERNS),
        (2, _TIER_2_PATTERNS),
        (3, _TIER_3_PATTERNS),
        (4, _TIER_4_PATTERNS),
        (5, _TIER_5_PATTERNS),
    ]:
        for pat in patterns:
            if pat.search(rel):
                return tier
    return 6


def _parse_file_imports(abs_path: str) -> list[str]:
    """Extract relative import paths (./foo, ../bar) from a source file."""
    try:
        content = Path(abs_path).read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return []
    imports: list[str] = []
    # ES module: from './foo' or from "../bar"
    for m in re.finditer(r'from\s+["\'](\./|\.\./)([^"\']+)["\']', content):
        imports.append(m.group(1) + m.group(2))
    # CommonJS: require('./foo')
    for m in re.finditer(r'require\(\s*["\'](\./|\.\./)([^"\']+)["\']\s*\)', content):
        imports.append(m.group(1) + m.group(2))
    return imports


def _resolve_import_candidates(from_rel: str, import_str: str, file_set: set[str]) -> list[str]:
    """Resolve a relative import string to candidate paths present in file_set."""
    base_dir = os.path.dirname(from_rel)
    raw = os.path.normpath(os.path.join(base_dir, import_str)).replace("\\", "/")
    candidates = [
        raw,
        raw + ".ts", raw + ".tsx", raw + ".js", raw + ".jsx",
        raw + "/index.ts", raw + "/index.tsx",
        raw + "/index.js", raw + "/index.jsx",
    ]
    return [c for c in candidates if c in file_set]


# Patterns that identify page/route entry files for common frameworks.
_PAGE_ENTRY_PATTERNS = [
    re.compile(r"(^|[/\\])pages?[/\\]", re.I),          # Next.js pages/, pages/
    re.compile(r"(^|[/\\])app[/\\].*\.(tsx?|jsx?)$", re.I),  # Next.js 13+ app router
    re.compile(r"(^|[/\\])_app\.(tsx?|jsx?)$", re.I),   # Next.js _app
    re.compile(r"(^|[/\\])_document\.(tsx?|jsx?)$", re.I),
    re.compile(r"(^|[/\\])views?[/\\]", re.I),           # Vue / Rails views
    re.compile(r"(^|[/\\])screens?[/\\]", re.I),         # React Native screens
]


def build_import_distances(
    rel_files: list[str],
    base_for_rel: "Path",
    max_depth: int = 10,
) -> dict[str, int]:
    """BFS from page entry files through import edges.

    Returns {relative_path: distance} where distance is the minimum number of
    import hops from any page/route entry file.  Page entry files themselves
    have distance 0.  Files not reachable from any page entry are absent.

    This is used to rank files that match the same flow keyword: a component
    directly imported by a page (distance=1) should be read before a similarly
    named component that is never imported by any page (distance=inf).
    """
    file_set = set(rel_files)

    # Seed: all files that look like page entry points
    seeds = [f for f in rel_files if any(p.search(f) for p in _PAGE_ENTRY_PATTERNS)]
    if not seeds:
        return {}

    distances: dict[str, int] = {s: 0 for s in seeds}
    frontier = list(seeds)

    for depth in range(1, max_depth + 1):
        next_frontier: list[str] = []
        for rel in frontier:
            abs_path = str(base_for_rel / rel)
            for imp in _parse_file_imports(abs_path):
                for resolved in _resolve_import_candidates(rel, imp, file_set):
                    if resolved not in distances:
                        distances[resolved] = depth
                        next_frontier.append(resolved)
        if not next_frontier:
            break
        frontier = next_frontier

    return distances


def prioritize_files_for_test_scan(
    files: list[str],
    import_distances: dict[str, int] | None = None,
) -> list[str]:
    """Sort files by importance tier for test generation scanning.

    Ensures the LLM sees routing, entry points, navigation, and auth
    files before utility code and deep components.  Within each tier,
    files are sorted by import distance from page entry files (closest
    first), then grouped by parent directory so related components land
    in the same batch.

    import_distances is optional.  When provided (from build_import_distances),
    files directly imported by pages are promoted above same-tier files that
    are never imported by any page.
    """
    _INF = 9999
    return sorted(set(files), key=lambda f: (
        _file_priority_tier(f),
        import_distances.get(f, _INF) if import_distances else _INF,
        str(Path(f).parent),  # group by directory
        f,
    ))


# ---------------------------------------------------------------------------
# Test-generation specific file filter
# ---------------------------------------------------------------------------

# Directories that are never useful for UI test generation.
# These are in addition to SKIP_DIRS (which covers node_modules, build, etc.)
_TEST_GEN_SKIP_DIRS = {
    # Test / spec directories — we're generating tests, not reading existing ones
    "test", "tests", "__tests__", "spec", "specs", "e2e", "cypress",
    "playwright", "jest", "fixtures", "mocks", "__mocks__", "stubs",
    # Database / backend infra
    "migrations", "seeds", "seeders", "alembic", "db", "database",
    "prisma", "drizzle",
    # Docs / storybook / demos
    "storybook", "stories", "__stories__", "docs", "documentation",
    "demo", "examples", "sandbox",
    # CI/CD and config dirs not relevant to UI flows
    "scripts", "tools", "bin", "cli",
    # Generated / compiled assets
    "generated", "gen", "auto-generated", "public/static", "static",
    "assets/vendor",
    # Python backend internals
    "alembic", "management", "commands",
}

# File name suffixes/patterns that are never relevant for UI test generation.
# Matched against lowercased filename.
_TEST_GEN_SKIP_NAME_PATTERNS = re.compile(
    r"(^|[._-])("
    # Test / spec files
    r"test|spec|e2e|integration|unit|__test__|\.test|\.spec"
    r"|stories|story|\.stories|\.story"
    # Type-only files (no runtime UI)
    r"|\.d\.ts"
    # Generated / schema files
    r"|\.generated\.|schema\.graphql|introspection\.json"
    # Config / tooling files that don't describe UI
    r"|jest\.config|vitest\.config|playwright\.config|cypress\.config"
    r"|tailwind\.config|postcss\.config|babel\.config|webpack\.config"
    r"|vite\.config|rollup\.config|eslint|prettier|tsconfig|jsconfig"
    r"|\.eslintrc|\.prettierrc|\.babelrc|\.stylelintrc"
    # CI/CD
    r"|dockerfile|\.dockerignore|\.gitignore|\.gitattributes"
    r"|\.nvmrc|\.tool-versions|\.editorconfig"
    # Changelog / license
    r"|changelog|license|contributing|readme|todo"
    r")(\.|$)",
    re.IGNORECASE,
)

# File extensions that carry no UI-relevant information.
_TEST_GEN_SKIP_EXTENSIONS = {
    ".md", ".mdx", ".txt", ".rst",          # docs
    ".sql", ".prisma",                        # DB schema
    ".sh", ".bash", ".zsh", ".fish",          # shell scripts
    ".Dockerfile",                            # infra
    ".csv", ".tsv", ".jsonl",                # data files
    ".graphql", ".gql",                      # GraphQL schema (no UI selectors)
    ".proto",                                 # protobuf
    ".snap",                                  # Jest snapshots
    ".log",                                   # log files
}


def filter_files_for_test_generation(files: list[str]) -> tuple[list[str], int]:
    """Remove files irrelevant to UI test generation.

    Drops test/spec files, migration files, docs, config-only files,
    type declaration files, storybook stories, and other content that
    cannot contribute selectors, routes, or UI flows to the LLM.

    Args:
        files: List of relative or absolute file paths.

    Returns:
        (kept_files, skipped_count) — kept_files in original order.
    """
    kept: list[str] = []
    skipped = 0

    for filepath in files:
        path = Path(filepath)
        name = path.name.lower()
        suffix = path.suffix.lower()

        # Skip by extension
        if suffix in _TEST_GEN_SKIP_EXTENSIONS:
            skipped += 1
            continue

        # Skip .d.ts declaration files
        if name.endswith(".d.ts"):
            skipped += 1
            continue

        # Skip by directory — check every path component
        parts_lower = {p.lower() for p in path.parts}
        if parts_lower & _TEST_GEN_SKIP_DIRS:
            skipped += 1
            continue

        # Skip by filename pattern
        if _TEST_GEN_SKIP_NAME_PATTERNS.search(name):
            # Exception: keep auth/login/session-related files even if their
            # name matches a generic pattern (e.g. "auth.test-utils.ts" is
            # a helper, but "login.stories.ts" is still a story and skipped).
            # For now, be strict — if the pattern matches, skip it.
            skipped += 1
            continue

        kept.append(filepath)

    return kept, skipped


def get_file_contents(file_paths: list[str]) -> dict[str, str]:
    """
    Read contents of specific files.
    Use this after collect_scan_files to get content only for files you need.

    Returns:
        Dict mapping file path to content (empty string if read failed)
    """
    contents: dict[str, str] = {}
    for file_path in file_paths:
        path = Path(file_path)
        if path.exists() and not is_sensitive_file(path):
            content = _read_file_content(path)
            contents[file_path] = content if content else ""
    return contents


def _read_file_content(path: Path) -> str | None:
    """Read file content, return None on error."""
    try:
        size = path.stat().st_size
        if size > MAX_FILE_SIZE:
            return None
        return path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return None


# ---------------------------------------------------------------------------
# App domain detection
# ---------------------------------------------------------------------------

# Heuristic signals for common app domains.
# Each domain has: dependency names (from package.json etc.), directory patterns,
# and route patterns.  A domain is detected when enough signals are present.

_DOMAIN_SIGNALS: dict[str, dict[str, list[str]]] = {
    "e-commerce": {
        "deps": [
            "stripe", "@stripe", "shopify", "snipcart", "commerce.js",
            "@commercetools", "medusa", "saleor", "woocommerce",
        ],
        "dirs": [
            "cart", "checkout", "products", "shop", "store", "orders",
            "catalog", "wishlist", "payment",
        ],
        "routes": [
            "/cart", "/checkout", "/products", "/shop", "/orders",
            "/wishlist", "/payment",
        ],
    },
    "cms": {
        "deps": [
            "@tinymce", "tinymce", "draft-js", "slate", "quill",
            "ckeditor", "tiptap", "editorjs", "@strapi", "sanity",
            "contentful", "prismic",
        ],
        "dirs": [
            "posts", "articles", "editor", "content", "blog",
            "pages", "media", "publish",
        ],
        "routes": [
            "/posts", "/articles", "/editor", "/blog", "/content",
            "/publish",
        ],
    },
    "social": {
        "deps": [
            "socket.io", "pusher", "@stream-io", "getstream",
        ],
        "dirs": [
            "feed", "timeline", "follow", "notifications", "chat",
            "messages", "friends", "groups",
        ],
        "routes": [
            "/feed", "/timeline", "/notifications", "/chat",
            "/messages", "/friends",
        ],
    },
    "saas-dashboard": {
        "deps": [
            "recharts", "chart.js", "@nivo", "d3", "apexcharts",
            "ag-grid", "@tanstack/react-table",
        ],
        "dirs": [
            "dashboard", "analytics", "reports", "billing",
            "settings", "admin", "workspace", "team",
        ],
        "routes": [
            "/dashboard", "/analytics", "/reports", "/billing",
            "/settings", "/admin", "/workspace",
        ],
    },
}

# Domain-specific flow hints appended to analysis instructions.
_DOMAIN_FLOW_HINTS: dict[str, str] = {
    "e-commerce": (
        "DOMAIN DETECTED: E-COMMERCE — prioritize these critical user flows:\n"
        "- Product browsing (search, filter, category navigation)\n"
        "- Product detail page (images, description, reviews, variants/sizes)\n"
        "- Add to cart (with quantity, variant selection)\n"
        "- Cart management (update quantity, remove item, view totals)\n"
        "- Checkout flow (shipping address, payment, order confirmation)\n"
        "- User account (order history, saved addresses, wishlist)\n"
    ),
    "cms": (
        "DOMAIN DETECTED: CMS / CONTENT MANAGEMENT — prioritize these critical user flows:\n"
        "- Create new content (post, article, page) with rich text editor\n"
        "- Edit existing content (update fields, change status)\n"
        "- Publish / unpublish content\n"
        "- Media upload and management\n"
        "- Content listing with filters and search\n"
        "- User roles and permissions\n"
    ),
    "social": (
        "DOMAIN DETECTED: SOCIAL / COMMUNITY — prioritize these critical user flows:\n"
        "- User registration and profile setup\n"
        "- Content creation (posts, comments, replies)\n"
        "- Social interactions (follow, like, share)\n"
        "- Feed / timeline viewing and scrolling\n"
        "- Notifications\n"
        "- Chat / messaging\n"
    ),
    "saas-dashboard": (
        "DOMAIN DETECTED: SAAS / DASHBOARD — prioritize these critical user flows:\n"
        "- Dashboard overview and widget interactions\n"
        "- Data table operations (sort, filter, paginate, export)\n"
        "- Settings management (account, team, billing)\n"
        "- Report generation and viewing\n"
        "- User/team management (invite, roles, permissions)\n"
        "- Search and filtering across the application\n"
    ),
}


def detect_app_domain(
    files: list[str],
    package_json_content: str | None = None,
) -> dict:
    """Detect the application domain using heuristic signals.

    Analyzes file paths and optionally package.json dependencies to
    determine the app type (e-commerce, CMS, social, SaaS dashboard).

    Returns:
        {"domain": "e-commerce", "confidence": "high", "signals": [...]}
        or {"domain": None} if no domain detected.
    """
    import json as _json

    # Collect dependency names from package.json
    dep_names: set[str] = set()
    if package_json_content:
        try:
            pkg = _json.loads(package_json_content)
            for dep_key in ("dependencies", "devDependencies", "peerDependencies"):
                dep_names.update(pkg.get(dep_key, {}).keys())
        except Exception:
            pass

    # Collect directory names from file paths
    dir_names: set[str] = set()
    for f in files:
        for part in Path(f).parts:
            dir_names.add(part.lower())

    # Score each domain
    scores: dict[str, tuple[int, list[str]]] = {}
    for domain, signals in _DOMAIN_SIGNALS.items():
        score = 0
        found: list[str] = []

        for dep in signals["deps"]:
            if dep.lower() in dep_names:
                score += 2  # dependency matches are strong signals
                found.append(f"dep:{dep}")

        for d in signals["dirs"]:
            if d.lower() in dir_names:
                score += 1
                found.append(f"dir:{d}")

        scores[domain] = (score, found)

    # Pick the highest scoring domain
    best_domain = max(scores, key=lambda d: scores[d][0])
    best_score, best_signals = scores[best_domain]

    if best_score == 0:
        return {"domain": None}

    confidence = "high" if best_score >= 4 else "medium" if best_score >= 2 else "low"

    return {
        "domain": best_domain,
        "confidence": confidence,
        "signals": best_signals,
    }


def get_domain_flow_hints(domain: str) -> str | None:
    """Return domain-specific flow hints for analysis instructions."""
    return _DOMAIN_FLOW_HINTS.get(domain)


# Legacy function for backwards compatibility
