"""Specialist: swallowed exceptions (except:pass / except:return None)."""

from __future__ import annotations

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


class SwallowedExceptionSpecialist(Specialist):
    name = "swallowed_exception"
    trigger_features: list[str] = []

    def should_run(self, ast_data: ASTData, focus: str) -> bool:
        return len(ast_data.except_handlers) > 0

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

        for handler in ast_data.except_handlers:
            if handler.body_type not in ("pass", "return_none"):
                continue

            # --- Deterministic SKIP rules ---

            # 1. Test/fixture functions — intentional catch-all
            func = handler.enclosing_function or ""
            if func.startswith(("test_", "_test", "fixture")):
                continue

            # 2. Best-effort function names
            if func.startswith(("detect_", "try_", "_try", "_discover", "_probe")):
                continue

            # 3. Comment above indicates intentional
            line_idx = handler.line - 2  # 0-indexed, line above
            if 0 <= line_idx < len(ast_data.source_lines):
                comment = ast_data.source_lines[line_idx].lower()
                if any(kw in comment for kw in (
                    "best-effort", "best effort", "optional", "intentional",
                    "ignore", "non-critical", "fallback", "graceful",
                )):
                    continue

            # 4. Catches specific exception (not bare/Exception/BaseException) + has logging
            if handler.type_name and handler.type_name not in ("Exception", "BaseException", "?"):
                continue  # specific exception types are usually intentional

            # --- Critical path check ---
            try_src = handler.try_body_source.lower()
            is_critical = any(kw in try_src for kw in (
                "db.", "save(", "insert(", "delete(", "update(",
                "auth", "password", "commit(", "write(", "send(",
                "payment", "charge(", "transfer(",
            ))

            if not is_critical:
                continue  # non-critical path — likely best-effort enrichment

            # --- LLM micro-call for critical-path exceptions ---
            ctx_start = max(0, handler.line - 6)
            ctx_end = min(len(ast_data.source_lines), handler.line + 5)
            context = "\n".join(ast_data.source_lines[ctx_start:ctx_end])

            verdict = await self.llm_ask(
                question="Does swallowing this exception cause the caller to get incorrect data or miss a failure?",
                context=context,
                examples=[
                    (
                        "try:\n    db.insert(data)\nexcept Exception:\n    pass",
                        "FLAG: caller thinks write succeeded but data was lost",
                    ),
                    (
                        "try:\n    metrics.increment('count')\nexcept Exception:\n    pass",
                        "SKIP: telemetry is optional, failure doesn't affect correctness",
                    ),
                ],
            )

            if verdict.flag:
                sev = "HIGH" if handler.body_type == "pass" else "MEDIUM"
                msg = f"Swallowed exception in `{func}()`: {verdict.reason}"
                findings.append(Finding(
                    line=handler.line, severity=sev, message=msg,
                    specialist=self.name, confidence="LIKELY",
                ))

        return findings
