"""FILE-001: Unsafe File Write detection rule."""

from __future__ import annotations

import ast

from . import Rule
from ..models import Finding, Severity

WRITE_MODES = {"w", "a", "wb", "ab", "w+", "a+", "wb+", "ab+", "r+", "rb+"}

SHUTIL_FUNCS = {"shutil.copy", "shutil.copy2", "shutil.move", "shutil.copyfile"}


def _is_dynamic(node: ast.expr) -> bool:
    return isinstance(node, (ast.Name, ast.JoinedStr, ast.BinOp, ast.Subscript))


def _is_write_mode(node: ast.expr) -> bool:
    if isinstance(node, ast.Constant) and isinstance(node.value, str):
        return node.value in WRITE_MODES
    return False


class UnsafeFileWriteRule(Rule):
    """Detects file write operations with dynamic paths."""

    @property
    def rule_id(self) -> str:
        return "FILE-001"

    @property
    def name(self) -> str:
        return "Unsafe File Write"

    @property
    def severity(self) -> Severity:
        return Severity.HIGH

    @property
    def description(self) -> str:
        return "File write with dynamic path may allow arbitrary file overwrite."

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        findings: list[Finding] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.Call):
                continue
            func_name = self._get_func_name(node)
            if func_name == "open" and len(node.args) >= 2:
                path_arg, mode_arg = node.args[0], node.args[1]
                if _is_dynamic(path_arg) and _is_write_mode(mode_arg):
                    findings.append(self._make_finding(func_name, node, source))
            elif func_name == "open" and node.keywords:
                mode_kw = next((k for k in node.keywords if k.arg == "mode"), None)
                if mode_kw and _is_write_mode(mode_kw.value) and node.args and _is_dynamic(node.args[0]):
                    findings.append(self._make_finding(func_name, node, source))
            elif isinstance(node.func, ast.Attribute) and node.func.attr == "open":
                for kw in node.keywords:
                    if kw.arg == "mode" and _is_write_mode(kw.value):
                        findings.append(self._make_finding("Path.open", node, source))
                        break
                else:
                    if len(node.args) >= 1 and isinstance(node.args[0], ast.Constant):
                        if isinstance(node.args[0].value, str) and node.args[0].value in WRITE_MODES:
                            findings.append(self._make_finding("Path.open", node, source))
            elif func_name in SHUTIL_FUNCS and len(node.args) >= 2:
                if _is_dynamic(node.args[1]):
                    findings.append(self._make_finding(func_name, node, source))
        return findings

    def _make_finding(self, func: str, node: ast.Call, source: str) -> Finding:
        return Finding(
            rule_id=self.rule_id,
            severity=self.severity,
            title="Unsafe file write with dynamic path",
            description=f"{func}() with dynamic path in write mode.",
            file="",
            line=node.lineno,
            code_snippet=self._get_snippet(source, node.lineno),
            recommendation="Validate and sanitize file paths, use a sandboxed directory.",
            cwe="CWE-73",
        )
