"""TS-IDOR-001: Insecure Direct Object Reference for TypeScript/JavaScript.

Detects patterns where user-supplied IDs or paths from args are used
to access objects directly without authorization checks.

OWASP Agentic: Agents that accept object references from tool arguments
and pass them straight to database lookups or file access without
verifying the caller is authorized to access that specific object.
"""

from __future__ import annotations

import re

from . import TSRule
from ..models import Finding, Severity

# Database lookup with user-supplied ID (e.g. db.findById(args.id))
DB_FIND_BY_ID = re.compile(
    r'\b(?:db|collection|model|repo|repository|store|table|prisma|knex|'
    r'mongoose|sequelize|typeorm|drizzle)\s*\.\s*'
    r'(?:findById|findOne|findUnique|findFirst|get|getOne|fetch|'
    r'findByPk|where|deleteOne|updateOne|removeOne|destroyOne)\s*\(\s*'
    r'(?:args|params|input|req\.(?:params|query|body))\s*\.\s*\w+'
)

# Collection/map direct access with user-supplied key
COLLECTION_GET = re.compile(
    r'\b(?:collection|records|users|documents|items|data|cache|store|map)\s*'
    r'(?:\.\s*get\s*\(|\[)\s*'
    r'(?:args|params|input|req\.(?:params|query|body))\s*\.\s*\w+'
)

# File access with user-supplied path (without prior validation)
FILE_ACCESS_ARGS = re.compile(
    r'\b(?:fs|fsPromises)\s*\.\s*'
    r'(?:readFile|readFileSync|writeFile|writeFileSync|'
    r'unlink|unlinkSync|access|accessSync|stat|statSync|'
    r'createReadStream|createWriteStream|open|openSync|'
    r'rm|rmSync|rename|renameSync)\s*\(\s*'
    r'(?:args|params|input)\s*\.\s*(?:path|file|filename|filePath|dir)'
)

# Authorization check patterns that indicate proper access control
AUTHZ_CHECK_PATTERNS = re.compile(
    r'(?:authorize|checkOwner|isOwner|verifyOwner|'
    r'canAccess|hasAccess|checkAccess|verifyAccess|'
    r'checkPermission|hasPermission|verifyPermission|'
    r'belongsTo|ownedBy|assertOwner|'
    r'\.userId\s*===|\.userId\s*!==|'
    r'\.ownerId\s*===|\.createdBy\s*===|'
    r'req\.user\.id\s*===|session\.userId\s*===|'
    r'where\s*.*userId|where\s*.*ownerId)',
    re.IGNORECASE,
)


class TSIDORRule(TSRule):
    """Detects Insecure Direct Object Reference patterns in TypeScript."""

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

    @property
    def name(self) -> str:
        return "Insecure Direct Object Reference"

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

    @property
    def cwe(self) -> str:
        return "CWE-639"

    @property
    def description(self) -> str:
        return (
            "User-supplied IDs or paths used to access objects directly "
            "without authorization checks."
        )

    @property
    def detailed_description(self) -> str:
        return (
            "User-supplied IDs or paths used to access objects directly "
            "without authorization checks.\n\n"
            "In MCP tool handlers, arguments come from the LLM client.\n"
            "Using args.id or args.path to directly look up database\n"
            "records or files allows any caller to access any object.\n\n"
            "Examples of vulnerable code:\n"
            "  const user = await db.findById(args.id);\n"
            "  const doc = collection.get(args.userId);\n"
            "  const data = fs.readFileSync(args.path);\n\n"
            "How to fix:\n"
            "  - Verify the caller owns or has access to the object\n"
            "  - Check object ownership against session/token user ID\n"
            "  - Use scoped queries: db.find({ id: args.id, ownerId: user.id })\n"
            "  - Implement per-resource authorization checks"
        )

    def check(self, source: str) -> list[Finding]:
        """Check for IDOR patterns."""
        findings: list[Finding] = []
        has_authz = bool(AUTHZ_CHECK_PATTERNS.search(source))
        lines = self._get_lines(source)

        for i, line in enumerate(lines, 1):
            if self._is_comment_line(line):
                continue

            matched = False
            detail = ""

            if DB_FIND_BY_ID.search(line):
                matched = True
                detail = (
                    "Database lookup using user-supplied ID without "
                    "ownership verification."
                )

            elif COLLECTION_GET.search(line):
                matched = True
                detail = (
                    "Collection access using user-supplied key without "
                    "authorization check."
                )

            elif FILE_ACCESS_ARGS.search(line):
                matched = True
                detail = (
                    "File system access using user-supplied path without "
                    "authorization check."
                )

            if matched and not has_authz:
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    title="Insecure Direct Object Reference",
                    description=detail,
                    file="",
                    line=i,
                    code_snippet=line.strip()[:80],
                    recommendation=(
                        "Add ownership/authorization check before accessing "
                        "the object. Verify the caller has permission to "
                        "access this specific resource."
                    ),
                    cwe=self.cwe,
                ))

        return findings
