"""Checker for swallowed exceptions.

Python: except: pass
JavaScript: .catch(() => {})
Go: if err != nil { } (ignored error)
Rust: let _ = result; (.unwrap() is separate)
Ruby: rescue => e; end
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import List

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


class ExceptionSwallowingChecker(BaseChecker):
    """Detects silently swallowed exceptions in multiple languages.

    Python: except: pass
    JavaScript: .catch(() => {})
    Go: if err != nil { } (ignored error)
    Rust: let _ = result;
    Ruby: rescue => e; end
    """

    rule_id = "QUAL001"
    rule_name = "exception-swallowing"
    description = "Silently swallowed exceptions hide bugs and make debugging hard"
    why = (
        "When you catch an exception and do nothing with it, you're hiding potential "
        "bugs. The code might be failing silently, and you won't know until something "
        "goes seriously wrong. At minimum, log the error so you can debug issues."
    )
    severity = Severity.WARNING
    file_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".rb"}

    # Python patterns
    _PY_EXCEPT_PASS = re.compile(r"except\s*(?:\w+)?:\s*$")
    _PY_PASS_LINE = re.compile(r"^\s*pass\s*$")
    _PY_ELLIPSIS_LINE = re.compile(r"^\s*\.\.\.\s*$")

    # TypeScript/JavaScript patterns
    _JS_EMPTY_CATCH = re.compile(r"\.catch\s*\(\s*\(\s*\w*\s*\)\s*=>\s*\{\s*\}\s*\)")
    _JS_EMPTY_CATCH_BLOCK = re.compile(r"catch\s*\(\s*\w*\s*\)\s*\{\s*\}")
    _JS_NOOP_CATCH = re.compile(r"\.catch\s*\(\s*\(\s*\)\s*=>\s*(?:undefined|null|void\s+0)\s*\)")

    # Go patterns - ignored errors
    _GO_IGNORED_ERROR = re.compile(r',\s*_\s*(?::)?=\s*\w+')  # result, _ := fn()
    _GO_EMPTY_IF_ERR = re.compile(r'if\s+err\s*!=\s*nil\s*\{\s*\}')

    # Rust patterns - discarded results
    _RUST_LET_UNDERSCORE = re.compile(r'let\s+_\s*=\s*[^;]+;')
    _RUST_SEMICOLON_RESULT = re.compile(r'\?\s*;\s*//\s*ignore')

    # Ruby patterns - empty rescue
    _RUBY_EMPTY_RESCUE = re.compile(r'rescue\s*(?:=>?\s*\w+)?\s*(?:;|\n)\s*end', re.MULTILINE)
    _RUBY_RESCUE_NIL = re.compile(r'rescue\s*(?:=>?\s*\w+)?\s*;\s*nil\s*;?\s*end')

    def check_file(self, path: Path, content: str) -> List[Finding]:
        if path.suffix == ".py":
            return self._check_python(path, content)
        elif path.suffix == ".go":
            return self._check_go(path, content)
        elif path.suffix == ".rs":
            return self._check_rust(path, content)
        elif path.suffix == ".rb":
            return self._check_ruby(path, content)
        else:
            return self._check_javascript(path, content)

    def _check_python(self, path: Path, content: str) -> List[Finding]:
        """Check Python code for swallowed exceptions."""
        findings = []
        lines = content.splitlines()

        i = 0
        while i < len(lines):
            line = lines[i]
            line_num = i + 1

            # Skip comments
            stripped = line.lstrip()
            if stripped.startswith("#"):
                i += 1
                continue

            # Check for bare except or except <type>:
            if self._PY_EXCEPT_PASS.search(line):
                # Look at the next non-empty line
                j = i + 1
                while j < len(lines) and not lines[j].strip():
                    j += 1

                if j < len(lines):
                    next_line = lines[j]
                    if self._PY_PASS_LINE.match(next_line) or self._PY_ELLIPSIS_LINE.match(next_line):
                        findings.append(
                            self.create_finding(
                                path=path,
                                line=line_num,
                                column=1,
                                code=line.strip(),
                                message=(
                                    "Exception caught and silently ignored with 'pass'. "
                                    "Errors will be hidden, making bugs hard to find."
                                ),
                                fix_hint=(
                                    "At minimum, log the error: except Exception as e: logger.warning(f'Ignored: {e}')"
                                ),
                            )
                        )
            i += 1

        return findings

    def _check_javascript(self, path: Path, content: str) -> List[Finding]:
        """Check TypeScript/JavaScript code for swallowed exceptions."""
        findings = []
        lines = content.splitlines()

        for line_num, line in enumerate(lines, start=1):
            # Skip comments
            stripped = line.lstrip()
            if stripped.startswith("//") or stripped.startswith("/*"):
                continue

            # Check for .catch(() => {})
            for pattern, msg in [
                (self._JS_EMPTY_CATCH, ".catch(() => {})"),
                (self._JS_NOOP_CATCH, ".catch(() => undefined)"),
            ]:
                match = pattern.search(line)
                if match:
                    findings.append(
                        self.create_finding(
                            path=path,
                            line=line_num,
                            column=match.start() + 1,
                            code=line.strip(),
                            message=(
                                f"Promise error silently swallowed with '{msg}'. "
                                "Errors will be hidden, making bugs hard to find."
                            ),
                            fix_hint=(
                                ".catch((err) => { console.error('Error:', err); }) "
                                "or .catch((err) => { /* intentionally ignored: reason */ })"
                            ),
                        )
                    )

            # Check for empty catch blocks: catch (e) {}
            match = self._JS_EMPTY_CATCH_BLOCK.search(line)
            if match:
                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() + 1,
                        code=line.strip(),
                        message=(
                            "Catch block is empty. Errors will be silently ignored."
                        ),
                        fix_hint=(
                            "Log the error: catch (err) { console.error('Error:', err); } "
                            "or add a comment explaining why it's intentionally ignored."
                        ),
                    )
                )

        return findings

    def _check_go(self, path: Path, content: str) -> List[Finding]:
        """Check Go code for ignored errors."""
        findings = []
        lines = content.splitlines()

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

            # Skip comments
            stripped = line_content.strip()
            if stripped.startswith('//'):
                continue

            # Skip test files
            if '_test.go' in path.name:
                continue

            findings.append(
                self.create_finding(
                    path=path,
                    line=line_num,
                    column=match.start() - content.rfind('\n', 0, match.start()),
                    code=line_content.strip(),
                    message=(
                        "Error ignored with blank identifier '_'. "
                        "Errors should be handled or explicitly documented."
                    ),
                    fix_hint=(
                        "Handle the error:\n"
                        "result, err := fn()\n"
                        "if err != nil {\n"
                        "    return fmt.Errorf(\"operation failed: %w\", err)\n"
                        "}\n\n"
                        "Or document intentional ignore:\n"
                        "result, _ := fn() // Intentionally ignored: reason"
                    ),
                )
            )

        return findings

    def _check_rust(self, path: Path, content: str) -> List[Finding]:
        """Check Rust code for discarded Results."""
        findings = []
        lines = content.splitlines()

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

            # Skip comments
            stripped = line_content.strip()
            if stripped.startswith('//'):
                continue

            # Skip if it's clearly not a Result/Option
            if 'Result' not in content[:match.start() + 200] and 'Option' not in content[:match.start() + 200]:
                continue

            findings.append(
                self.create_finding(
                    path=path,
                    line=line_num,
                    column=match.start() - content.rfind('\n', 0, match.start()),
                    code=line_content.strip(),
                    message=(
                        "Result/Option discarded with 'let _ ='. "
                        "Errors may be silently ignored."
                    ),
                    fix_hint=(
                        "Handle the result:\n"
                        "let result = fn()?;  // Propagate error\n\n"
                        "Or handle explicitly:\n"
                        "if let Err(e) = fn() {\n"
                        "    log::warn!(\"Ignored: {}\", e);\n"
                        "}"
                    ),
                )
            )

        return findings

    def _check_ruby(self, path: Path, content: str) -> List[Finding]:
        """Check Ruby code for empty rescue blocks."""
        findings = []
        lines = content.splitlines()

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

            findings.append(
                self.create_finding(
                    path=path,
                    line=line_num,
                    column=match.start() - content.rfind('\n', 0, match.start()),
                    code=line_content.strip(),
                    message=(
                        "Empty rescue block silently swallows exceptions."
                    ),
                    fix_hint=(
                        "Handle or log the exception:\n"
                        "begin\n"
                        "  risky_operation\n"
                        "rescue => e\n"
                        "  Rails.logger.warn(\"Ignored: #{e.message}\")\n"
                        "end"
                    ),
                )
            )

        return findings
