"""Checker for innerHTML XSS vulnerabilities."""

import re
from pathlib import Path
from typing import List

from stablestack.checkers.base import BaseChecker
from stablestack.models import Finding, Severity, Tier


class InnerHtmlXssChecker(BaseChecker):
    """Detects potentially unsafe innerHTML usage that could lead to XSS."""

    rule_id = "SEC005"
    rule_name = "inner-html-xss"
    description = "Direct innerHTML assignment can lead to XSS vulnerabilities"
    why = (
        "Setting innerHTML with user-controlled content is one of the most common XSS "
        "vulnerabilities. Attackers can inject malicious scripts that steal cookies, "
        "session tokens, or perform actions on behalf of users. Use textContent for "
        "plain text, or sanitize HTML with a library like DOMPurify."
    )
    severity = Severity.ERROR
    tier = Tier.CRITICAL
    file_extensions = {".js", ".ts", ".jsx", ".tsx"}

    # Patterns for innerHTML usage
    _INNER_HTML_ASSIGNMENT = re.compile(
        r'\.innerHTML\s*=\s*(?![\s]*["\'][\s]*["\'])',  # .innerHTML = (not empty string)
        re.MULTILINE
    )
    _INNER_HTML_VARIABLE = re.compile(
        r'\.innerHTML\s*=\s*[a-zA-Z_$]',  # .innerHTML = variable
        re.MULTILINE
    )
    _DANGEROUS_SET_HTML = re.compile(
        r'dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:',  # React's dangerouslySetInnerHTML
        re.MULTILINE
    )

    # Safe patterns to ignore
    _SAFE_PATTERNS = [
        r'\.innerHTML\s*=\s*["\'][\s]*["\']',  # Empty string assignment
        r'\.innerHTML\s*=\s*``',  # Empty template literal
        r'DOMPurify\.sanitize',  # Using DOMPurify
        r'sanitizeHtml',  # Using sanitize-html
        r'xss\(',  # Using xss library
    ]

    def should_skip_file(self, path: Path) -> bool:
        """Skip test files where innerHTML might be used for testing."""
        if 'test' in path.stem.lower() or 'spec' in path.stem.lower():
            return True
        if 'mock' in path.stem.lower():
            return True
        return super().should_skip_file(path)

    def check_file(self, path: Path, content: str) -> List[Finding]:
        findings = []
        lines = content.splitlines()

        # Check for innerHTML assignments
        for match in self._INNER_HTML_ASSIGNMENT.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Skip if line contains safe patterns
            if self._is_safe_usage(line_content, content, line_num):
                continue

            # Skip comments
            stripped = line_content.lstrip()
            if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('*'):
                continue

            findings.append(
                self.create_finding(
                    path=path,
                    line=line_num,
                    column=match.start() - content.rfind('\n', 0, match.start()),
                    code=line_content.strip()[:80] + ("..." if len(line_content.strip()) > 80 else ""),
                    message="innerHTML assignment detected. This can lead to XSS if content is user-controlled.",
                    fix_hint=(
                        "Options to fix:\n"
                        "1. Use textContent for plain text:\n"
                        "   element.textContent = userInput;\n\n"
                        "2. Sanitize HTML with DOMPurify:\n"
                        "   import DOMPurify from 'dompurify';\n"
                        "   element.innerHTML = DOMPurify.sanitize(userInput);\n\n"
                        "3. In React, avoid dangerouslySetInnerHTML or sanitize first:\n"
                        "   <div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(html)}} />"
                    ),
                )
            )

        # Check for React's dangerouslySetInnerHTML
        for match in self._DANGEROUS_SET_HTML.finditer(content):
            line_num = content[:match.start()].count('\n') + 1
            line_content = lines[line_num - 1] if line_num <= len(lines) else ""

            # Skip if sanitization is present
            if self._is_safe_usage(line_content, content, line_num):
                continue

            # Skip comments
            stripped = line_content.lstrip()
            if stripped.startswith('//') or stripped.startswith('/*') or stripped.startswith('{/*'):
                continue

            findings.append(
                self.create_finding(
                    path=path,
                    line=line_num,
                    column=match.start() - content.rfind('\n', 0, match.start()),
                    code=line_content.strip()[:80] + ("..." if len(line_content.strip()) > 80 else ""),
                    message="dangerouslySetInnerHTML used without visible sanitization.",
                    fix_hint=(
                        "Always sanitize content before using dangerouslySetInnerHTML:\n\n"
                        "import DOMPurify from 'dompurify';\n\n"
                        "<div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(content)}} />"
                    ),
                )
            )

        return findings

    def _is_safe_usage(self, line_content: str, full_content: str, line_num: int) -> bool:
        """Check if the innerHTML usage appears to be safe."""
        # Check the current line and a few lines before for sanitization
        lines = full_content.splitlines()
        start_line = max(0, line_num - 5)
        context = '\n'.join(lines[start_line:line_num])

        for pattern in self._SAFE_PATTERNS:
            if re.search(pattern, line_content, re.IGNORECASE):
                return True
            if re.search(pattern, context, re.IGNORECASE):
                return True

        return False
