"""Specialist: path traversal via unvalidated file path construction."""

from __future__ import annotations

import ast
from autodebug.agent.specialists.base import ASTData, Finding, Specialist


class PathTraversalSpecialist(Specialist):
    name = "path_traversal"
    trigger_features = ["has_file_ops"]

    async def run(self, source: str, ast_data: ASTData) -> list[Finding]:
        findings: list[Finding] = []
        lines = ast_data.source_lines

        for node in ast.walk(ast_data.tree):
            if not isinstance(node, ast.Call):
                continue

            # Match open(), os.path.join(), Path()
            func_name = None
            if isinstance(node.func, ast.Name) and node.func.id == "open":
                func_name = "open"
            elif (isinstance(node.func, ast.Attribute)
                  and node.func.attr == "join"
                  and isinstance(node.func.value, ast.Attribute)
                  and node.func.value.attr == "path"):
                func_name = "os.path.join"
            elif isinstance(node.func, ast.Name) and node.func.id == "Path":
                func_name = "Path"

            if not func_name:
                continue

            if not node.args:
                continue

            # Check if ALL arguments are constants → safe
            all_const = all(isinstance(a, ast.Constant) for a in node.args)
            if all_const:
                continue

            # Check if there's a path validation nearby (within 10 lines before)
            check_start = max(0, node.lineno - 11)
            check_end = node.lineno - 1
            preceding = "\n".join(lines[check_start:check_end]).lower()

            has_validation = any(kw in preceding for kw in (
                "realpath", "resolve()", "is_relative_to",
                "startswith(", "abspath", "normpath",
                ".." in preceding and "reject" in preceding,
            ))
            if has_validation:
                continue

            # Check if path comes from a function parameter (external input)
            has_param_arg = False
            func_range = None
            enclosing = None
            for fname, (start, end) in ast_data.function_ranges.items():
                if start <= node.lineno <= end:
                    enclosing = fname
                    func_range = (start, end)
                    break

            if enclosing and func_range:
                # Get function parameter names
                def_line = lines[func_range[0] - 1] if func_range[0] <= len(lines) else ""
                for arg in node.args:
                    if isinstance(arg, ast.Name):
                        # Check if this variable name is a function parameter
                        if any(kw in arg.id.lower() for kw in ("path", "file", "name", "dir")):
                            has_param_arg = True
                            break

            if not has_param_arg:
                continue  # path from internal source, not external

            # LLM micro-call for ambiguous cases
            ctx_start = max(0, node.lineno - 5)
            ctx_end = min(len(lines), node.lineno + 3)
            context = "\n".join(lines[ctx_start:ctx_end])

            verdict = await self.llm_ask(
                question="Is this file path constructed from user/external input without validation?",
                context=context,
                examples=[
                    (
                        "def download(filename):\n    path = os.path.join(UPLOAD_DIR, filename)\n    return open(path).read()",
                        "FLAG: filename from user input, no traversal check",
                    ),
                    (
                        "def read_config():\n    path = os.path.join(CONFIG_DIR, 'settings.json')\n    return open(path).read()",
                        "SKIP: hardcoded filename, no user input",
                    ),
                ],
            )

            if verdict.flag:
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message=f"`{func_name}()` with unvalidated path: {verdict.reason}",
                    specialist=self.name,
                    confidence="LIKELY",
                ))

        return findings
