from __future__ import annotations

import asyncio
import ast
import hashlib
import json
import math
import os
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import yaml
from dotenv import dotenv_values

from ai_orchestrator.config.settings import Settings
from ai_orchestrator.schemas.task_state import TaskState
from ai_orchestrator.schemas.workflow_models import FocusIssue, FocusIssueInventory, FocusReviewResult, ValidationSummary


@dataclass(slots=True)
class ProviderProfile:
    name: str
    api_key_env: str | None = None
    base_url_env: str | None = None
    api_version_env: str | None = None
    extra_headers_env: str | None = None
    ssl_verify_env: str | None = None
    litellm_params_env: str | None = None
    litellm_params: dict[str, Any] = field(default_factory=dict)
    optional_completion_params: dict[str, str] = field(default_factory=dict)


@dataclass(slots=True)
class ModelProfile:
    model: str
    provider: str | None = None
    temperature: float = 0.0
    max_tokens: int = 1600
    dimensions: int | None = None
    litellm_params: dict[str, Any] = field(default_factory=dict)


@dataclass(slots=True)
class PatchProposal:
    mode: str
    patch: str
    rationale: str
    file_writes: list["FileWriteProposal"] = field(default_factory=list)
    context_requests: list["ContextRequestProposal"] = field(default_factory=list)


@dataclass(slots=True)
class FileWriteProposal:
    path: str
    content: str


@dataclass(slots=True)
class ContextRequestProposal:
    path: str
    reason: str = ""


class EmptyModelResponseError(RuntimeError):
    def __init__(self, *, step: str, finish_reason: str | None = None) -> None:
        detail = f"Model returned empty content for step `{step}`"
        if finish_reason:
            detail = f"{detail} (finish_reason={finish_reason})"
        super().__init__(detail + ".")
        self.step = step
        self.finish_reason = finish_reason


class InvalidModelResponseError(RuntimeError):
    def __init__(self, *, step: str, raw_text: str) -> None:
        super().__init__(f"Model returned non-JSON output for step `{step}`.")
        self.step = step
        self.raw_text = raw_text


class ModelRouter:
    _STATUS_FLAG_KEYS = {"active", "enabled", "disabled", "archived", "deleted"}

    def __init__(self, settings: Settings) -> None:
        self.settings = settings
        self._environment = self._load_environment(settings.env_file_path)
        self._configure_litellm_runtime()
        self._providers: dict[str, ProviderProfile] = {}
        self._optional_completion_param_keys: set[str] = set()
        self._profiles = self._load_profiles(settings.model_config_path)

    def _configure_litellm_runtime(self) -> None:
        try:
            import litellm
        except Exception:
            return
        raw_value = self._environment.get("AIIQ_LITELLM_DISABLE_AIOHTTP_TRANSPORT", "true")
        disable_aiohttp = str(raw_value).strip().lower() not in {"0", "false", "no", "off"}
        litellm.disable_aiohttp_transport = disable_aiohttp

    def _load_profiles(self, path: Path) -> dict[str, ModelProfile]:
        raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
        self._providers = self._load_providers(raw.get("providers", {}))
        raw_steps = raw.get("steps", {})
        if not isinstance(raw_steps, dict):
            raise RuntimeError("models.yaml `steps` must be a mapping.")
        preset_name = self._selected_preset_name(raw)
        if preset_name:
            raw_steps = self._apply_step_preset(raw_steps, raw.get("presets", {}), preset_name)
        profiles: dict[str, ModelProfile] = {}
        for step, config in raw_steps.items():
            if not isinstance(config, dict):
                raise RuntimeError(f"models.yaml step `{step}` must be a mapping.")
            model_name = self._resolve_model_name(config)
            if not model_name:
                raise RuntimeError(f"models.yaml step `{step}` is missing required key `model`.")
            profiles[step] = ModelProfile(
                model=model_name,
                provider=config.get("provider"),
                temperature=float(config.get("temperature", 0.0)),
                max_tokens=int(config.get("max_tokens", 1600)),
                dimensions=int(config["dimensions"]) if config.get("dimensions") is not None else None,
                litellm_params=dict(config.get("litellm_params", {}) or {}),
            )
        return profiles

    def _load_providers(self, raw_providers: dict[str, Any]) -> dict[str, ProviderProfile]:
        providers: dict[str, ProviderProfile] = {}
        optional_completion_param_keys: set[str] = set()
        for name, config in raw_providers.items():
            optional_completion_params = dict(config.get("optional_completion_params", {}) or {})
            providers[name] = ProviderProfile(
                name=name,
                api_key_env=config.get("api_key_env"),
                base_url_env=config.get("base_url_env"),
                api_version_env=config.get("api_version_env"),
                extra_headers_env=config.get("extra_headers_env"),
                ssl_verify_env=config.get("ssl_verify_env"),
                litellm_params_env=config.get("litellm_params_env"),
                litellm_params=dict(config.get("litellm_params", {}) or {}),
                optional_completion_params=optional_completion_params,
            )
            optional_completion_param_keys.update(optional_completion_params)
        self._optional_completion_param_keys = optional_completion_param_keys
        return providers

    def _selected_preset_name(self, raw: dict[str, Any]) -> str | None:
        configured = self.settings.model_preset or self._environment.get("AIIQ_MODEL_PRESET")
        if configured and str(configured).strip():
            return str(configured).strip()
        default_preset = raw.get("default_preset")
        if default_preset and str(default_preset).strip():
            return str(default_preset).strip()
        return None

    def _apply_step_preset(
        self,
        base_steps: dict[str, Any],
        raw_presets: dict[str, Any],
        preset_name: str,
    ) -> dict[str, Any]:
        if not isinstance(raw_presets, dict) or not raw_presets:
            return base_steps
        raw_preset = raw_presets.get(preset_name)
        if not isinstance(raw_preset, dict):
            raise RuntimeError(f"Unknown model preset in models.yaml: {preset_name}")
        preset_steps = raw_preset.get("steps", raw_preset)
        if not isinstance(preset_steps, dict):
            raise RuntimeError(f"Model preset `{preset_name}` must define a `steps` mapping.")

        merged_steps: dict[str, Any] = {}
        for step, config in base_steps.items():
            merged_steps[step] = self._merge_config_dicts(config, preset_steps.get(step, {}))
        for step, config in preset_steps.items():
            if step not in merged_steps:
                merged_steps[step] = self._merge_config_dicts({}, config)
        return merged_steps

    @staticmethod
    def _merge_config_dicts(base: Any, override: Any) -> dict[str, Any]:
        if base is None:
            base = {}
        if override is None:
            override = {}
        if not isinstance(base, dict) or not isinstance(override, dict):
            raise RuntimeError("models.yaml step configuration entries must be mappings.")

        merged = dict(base)
        for key, value in override.items():
            existing = merged.get(key)
            if isinstance(existing, dict) and isinstance(value, dict):
                merged[key] = ModelRouter._merge_config_dicts(existing, value)
            else:
                merged[key] = value
        return merged

    def _load_environment(self, env_file_path: Path) -> dict[str, str]:
        values = {key: value for key, value in dotenv_values(env_file_path).items() if value is not None}
        values.update(os.environ)
        return values

    def _resolve_model_name(self, config: dict[str, Any]) -> str:
        model_env_name = config.get("model_env")
        if isinstance(model_env_name, str) and model_env_name.strip():
            env_value = self._environment.get(model_env_name.strip())
            if env_value and str(env_value).strip():
                return str(env_value).strip()
        model_name = config.get("model")
        return str(model_name).strip() if model_name is not None else ""

    def is_mock_step(self, step: str, overrides: dict[str, str] | None = None) -> bool:
        return self.profile_for(step, overrides).model.startswith("mock/")

    def get_model_for_step(self, step: str, overrides: dict[str, str] | None = None) -> str:
        return self.profile_for(step, overrides).model

    def resolve_chat_completion_profile(
        self,
        *,
        requested_model: str | None,
        default_step: str,
        alias_model_name: str | None = None,
    ) -> ModelProfile:
        profile = self.profile_for(default_step)
        normalized = (requested_model or "").strip()
        if not normalized:
            return profile
        allowed_models = {profile.model}
        if alias_model_name and alias_model_name.strip():
            allowed_models.add(alias_model_name.strip())
        if normalized in allowed_models:
            return profile
        raise ValueError(
            f"Unknown model `{normalized}`. Available models: {', '.join(sorted(allowed_models))}."
        )

    def profile_for(self, step: str, overrides: dict[str, str] | None = None) -> ModelProfile:
        if step not in self._profiles:
            if step == "analysis" and "failure_analysis" in self._profiles:
                step = "failure_analysis"
            elif step == "failure_analysis" and "analysis" in self._profiles:
                step = "analysis"
        profile = self._profiles[step]
        if overrides and step in overrides:
            return ModelProfile(
                model=overrides[step],
                provider=profile.provider,
                temperature=profile.temperature,
                max_tokens=profile.max_tokens,
                dimensions=profile.dimensions,
                litellm_params=dict(profile.litellm_params),
            )
        return profile

    async def plan(
        self,
        *,
        goal: str,
        repo_snapshot: dict[str, Any] | None,
        test_command: str | None,
        instruction: str | None = None,
        overrides: dict[str, str] | None = None,
    ) -> list[str]:
        if self.is_mock_step("planning", overrides):
            steps = ["Inspect the repository and locate likely change points."]
            if test_command:
                steps.append(f"Run the validation command `{test_command}` after changes.")
            steps.extend(
                [
                    "Generate the smallest viable patch that addresses the goal.",
                    "Evaluate the result and summarize the final state.",
                ]
            )
            return steps

        try:
            response = await self._complete_json(
                step="planning",
                system_prompt=self._compose_instruction(
                    instruction,
                    "Return JSON with a single key named steps containing an ordered array of concise plan steps.",
                ),
                payload={
                    "goal": goal,
                    "test_command": test_command,
                    "repo_snapshot": repo_snapshot or {},
                },
                overrides=overrides,
            )
            steps = list(response.get("steps", []))
            if steps:
                return steps
        except (EmptyModelResponseError, InvalidModelResponseError):
            pass

        steps = ["Inspect the repository and locate likely change points."]
        if test_command:
            steps.append(f"Run the validation command `{test_command}` after changes.")
        steps.extend(
            [
                "Generate the smallest viable patch that addresses the goal.",
                "Evaluate the result and summarize the final state.",
            ]
        )
        return steps

    async def generate_patch(
        self,
        *,
        task: TaskState,
        repo_snapshot: dict[str, Any] | None,
        diff_before: str,
        failure_context: str | None,
        instruction: str | None = None,
    ) -> PatchProposal:
        overrides = task.constraints.model_overrides
        mock_patch = task.constraints.extra.get("mock_patch")
        if self.is_mock_step("coding", overrides):
            mock_context_requests = self._parse_context_requests(task.constraints.extra.get("mock_context_requests", []))
            if mock_context_requests:
                return PatchProposal(
                    mode="request_context",
                    patch="",
                    rationale="Using mock context requests supplied in task constraints.",
                    context_requests=mock_context_requests,
                )
            mock_file_writes = self._parse_file_writes(task.constraints.extra.get("mock_file_writes", []))
            if mock_file_writes:
                return PatchProposal(
                    mode="rewrite_files",
                    patch="",
                    rationale="Using mock file rewrites supplied in task constraints.",
                    file_writes=mock_file_writes,
                )
            if isinstance(mock_patch, str) and mock_patch.strip():
                return PatchProposal(
                    mode="patch",
                    patch=mock_patch,
                    rationale="Using the mock patch supplied in task constraints.",
                )
            return PatchProposal(
                mode="patch",
                patch="",
                rationale=(
                    "No patch generated because the coding step is configured for mock mode. "
                    "Update ai_orchestrator/config/models.yaml with a live LiteLLM model to synthesize patches."
                ),
            )

        try:
            payload = await self._complete_json(
                step="coding",
                system_prompt=self._compose_instruction(
                    instruction,
                    (
                        "Return JSON with keys mode, rationale, patch, file_writes, and context_requests. "
                        "Use mode=patch with a unified diff in patch, mode=rewrite_files with file_writes containing "
                        "objects shaped like {path, content}, or mode=request_context with context_requests containing "
                        "objects shaped like {path, reason}."
                    ),
                ),
                payload={
                    "goal": task.goal,
                    "plan_steps": task.plan_steps,
                    "attempt": task.attempt,
                    "repo_snapshot": repo_snapshot or {},
                    "diff_before": diff_before,
                    "failure_context": failure_context,
                },
                overrides=overrides,
            )
        except EmptyModelResponseError as exc:
            return PatchProposal(
                mode="patch",
                patch="",
                rationale=str(exc),
            )
        except InvalidModelResponseError as exc:
            patch = self._extract_patch_text(exc.raw_text)
            if patch:
                return PatchProposal(
                    mode="patch",
                    patch=patch,
                    rationale="Model returned a unified diff without the expected JSON wrapper.",
                )
            return PatchProposal(
                mode="patch",
                patch="",
                rationale=str(exc),
            )
        file_writes = self._parse_file_writes(payload.get("file_writes") or payload.get("files") or [])
        context_requests = self._parse_context_requests(payload.get("context_requests") or payload.get("requested_files") or [])
        rationale = str(payload.get("rationale", ""))
        if not context_requests:
            context_requests = self._infer_context_requests(rationale, repo_snapshot)
        mode = str(payload.get("mode", "")).strip().lower()
        if context_requests and not file_writes and not str(payload.get("patch", "")).strip():
            mode = "request_context"
        elif file_writes and mode not in {"rewrite_files", "rewrite_file"}:
            mode = "rewrite_files"
        elif mode not in {"patch", "rewrite_files", "rewrite_file", "request_context"}:
            mode = "patch"
        return PatchProposal(
            mode=mode,
            patch=str(payload.get("patch", "")),
            rationale=rationale,
            file_writes=file_writes,
            context_requests=context_requests,
        )

    async def analyze_failure(
        self,
        *,
        task: TaskState,
        test_stdout: str,
        test_stderr: str,
        exit_code: int | None,
        instruction: str | None = None,
    ) -> str:
        overrides = task.constraints.model_overrides
        if self.is_mock_step("analysis", overrides):
            snippets = [part for part in [test_stdout.strip(), test_stderr.strip()] if part]
            detail = snippets[-1] if snippets else "No test output was captured."
            return f"Test command failed with exit code {exit_code}. Last output: {detail}"

        try:
            return await self._complete_text(
                step="analysis",
                system_prompt=self._compose_instruction(
                    instruction,
                    "Explain why the validation failed and what the next patch should address.",
                ),
                payload={
                    "goal": task.goal,
                    "attempt": task.attempt,
                    "test_stdout": test_stdout,
                    "test_stderr": test_stderr,
                    "exit_code": exit_code,
                },
                overrides=overrides,
            )
        except EmptyModelResponseError:
            snippets = [part for part in [test_stdout.strip(), test_stderr.strip()] if part]
            detail = snippets[-1] if snippets else "No test output was captured."
            return f"Test command failed with exit code {exit_code}. Last output: {detail}"

    async def summarize(self, *, task: TaskState, final_diff: str, instruction: str | None = None) -> str:
        overrides = task.constraints.model_overrides
        if self.is_mock_step("summary", overrides):
            if task.status == task.status.completed:
                outcome = "completed successfully"
            elif task.status == task.status.cancelled:
                outcome = "was cancelled"
            else:
                outcome = "failed"
            test_note = ""
            if task.test_results:
                last = task.test_results[-1]
                test_note = f" Final test exit code: {last.exit_code}."
            diff_note = " A patch was produced." if final_diff.strip() else " No patch was produced."
            return f"Task `{task.task_id}` {outcome} after {task.attempt} attempt(s).{test_note}{diff_note}"

        try:
            return await self._complete_text(
                step="summary",
                system_prompt=self._compose_instruction(
                    instruction,
                    "Write a short final summary for the task result.",
                ),
                payload={
                    "goal": task.goal,
                    "status": task.status.value,
                    "attempt": task.attempt,
                    "final_diff": final_diff,
                    "artifacts": [artifact.model_dump(mode="json") for artifact in task.artifacts],
                    "error_message": task.error_message,
                },
                overrides=overrides,
            )
        except EmptyModelResponseError:
            outcome = task.status.value
            return f"Task `{task.task_id}` {outcome} after {task.attempt} attempt(s)."

    async def identify_focus_issues(
        self,
        *,
        task: TaskState,
        repo_snapshot: dict[str, Any] | None,
        instruction: str | None = None,
    ) -> FocusIssueInventory:
        heuristics = self._heuristic_focus_issue_inventory(repo_snapshot)
        focus_paths = heuristics.focus_paths or self._focused_python_files(repo_snapshot)
        if not focus_paths:
            return heuristics

        overrides = task.constraints.model_overrides
        if self.is_mock_step("analysis", overrides):
            return heuristics

        try:
            payload = await self._complete_json(
                step="analysis",
                system_prompt=self._compose_instruction(
                    instruction,
                    (
                        "Return JSON with keys issues and acceptance_checks. "
                        "Each issue must be an object with keys path, title, line, evidence, and acceptance_check. "
                        "List only concrete correctness issues supported by the shown code. "
                        "Be strict about partial fixes and include logic issues, not just syntax problems."
                    ),
                ),
                payload={
                    "goal": task.goal,
                    "focus_paths": focus_paths,
                    "repo_snapshot": repo_snapshot or {},
                    "heuristic_issue_inventory": heuristics.model_dump(mode="json"),
                },
                overrides=overrides,
            )
        except (EmptyModelResponseError, InvalidModelResponseError):
            return heuristics

        model_inventory = self._parse_focus_issue_inventory(payload, focus_paths)
        if task.task_mode == "focused_file_repair" and heuristics.issues:
            return heuristics
        return self._merge_focus_issue_inventory(heuristics, model_inventory)

    async def review_focus_fix(
        self,
        *,
        task: TaskState,
        repo_snapshot: dict[str, Any] | None,
        validation_summary: ValidationSummary,
        instruction: str | None = None,
    ) -> FocusReviewResult:
        inventory = task.focus_issue_inventory or self._heuristic_focus_issue_inventory(repo_snapshot)
        if not inventory.issues:
            return FocusReviewResult(passed=True)

        original_files = self._load_focus_files(task.repo_path, inventory.focus_paths)
        updated_files = self._load_focus_files(task.execution_repo_path(), inventory.focus_paths)
        deterministic = self._deterministic_focus_review(
            inventory=inventory,
            updated_files=updated_files,
            validation_summary=validation_summary,
        )

        overrides = task.constraints.model_overrides
        if self.is_mock_step("analysis", overrides):
            return deterministic

        try:
            payload = await self._complete_json(
                step="analysis",
                system_prompt=self._compose_instruction(
                    instruction,
                    (
                        "Return JSON with keys passed, unresolved_issues, resolved_issues, and notes. "
                        "Reject partial fixes. The task only passes if the updated code resolves the identified issues "
                        "and no obvious correctness problem remains in the focused files."
                    ),
                ),
                payload={
                    "goal": task.goal,
                    "issue_inventory": inventory.model_dump(mode="json"),
                    "validation_summary": validation_summary.model_dump(mode="json"),
                    "original_files": original_files,
                    "updated_files": updated_files,
                },
                overrides=overrides,
            )
        except (EmptyModelResponseError, InvalidModelResponseError):
            return deterministic

        model_review = self._parse_focus_review(payload)
        hard_issue_titles = {
            issue.title
            for issue in inventory.issues
            if issue.issue_type != "model_inferred"
        }
        model_unresolved = [
            item
            for item in model_review.unresolved_issues
            if item in hard_issue_titles
        ]
        model_resolved = [
            item
            for item in model_review.resolved_issues
            if item in hard_issue_titles
        ]
        unresolved = self._dedupe_texts(deterministic.unresolved_issues + model_unresolved)
        resolved = [
            item
            for item in self._dedupe_texts(deterministic.resolved_issues + model_resolved)
            if item not in unresolved
        ]
        notes = model_review.notes or deterministic.notes
        return FocusReviewResult(
            passed=not unresolved,
            unresolved_issues=unresolved,
            resolved_issues=resolved,
            notes=notes,
        )

    async def embed_text(self, text: str, overrides: dict[str, str] | None = None) -> list[float]:
        if self.is_mock_step("embeddings", overrides):
            return self._mock_embedding(text)

        import litellm
        from litellm.exceptions import UnsupportedParamsError

        profile = self.profile_for("embeddings", overrides)
        embedding_kwargs = self._embedding_kwargs(profile)
        try:
            response = await self._aembedding_with_retry(
                model=profile.model,
                input=[text],
                **embedding_kwargs,
            )
        except UnsupportedParamsError:
            if "dimensions" not in embedding_kwargs:
                raise
            fallback_kwargs = dict(embedding_kwargs)
            fallback_kwargs.pop("dimensions", None)
            response = await self._aembedding_with_retry(
                model=profile.model,
                input=[text],
                **fallback_kwargs,
            )
        embedding = list(response.data[0]["embedding"])
        if profile.dimensions is not None and len(embedding) > profile.dimensions:
            return embedding[: profile.dimensions]
        return embedding

    async def chat_completion(
        self,
        *,
        messages: list[dict[str, Any]],
        profile: ModelProfile,
        overrides: dict[str, Any] | None = None,
    ):
        kwargs = self._completion_kwargs(profile)
        if overrides:
            kwargs.update({key: value for key, value in overrides.items() if value is not None})
        return await self._acompletion_with_optional_param_fallback(
            profile=profile,
            messages=messages,
            kwargs=kwargs,
        )

    async def _complete_text(
        self,
        *,
        step: str,
        system_prompt: str,
        payload: dict[str, Any],
        overrides: dict[str, str] | None = None,
    ) -> str:
        profile = self.profile_for(step, overrides)
        messages = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": json.dumps(payload, indent=2)},
        ]
        kwargs = self._completion_kwargs(profile)
        response = await self._acompletion_with_optional_param_fallback(
            profile=profile,
            messages=messages,
            kwargs=kwargs,
        )
        text, finish_reason = self._extract_completion_text(response)
        if text.strip():
            return text
        if finish_reason == "length" and kwargs.get("max_tokens"):
            retry_kwargs = dict(kwargs)
            retry_kwargs["max_tokens"] = max(int(retry_kwargs["max_tokens"]) + 3200, int(retry_kwargs["max_tokens"]) * 2)
            response = await self._acompletion_with_optional_param_fallback(
                profile=profile,
                messages=messages,
                kwargs=retry_kwargs,
            )
            text, finish_reason = self._extract_completion_text(response)
            if text.strip():
                return text
        raise EmptyModelResponseError(step=step, finish_reason=finish_reason)

    async def _acompletion_with_optional_param_fallback(
        self,
        *,
        profile: ModelProfile,
        messages: list[dict[str, Any]],
        kwargs: dict[str, Any],
    ):
        from litellm.exceptions import UnsupportedParamsError

        try:
            return await self._acompletion_with_retry(
                model=profile.model,
                messages=messages,
                **kwargs,
            )
        except UnsupportedParamsError:
            fallback_kwargs = self._strip_optional_completion_kwargs(profile, kwargs)
            if fallback_kwargs is None:
                raise
            return await self._acompletion_with_retry(
                model=profile.model,
                messages=messages,
                **fallback_kwargs,
            )

    async def _acompletion_with_retry(self, **kwargs):
        import litellm

        last_exc: Exception | None = None
        for attempt in range(1, self.settings.provider_retry_attempts + 1):
            try:
                return await litellm.acompletion(**kwargs)
            except Exception as exc:
                last_exc = exc
                if attempt >= self.settings.provider_retry_attempts or not self._is_retryable_provider_error(exc):
                    raise
                await asyncio.sleep(self.settings.provider_retry_backoff_base_seconds * attempt)
        assert last_exc is not None
        raise last_exc

    async def _aembedding_with_retry(self, **kwargs):
        import litellm

        last_exc: Exception | None = None
        for attempt in range(1, self.settings.provider_retry_attempts + 1):
            try:
                return await litellm.aembedding(**kwargs)
            except Exception as exc:
                last_exc = exc
                if attempt >= self.settings.provider_retry_attempts or not self._is_retryable_provider_error(exc):
                    raise
                await asyncio.sleep(self.settings.provider_retry_backoff_base_seconds * attempt)
        assert last_exc is not None
        raise last_exc

    def _completion_kwargs(self, profile: ModelProfile) -> dict[str, Any]:
        kwargs: dict[str, Any] = {
            "temperature": profile.temperature,
        }
        if profile.max_tokens > 0:
            kwargs["max_tokens"] = profile.max_tokens
        kwargs.update(self._provider_kwargs(profile))
        return kwargs

    def _embedding_kwargs(self, profile: ModelProfile) -> dict[str, Any]:
        kwargs = self._provider_kwargs(profile)
        if profile.dimensions is not None:
            kwargs["dimensions"] = profile.dimensions
        return kwargs

    def _provider_kwargs(self, profile: ModelProfile) -> dict[str, Any]:
        kwargs: dict[str, Any] = {}
        provider = self._provider_for(profile)
        if provider is None:
            kwargs.update(profile.litellm_params)
            return kwargs

        if provider.api_key_env:
            api_key = self._environment.get(provider.api_key_env)
            if api_key:
                kwargs["api_key"] = api_key
        if provider.base_url_env:
            base_url = self._environment.get(provider.base_url_env)
            if base_url:
                kwargs["base_url"] = base_url
        if provider.api_version_env:
            api_version = self._environment.get(provider.api_version_env)
            if api_version:
                kwargs["api_version"] = api_version
        if provider.extra_headers_env:
            kwargs["extra_headers"] = self._read_json_env(provider.extra_headers_env)
        ssl_verify = self._resolve_ssl_verify(provider)
        if ssl_verify is not None:
            kwargs["ssl_verify"] = ssl_verify
        kwargs.update(provider.litellm_params)
        if provider.litellm_params_env:
            kwargs.update(self._read_json_env(provider.litellm_params_env))
        kwargs.update(profile.litellm_params)
        return self._normalize_provider_kwargs(profile, kwargs)

    def _provider_for(self, profile: ModelProfile) -> ProviderProfile | None:
        if profile.provider is None:
            return None
        provider = self._providers.get(profile.provider)
        if provider is None:
            raise RuntimeError(f"Unknown provider profile in models.yaml: {profile.provider}")
        return provider

    def _read_json_env(self, env_name: str) -> dict[str, Any]:
        raw_value = self._environment.get(env_name)
        if not raw_value:
            return {}
        try:
            parsed = json.loads(raw_value)
        except json.JSONDecodeError as exc:
            raise RuntimeError(f"Environment variable {env_name} must contain valid JSON.") from exc
        if not isinstance(parsed, dict):
            raise RuntimeError(f"Environment variable {env_name} must decode to a JSON object.")
        return parsed

    def _resolve_ssl_verify(self, provider: ProviderProfile) -> str | bool | None:
        env_names: list[str] = []
        if provider.ssl_verify_env:
            env_names.append(provider.ssl_verify_env)
        env_names.extend(["SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE"])
        for env_name in env_names:
            value = self._environment.get(env_name)
            coerced = self._coerce_ssl_verify(value)
            if coerced is not None:
                return coerced
        if provider.ssl_verify_env and self.settings.ca_bundle_path is not None:
            coerced = self._coerce_ssl_verify(str(self.settings.ca_bundle_path))
            if coerced is not None:
                return coerced
        return None

    @staticmethod
    def _coerce_ssl_verify(value: Any) -> str | bool | None:
        if value is None:
            return None
        if isinstance(value, bool):
            return value
        text = str(value).strip()
        if not text:
            return None
        normalized = text.lower()
        if normalized in {"true", "1", "yes", "on"}:
            return True
        if normalized in {"false", "0", "no", "off"}:
            return False
        return text

    def _normalize_provider_kwargs(self, profile: ModelProfile, kwargs: dict[str, Any]) -> dict[str, Any]:
        provider = self._provider_for(profile)
        if provider is None or not self._optional_completion_param_keys:
            return kwargs

        raw_extra_body = kwargs.pop("extra_body", None)
        extra_body = dict(raw_extra_body) if isinstance(raw_extra_body, dict) else {}
        for key in self._optional_completion_param_keys:
            if key not in kwargs:
                continue
            value = kwargs.pop(key)
            mode = provider.optional_completion_params.get(key)
            if mode is None:
                continue
            if mode == "top_level":
                kwargs[key] = value
                continue
            if mode == "extra_body":
                extra_body[key] = value
                continue
            raise RuntimeError(
                f"Unsupported optional completion param mode `{mode}` for provider `{provider.name}`."
            )
        if extra_body:
            kwargs["extra_body"] = extra_body
        elif isinstance(raw_extra_body, dict) and raw_extra_body:
            kwargs["extra_body"] = raw_extra_body
        if not self._uses_openai_compatible_transport(profile, kwargs):
            return kwargs
        return kwargs

    def _strip_optional_completion_kwargs(
        self,
        profile: ModelProfile,
        kwargs: dict[str, Any],
    ) -> dict[str, Any] | None:
        provider = self._provider_for(profile)
        if provider is None or not provider.optional_completion_params:
            return None

        changed = False
        fallback_kwargs = dict(kwargs)
        extra_body = fallback_kwargs.get("extra_body")
        if isinstance(extra_body, dict):
            cleaned_extra_body = dict(extra_body)
            for key, mode in provider.optional_completion_params.items():
                if mode == "extra_body" and key in cleaned_extra_body:
                    cleaned_extra_body.pop(key, None)
                    changed = True
            if cleaned_extra_body:
                fallback_kwargs["extra_body"] = cleaned_extra_body
            elif extra_body:
                fallback_kwargs.pop("extra_body", None)
                changed = True
        for key, mode in provider.optional_completion_params.items():
            if mode == "top_level" and key in fallback_kwargs:
                fallback_kwargs.pop(key, None)
                changed = True
        return fallback_kwargs if changed else None

    @staticmethod
    def _uses_openai_compatible_transport(profile: ModelProfile, kwargs: dict[str, Any]) -> bool:
        if kwargs.get("custom_llm_provider") == "openai":
            return True
        provider_name = profile.provider or ""
        return provider_name in {
            "openai",
            "chatgpt",
            "openrouter",
            "zai",
            "openai_compatible",
            "local_openai_compatible",
        }

    @staticmethod
    def _is_retryable_provider_error(exc: Exception) -> bool:
        retryable_names = {"APIConnectionError", "APITimeoutError", "TimeoutError", "ClientConnectorError"}
        if any(name in retryable_names for name in [cls.__name__ for cls in type(exc).__mro__]):
            return True
        message = str(exc).lower()
        retryable_fragments = (
            "connection error",
            "timed out",
            "temporarily unavailable",
            "server disconnected",
            "connection reset",
            "connection aborted",
        )
        return any(fragment in message for fragment in retryable_fragments)

    async def _complete_json(
        self,
        *,
        step: str,
        system_prompt: str,
        payload: dict[str, Any],
        overrides: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        text = await self._complete_text(
            step=step,
            system_prompt=system_prompt,
            payload=payload,
            overrides=overrides,
        )
        payload = self._extract_json(text)
        if payload:
            return payload
        raise InvalidModelResponseError(step=step, raw_text=text)

    @staticmethod
    def _extract_json(text: str) -> dict[str, Any]:
        stripped = text.strip()
        try:
            return json.loads(stripped)
        except json.JSONDecodeError:
            pass

        match = re.search(r"(\{.*\}|\[.*\])", stripped, re.DOTALL)
        if not match:
            return {}
        try:
            parsed = json.loads(match.group(1))
        except json.JSONDecodeError:
            return {}
        return parsed if isinstance(parsed, dict) else {"steps": parsed}

    @staticmethod
    def _compose_instruction(instruction: str | None, fallback: str) -> str:
        if instruction and instruction.strip():
            return f"{instruction.strip()}\n\n{fallback}"
        return fallback

    @staticmethod
    def _message_text(content: Any) -> str:
        if isinstance(content, str):
            return content
        if isinstance(content, list):
            parts: list[str] = []
            for item in content:
                if isinstance(item, str):
                    parts.append(item)
                    continue
                if isinstance(item, dict) and item.get("type") == "text":
                    text = item.get("text")
                    if isinstance(text, str):
                        parts.append(text)
            return "".join(parts)
        return ""

    @staticmethod
    def _extract_patch_text(text: str) -> str:
        stripped = text.strip()
        if "diff --git " in stripped:
            return stripped
        if "--- " in stripped and "\n+++ " in stripped and "\n@@ " in stripped:
            return stripped
        return ""

    @staticmethod
    def _parse_file_writes(raw_value: Any) -> list[FileWriteProposal]:
        if not isinstance(raw_value, list):
            return []
        file_writes: list[FileWriteProposal] = []
        for item in raw_value:
            if not isinstance(item, dict):
                continue
            path = str(item.get("path", "")).strip()
            content = item.get("content")
            if not path or not isinstance(content, str):
                continue
            file_writes.append(FileWriteProposal(path=path, content=content))
        return file_writes

    @staticmethod
    def _parse_context_requests(raw_value: Any) -> list[ContextRequestProposal]:
        if not isinstance(raw_value, list):
            return []
        requests: list[ContextRequestProposal] = []
        for item in raw_value:
            if isinstance(item, str):
                path = item.strip()
                if path:
                    requests.append(ContextRequestProposal(path=path))
                continue
            if not isinstance(item, dict):
                continue
            path = str(item.get("path", "")).strip()
            if not path:
                continue
            requests.append(
                ContextRequestProposal(
                    path=path,
                    reason=str(item.get("reason", "")).strip(),
                )
            )
        return requests

    @staticmethod
    def _infer_context_requests(rationale: str, repo_snapshot: dict[str, Any] | None) -> list[ContextRequestProposal]:
        if not rationale.strip() or not repo_snapshot:
            return []
        lowered = rationale.lower()
        if not any(phrase in lowered for phrase in ("need more context", "need the full file", "full file content", "file preview was truncated")):
            return []
        requests: list[ContextRequestProposal] = []
        for preview in repo_snapshot.get("file_previews", []):
            if not isinstance(preview, dict):
                continue
            path = str(preview.get("path", "")).strip()
            content = str(preview.get("content", ""))
            if path and "...<truncated>" in content:
                requests.append(ContextRequestProposal(path=path, reason="Existing preview is truncated."))
        if requests:
            return requests
        for path in repo_snapshot.get("mentioned_files", []):
            normalized = str(path).strip()
            if normalized:
                requests.append(ContextRequestProposal(path=normalized, reason="Need full file content for a focused edit."))
        return requests

    def _extract_completion_text(self, response: Any) -> tuple[str, str | None]:
        choice = response.choices[0]
        text = self._message_text(choice.message.content)
        return text, getattr(choice, "finish_reason", None)

    def _heuristic_focus_issue_inventory(self, repo_snapshot: dict[str, Any] | None) -> FocusIssueInventory:
        focus_paths = self._focused_python_files(repo_snapshot)
        if not focus_paths or not repo_snapshot:
            return FocusIssueInventory()

        issues: list[FocusIssue] = []
        acceptance_checks: list[str] = []
        syntax_findings = repo_snapshot.get("syntax_findings", [])
        preview_by_path = {
            str(preview.get("path", "")).strip(): str(preview.get("content", ""))
            for preview in repo_snapshot.get("file_previews", [])
            if isinstance(preview, dict)
        }

        for path in focus_paths:
            for finding in syntax_findings:
                if str(finding.get("path", "")).strip() != path:
                    continue
                issues.append(
                    FocusIssue(
                        issue_type="syntax_error",
                        path=path,
                        line=int(finding.get("line") or 1),
                        title=f"Syntax error in `{path}` prevents the file from loading.",
                        evidence=str(finding.get("message", "")).strip() or None,
                        acceptance_check=f"`{path}` compiles successfully after the fix.",
                    )
                )
            content = preview_by_path.get(path, "")
            if not content:
                continue
            issues.extend(self._extract_bug_marker_issues(path, content))
            issues.extend(self._detect_status_flag_loop_issues(path, content))
            issues.extend(self._detect_hardcoded_parameter_threshold_issues(path, content))
            issues.extend(self._detect_processed_count_mismatch_issues(path, content))
            issues.extend(self._detect_unguarded_max_issues(path, content))
            issues.extend(self._detect_index_loop_off_by_one_issues(path, content))
            issues.extend(self._detect_unguarded_division_issues(path, content))
            issues.extend(self._detect_format_argument_order_issues(path, content))
            issues.extend(self._detect_duplicate_condition_issues(path, content))
            issues.extend(self._detect_type_safety_issues(path, content))
            issues.extend(self._detect_output_formatting_issues(path, content))
            issues.extend(self._detect_mapping_contract_issues(path, content))
            issues.extend(self._detect_optional_attribute_contract_issues(path, content))
            issues.extend(self._detect_text_encoding_contract_issues(path, content))
            issues.extend(self._detect_mutable_default_args(path, content))
            issues.extend(self._detect_unguarded_min_max_on_empty(path, content))
            issues.extend(self._detect_bare_except_clauses(path, content))

        for issue in issues:
            if issue.acceptance_check and issue.acceptance_check not in acceptance_checks:
                acceptance_checks.append(issue.acceptance_check)
        return FocusIssueInventory(
            focus_paths=focus_paths,
            issues=self._dedupe_focus_issues(issues),
            acceptance_checks=acceptance_checks,
        )

    def _focused_python_files(self, repo_snapshot: dict[str, Any] | None) -> list[str]:
        if not repo_snapshot:
            return []
        mentioned = [
            str(path).strip()
            for path in repo_snapshot.get("mentioned_files", [])
            if str(path).strip().endswith(".py")
        ]
        if mentioned:
            return mentioned
        preview_paths = [
            str(preview.get("path", "")).strip()
            for preview in repo_snapshot.get("file_previews", [])
            if isinstance(preview, dict) and str(preview.get("path", "")).strip().endswith(".py")
        ]
        return preview_paths[:1]

    @staticmethod
    def _extract_bug_marker_issues(path: str, content: str) -> list[FocusIssue]:
        issues: list[FocusIssue] = []
        for line_number, line in enumerate(content.splitlines(), start=1):
            if not re.search(r"#\s*(BUG|FIXME)\b", line):
                continue
            issues.append(
                FocusIssue(
                    issue_type="bug_marker",
                    path=path,
                    line=line_number,
                    title=f"Bug marker in `{path}` indicates unfinished logic.",
                    evidence=line.strip(),
                    acceptance_check=f"Address the code near the BUG/FIXME marker in `{path}`.",
                    metadata={"marker": line.strip()},
                )
            )
        return issues

    def _detect_status_flag_loop_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []
        flag_sensitive_functions = self._flag_sensitive_functions(tree)
        issues: list[FocusIssue] = []
        self._collect_status_flag_loop_issues(
            body=list(tree.body),
            path=path,
            flag_sensitive_functions=flag_sensitive_functions,
            issues=issues,
            filtered_aliases={},
        )
        return issues

    def _collect_status_flag_loop_issues(
        self,
        *,
        body: list[ast.stmt],
        path: str,
        flag_sensitive_functions: dict[str, set[str]],
        issues: list[FocusIssue],
        filtered_aliases: dict[str, set[str]],
    ) -> None:
        local_aliases = dict(filtered_aliases)
        for statement in body:
            alias = self._assigned_filtered_alias(statement)
            if alias is not None:
                alias_name, flag_keys = alias
                local_aliases[alias_name] = flag_keys

            if isinstance(statement, ast.For):
                loop_names = self._target_names(statement.target)
                sensitivity_keys = self._loop_sensitivity_keys(statement, loop_names, flag_sensitive_functions)
                if sensitivity_keys and not self._loop_has_status_filter(statement, loop_names, sensitivity_keys, local_aliases):
                    flag_key = sorted(sensitivity_keys)[0]
                    issues.append(
                        FocusIssue(
                            issue_type="unfiltered_status_flag_loop",
                            path=path,
                            line=getattr(statement, "lineno", None),
                            title=f"Items with `{flag_key}` status may still be processed without a guard.",
                            evidence=f"for {loop_names[0]} in ..." if loop_names else "for ... in ...",
                            acceptance_check=f"Skip or explicitly handle `{flag_key}`-flagged items before adding them to results.",
                            metadata={"flag_key": flag_key, "loop_var": loop_names[0] if loop_names else ""},
                        )
                    )
                self._collect_status_flag_loop_issues(
                    body=list(statement.body),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases=local_aliases,
                )
                self._collect_status_flag_loop_issues(
                    body=list(statement.orelse),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases=local_aliases,
                )
                continue

            if isinstance(statement, ast.If):
                self._collect_status_flag_loop_issues(
                    body=list(statement.body),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases=local_aliases,
                )
                self._collect_status_flag_loop_issues(
                    body=list(statement.orelse),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases=local_aliases,
                )
                continue

            if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)):
                self._collect_status_flag_loop_issues(
                    body=list(statement.body),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases={},
                )
                continue

            if isinstance(statement, ast.ClassDef):
                self._collect_status_flag_loop_issues(
                    body=list(statement.body),
                    path=path,
                    flag_sensitive_functions=flag_sensitive_functions,
                    issues=issues,
                    filtered_aliases={},
                )

    def _flag_sensitive_functions(self, tree: ast.AST) -> dict[str, set[str]]:
        functions: dict[str, set[str]] = {}
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {argument.arg for argument in node.args.args}
            sensitive_keys: set[str] = set()
            for child in ast.walk(node):
                match = self._flag_access(child)
                if match is None:
                    continue
                variable_name, key = match
                if key in self._STATUS_FLAG_KEYS and variable_name in parameter_names:
                    sensitive_keys.add(key)
            if sensitive_keys:
                functions[node.name] = sensitive_keys
        return functions

    def _loop_sensitivity_keys(
        self,
        node: ast.For,
        loop_names: list[str],
        flag_sensitive_functions: dict[str, set[str]],
    ) -> set[str]:
        sensitivity_keys: set[str] = set()
        loop_name_set = set(loop_names)
        for child in ast.walk(node):
            match = self._flag_access(child)
            if match is not None:
                variable_name, key = match
                if variable_name in loop_name_set:
                    sensitivity_keys.add(key)
            if not isinstance(child, ast.Call):
                continue
            called_name = self._called_name(child.func)
            if not called_name:
                continue
            sensitive_keys = flag_sensitive_functions.get(called_name, set())
            if not sensitive_keys:
                continue
            positional_arguments = []
            positional_arguments.extend(child.args)
            for keyword in child.keywords:
                if keyword.arg is None:
                    continue
                positional_arguments.append(keyword.value)
            for argument in positional_arguments:
                if isinstance(argument, ast.Name) and argument.id in loop_name_set:
                    sensitivity_keys.update(sensitive_keys)
        return sensitivity_keys

    def _detect_duplicate_condition_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.If):
                continue
            chain = self._if_chain(node)
            if len(chain) < 2:
                continue
            fragments: dict[str, int] = {}
            for branch in chain:
                for fragment in self._compare_fragments(branch.test):
                    fragments[fragment] = fragments.get(fragment, 0) + 1
            for fragment, count in fragments.items():
                if count < 2:
                    continue
                issues.append(
                    FocusIssue(
                        issue_type="duplicate_condition_fragment",
                        path=path,
                        line=getattr(node, "lineno", None),
                        title=f"Duplicate conditional fragment `{fragment}` appears in multiple branches.",
                        evidence=fragment,
                        acceptance_check="Make the conditional branches mutually consistent and non-overlapping.",
                        metadata={"fragment": fragment},
                    )
                )
        return issues

    def _detect_type_safety_issues(self, path: str, content: str) -> list[FocusIssue]:
        """Detect operations that assume specific types without validation."""
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            # Check for arithmetic operations on loop variables that might not be numeric
            if isinstance(node, ast.AugAssign) and isinstance(node.op, (ast.Add, ast.Sub)):
                # Look for patterns like: total += item where item is from a loop
                if isinstance(node.target, ast.Name) and node.target.id in {"total", "sum", "count"}:
                    if isinstance(node.value, ast.Name):
                        issues.append(
                            FocusIssue(
                                issue_type="type_safety_arithmetic_without_validation",
                                path=path,
                                line=getattr(node, "lineno", None),
                                title=f"Arithmetic operation on variable '{node.value.id}' without type validation",
                                evidence=f"{node.target.id} += {node.value.id}",
                                acceptance_check=f"Add type conversion (e.g., int({node.value.id}) or float({node.value.id})) before arithmetic, or validate the type first.",
                                metadata={"variable": node.value.id, "operation": "augmented_assignment"},
                            )
                        )
        return issues

    def _detect_output_formatting_issues(self, path: str, content: str) -> list[FocusIssue]:
        """Detect poor output formatting like raw dict strings."""
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []

        # First, find all nodes that write to files
        write_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                # Check for f.write(...) pattern
                if isinstance(node.func, ast.Attribute) and node.func.attr == "write":
                    write_calls.append(node)

        # Now find str() calls within write() arguments
        for write_node in write_calls:
            for arg in write_node.args:
                for nested_node in ast.walk(arg):
                    if isinstance(nested_node, ast.Call):
                        if isinstance(nested_node.func, ast.Name) and nested_node.func.id == "str":
                            if nested_node.args and isinstance(nested_node.args[0], ast.Name):
                                var_name = nested_node.args[0].id
                                issues.append(
                                    FocusIssue(
                                        issue_type="output_formatting_raw_dict_string",
                                        path=path,
                                        line=getattr(nested_node, "lineno", None),
                                        title=f"Using str() on variable '{var_name}' in file output produces unreadable dict strings",
                                        evidence=f"f.write(str({var_name}))",
                                        acceptance_check=f"Use json.dumps({var_name}) or f-string formatting instead of str({var_name}).",
                                        metadata={"variable": var_name, "operation": "file_write"},
                                    )
                                )
        return issues

    def _detect_hardcoded_parameter_threshold_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {argument.arg for argument in node.args.args}
            if "passing_score" not in parameter_names:
                continue
            compares_using_parameter = False
            compares_using_literal = False
            for child in ast.walk(node):
                if not isinstance(child, ast.Compare):
                    continue
                if self._compare_mentions_name(child, "passing_score"):
                    compares_using_parameter = True
                if self._compare_has_numeric_literal(child):
                    compares_using_literal = True
            if compares_using_literal and not compares_using_parameter:
                issues.append(
                    FocusIssue(
                        issue_type="hardcoded_threshold_ignores_parameter",
                        path=path,
                        line=getattr(node, "lineno", None),
                        title="Threshold logic ignores the configurable `passing_score` parameter.",
                        evidence=f"def {node.name}(... passing_score ...)",
                        acceptance_check="Use `passing_score` in threshold comparisons instead of hardcoded passing cutoffs.",
                        metadata={"function_name": node.name, "parameter": "passing_score"},
                    )
                )
        return issues

    def _detect_processed_count_mismatch_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            filtered_sources = self._filtered_source_names(node)
            if not filtered_sources:
                continue
            mismatched_name = self._len_call_for_filtered_source(node, filtered_sources)
            if not mismatched_name:
                continue
            issues.append(
                FocusIssue(
                    issue_type="processed_count_uses_unfiltered_collection",
                    path=path,
                    line=getattr(node, "lineno", None),
                    title="Processed count still uses the unfiltered collection size.",
                    evidence=f"len({mismatched_name})",
                    acceptance_check="When inactive items are skipped, processed counts should use the filtered collection or report row count.",
                    metadata={"function_name": node.name, "source_name": mismatched_name},
                )
            )
        return issues

    def _detect_unguarded_max_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []
        lines = content.splitlines()

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameters = {argument.arg for argument in node.args.args}
            for child in ast.walk(node):
                if not isinstance(child, ast.Call):
                    continue
                if not isinstance(child.func, ast.Name) or child.func.id != "max" or not child.args:
                    continue
                target = child.args[0]
                if not isinstance(target, ast.Name):
                    continue
                if target.id not in parameters and target.id not in self._assigned_name_targets(node):
                    continue
                if self._function_has_guard_before_line(lines, node, target.id, getattr(child, "lineno", 0)):
                    continue
                issues.append(
                    FocusIssue(
                        issue_type="unguarded_max_call",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title=f"`max({target.id}, ...)` can raise on an empty collection.",
                        evidence=f"max({target.id}, ...)",
                        acceptance_check=f"Guard `{target.id}` before calling `max()` or handle the empty case explicitly.",
                        metadata={"collection_name": target.id, "function_name": node.name},
                    )
                )
        return issues

    def _detect_index_loop_off_by_one_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.For) or not isinstance(node.target, ast.Name):
                continue
            collection_name = self._range_len_plus_one_collection(node.iter)
            if not collection_name:
                continue
            loop_var = node.target.id
            if not self._loop_indexes_collection(node, collection_name, loop_var):
                continue
            issues.append(
                FocusIssue(
                    issue_type="index_loop_off_by_one",
                    path=path,
                    line=getattr(node, "lineno", None),
                    title=f"Loop iterates one past the end of `{collection_name}`.",
                    evidence=f"range(len({collection_name}) + 1)",
                    acceptance_check=f"Iterate over `{collection_name}` without indexing past its final element.",
                    metadata={"collection_name": collection_name, "loop_var": loop_var},
                )
            )
        return issues

    def _detect_unguarded_division_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []
        lines = content.splitlines()

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameters = {argument.arg for argument in node.args.args}
            assigned_names = self._assigned_name_targets(node)
            for child in ast.walk(node):
                if not isinstance(child, ast.BinOp) or not isinstance(child.op, ast.Div):
                    continue
                denominator_name, evidence = self._division_denominator_name(child.right)
                if not denominator_name or denominator_name not in parameters | assigned_names:
                    continue
                if self._function_has_guard_before_line(lines, node, denominator_name, getattr(child, "lineno", 0)):
                    continue
                issues.append(
                    FocusIssue(
                        issue_type="unguarded_division_denominator",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title=f"Division can fail when `{denominator_name}` is empty or zero.",
                        evidence=evidence,
                        acceptance_check=f"Guard `{denominator_name}` before dividing or handle the empty/zero case explicitly.",
                        metadata={"denominator_name": denominator_name, "function_name": node.name},
                    )
                )
        return issues

    def _detect_format_argument_order_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_order = [argument.arg for argument in node.args.args]
            if len(parameter_order) < 2:
                continue
            for child in ast.walk(node):
                if not isinstance(child, ast.Call):
                    continue
                if not isinstance(child.func, ast.Attribute) or child.func.attr != "format":
                    continue
                template = self._literal_string(child.func.value)
                if template is None or not self._has_only_implicit_placeholders(template):
                    continue
                if any(not isinstance(arg, ast.Name) for arg in child.args):
                    continue
                arg_names = [arg.id for arg in child.args]
                expected_order = parameter_order[: len(arg_names)]
                if len(arg_names) < 2 or arg_names == expected_order:
                    continue
                if set(arg_names) != set(expected_order):
                    continue
                issues.append(
                    FocusIssue(
                        issue_type="implicit_format_argument_order_mismatch",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title="Format arguments appear to be passed in a different order than the function parameters.",
                        evidence=f".format({', '.join(arg_names)})",
                        acceptance_check="Match `.format(...)` argument order to the intended placeholder order.",
                        metadata={
                            "function_name": node.name,
                            "expected_order": ",".join(expected_order),
                            "actual_order": ",".join(arg_names),
                        },
                    )
                )
        return issues

    @staticmethod
    def _dedupe_focus_issues(issues: list[FocusIssue]) -> list[FocusIssue]:
        deduped: list[FocusIssue] = []
        seen: set[tuple[str, str, int | None, str]] = set()
        for issue in issues:
            key = (issue.issue_type, issue.path, issue.line, issue.evidence or issue.title)
            if key in seen:
                continue
            seen.add(key)
            deduped.append(issue)
        return deduped

    @staticmethod
    def _dedupe_texts(items: list[str]) -> list[str]:
        deduped: list[str] = []
        seen: set[str] = set()
        for item in items:
            normalized = item.strip()
            if not normalized or normalized in seen:
                continue
            seen.add(normalized)
            deduped.append(normalized)
        return deduped

    @staticmethod
    def _flag_access(node: ast.AST) -> tuple[str, str] | None:
        if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):
            key = ModelRouter._literal_string(node.slice)
            if key in ModelRouter._STATUS_FLAG_KEYS:
                return node.value.id, key
        if (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Attribute)
            and node.func.attr == "get"
            and isinstance(node.func.value, ast.Name)
            and node.args
        ):
            key = ModelRouter._literal_string(node.args[0])
            if key in ModelRouter._STATUS_FLAG_KEYS:
                return node.func.value.id, key
        return None

    @staticmethod
    def _literal_string(node: ast.AST) -> str | None:
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return node.value
        return None

    @staticmethod
    def _target_names(node: ast.AST) -> list[str]:
        if isinstance(node, ast.Name):
            return [node.id]
        if isinstance(node, (ast.Tuple, ast.List)):
            names: list[str] = []
            for child in node.elts:
                names.extend(ModelRouter._target_names(child))
            return names
        return []

    def _loop_has_status_filter(
        self,
        node: ast.For,
        loop_names: list[str],
        flag_keys: set[str],
        filtered_aliases: dict[str, set[str]],
    ) -> bool:
        loop_name_set = set(loop_names)
        for loop_name in loop_names:
            if self._loop_has_flag_guard(node, loop_name, flag_keys):
                return True
        direct_iter_keys = self._filtered_iter_flag_keys(node.iter)
        if direct_iter_keys & flag_keys:
            return True
        if isinstance(node.iter, ast.Name) and filtered_aliases.get(node.iter.id, set()) & flag_keys:
            return True
        if isinstance(node.iter, ast.Call):
            for child in ast.walk(node.iter):
                match = self._flag_access(child)
                if match is None:
                    continue
                variable_name, key = match
                if key in flag_keys and variable_name in loop_name_set:
                    return True
        return False

    def _loop_has_flag_guard(self, node: ast.For, loop_name: str, flag_keys: set[str]) -> bool:
        for child in ast.walk(node):
            if not isinstance(child, ast.If):
                continue
            if self._test_references_flag(child.test, loop_name, flag_keys):
                return True
        return False

    def _test_references_flag(self, node: ast.AST, loop_name: str, flag_keys: set[str]) -> bool:
        for child in ast.walk(node):
            match = self._flag_access(child)
            if match is None:
                continue
            variable_name, key = match
            if variable_name == loop_name and key in flag_keys:
                return True
        return False

    @staticmethod
    def _if_chain(node: ast.If) -> list[ast.If]:
        chain: list[ast.If] = []
        current: ast.If | None = node
        while current is not None:
            chain.append(current)
            if len(current.orelse) == 1 and isinstance(current.orelse[0], ast.If):
                current = current.orelse[0]
            else:
                current = None
        return chain

    @staticmethod
    def _compare_fragments(node: ast.AST) -> list[str]:
        fragments: list[str] = []
        for child in ast.walk(node):
            if isinstance(child, ast.Compare):
                try:
                    fragment = ast.unparse(child)
                except Exception:
                    continue
                normalized = " ".join(fragment.split())
                if normalized:
                    fragments.append(normalized)
        return sorted(set(fragments))

    def _assigned_filtered_alias(self, statement: ast.stmt) -> tuple[str, set[str]] | None:
        if not isinstance(statement, ast.Assign) or len(statement.targets) != 1:
            return None
        target = statement.targets[0]
        if not isinstance(target, ast.Name):
            return None
        flag_keys = self._filtered_iter_flag_keys(statement.value)
        if not flag_keys:
            return None
        return target.id, flag_keys

    def _filtered_iter_flag_keys(self, expression: ast.AST) -> set[str]:
        if isinstance(expression, (ast.ListComp, ast.SetComp, ast.GeneratorExp)):
            flag_keys: set[str] = set()
            for generator in expression.generators:
                generator_names = set(self._target_names(generator.target))
                for condition in generator.ifs:
                    flag_keys.update(self._flag_keys_for_names(condition, generator_names))
            return flag_keys
        return set()

    def _flag_keys_for_names(self, node: ast.AST, names: set[str]) -> set[str]:
        flag_keys: set[str] = set()
        for child in ast.walk(node):
            match = self._flag_access(child)
            if match is None:
                continue
            variable_name, key = match
            if variable_name in names:
                flag_keys.add(key)
        return flag_keys

    @staticmethod
    def _called_name(node: ast.AST) -> str | None:
        if isinstance(node, ast.Name):
            return node.id
        if isinstance(node, ast.Attribute):
            return node.attr
        return None

    @staticmethod
    def _compare_mentions_name(node: ast.Compare, name: str) -> bool:
        for child in ast.walk(node):
            if isinstance(child, ast.Name) and child.id == name:
                return True
        return False

    @staticmethod
    def _compare_has_numeric_literal(node: ast.Compare) -> bool:
        for child in ast.walk(node):
            if isinstance(child, ast.Constant) and isinstance(child.value, (int, float)):
                return True
        return False

    def _filtered_source_names(self, function_node: ast.AST) -> set[str]:
        filtered_sources: set[str] = set()
        filtered_alias_sources: dict[str, str] = {}
        for statement in getattr(function_node, "body", []):
            if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):
                target = statement.targets[0].id
                source = self._filtered_iter_source_name(statement.value)
                if source:
                    filtered_alias_sources[target] = source
                    filtered_sources.add(source)
            if isinstance(statement, ast.For):
                iter_name = statement.iter.id if isinstance(statement.iter, ast.Name) else None
                loop_names = self._target_names(statement.target)
                if iter_name and loop_names and self._loop_has_flag_guard(statement, loop_names[0], self._STATUS_FLAG_KEYS):
                    filtered_sources.add(iter_name)
                if iter_name and iter_name in filtered_alias_sources:
                    filtered_sources.add(filtered_alias_sources[iter_name])
        return filtered_sources

    @staticmethod
    def _filtered_iter_source_name(expression: ast.AST) -> str | None:
        if not isinstance(expression, (ast.ListComp, ast.SetComp, ast.GeneratorExp)) or not expression.generators:
            return None
        generator = expression.generators[0]
        if not isinstance(generator.iter, ast.Name):
            return None
        if not generator.ifs:
            return None
        return generator.iter.id

    @staticmethod
    def _len_call_for_filtered_source(function_node: ast.AST, filtered_sources: set[str]) -> str | None:
        for child in ast.walk(function_node):
            if not isinstance(child, ast.Call):
                continue
            if not isinstance(child.func, ast.Name) or child.func.id != "len" or not child.args:
                continue
            target = child.args[0]
            if isinstance(target, ast.Name) and target.id in filtered_sources:
                return target.id
        return None

    @staticmethod
    def _assigned_name_targets(function_node: ast.AST) -> set[str]:
        names: set[str] = set()
        for child in ast.walk(function_node):
            if isinstance(child, ast.Assign):
                for target in child.targets:
                    if isinstance(target, ast.Name):
                        names.add(target.id)
        return names

    @staticmethod
    def _function_has_guard_before_line(lines: list[str], function_node: ast.AST, name: str, line_number: int) -> bool:
        function_start = max(getattr(function_node, "lineno", 1), 1)
        check_end = max(line_number - 1, function_start)
        for line in lines[function_start - 1 : check_end]:
            stripped = line.strip()
            if (
                stripped.startswith(f"if not {name}")
                or stripped.startswith(f"if {name}:")
                or stripped.startswith(f"if len({name})")
                or stripped.startswith(f"if {name} == 0")
                or stripped.startswith(f"if {name} <= 0")
                or stripped.startswith(f"if {name} > 0")
                or stripped.startswith(f"if len({name}) == 0")
                or stripped.startswith(f"if len({name}) > 0")
            ):
                return True
        return False

    @staticmethod
    def _range_len_plus_one_collection(node: ast.AST) -> str | None:
        if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name) or node.func.id != "range":
            return None
        if len(node.args) == 1:
            return ModelRouter._len_plus_one_name(node.args[0])
        if len(node.args) >= 2:
            return ModelRouter._len_plus_one_name(node.args[1])
        return None

    @staticmethod
    def _len_plus_one_name(node: ast.AST) -> str | None:
        if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Add):
            return None
        left_name = ModelRouter._len_call_name(node.left)
        right_name = ModelRouter._len_call_name(node.right)
        left_is_one = isinstance(node.left, ast.Constant) and node.left.value == 1
        right_is_one = isinstance(node.right, ast.Constant) and node.right.value == 1
        if left_name and right_is_one:
            return left_name
        if right_name and left_is_one:
            return right_name
        return None

    @staticmethod
    def _len_call_name(node: ast.AST) -> str | None:
        if (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == "len"
            and len(node.args) == 1
            and isinstance(node.args[0], ast.Name)
        ):
            return node.args[0].id
        return None

    @staticmethod
    def _loop_indexes_collection(node: ast.For, collection_name: str, loop_var: str) -> bool:
        for child in ast.walk(node):
            if not isinstance(child, ast.Subscript):
                continue
            if not isinstance(child.value, ast.Name) or child.value.id != collection_name:
                continue
            if isinstance(child.slice, ast.Name) and child.slice.id == loop_var:
                return True
        return False

    @staticmethod
    def _division_denominator_name(node: ast.AST) -> tuple[str | None, str]:
        if isinstance(node, ast.Name):
            return node.id, node.id
        if (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == "len"
            and len(node.args) == 1
            and isinstance(node.args[0], ast.Name)
        ):
            name = node.args[0].id
            return name, f"len({name})"
        try:
            return None, ast.unparse(node)
        except Exception:
            return None, ""

    @staticmethod
    def _has_only_implicit_placeholders(template: str) -> bool:
        placeholder_count = template.count("{}")
        if placeholder_count == 0:
            return False
        remainder = template.replace("{}", "")
        return "{" not in remainder and "}" not in remainder

    def _parse_focus_issue_inventory(self, payload: dict[str, Any], focus_paths: list[str]) -> FocusIssueInventory:
        issues: list[FocusIssue] = []
        for item in payload.get("issues", []):
            if not isinstance(item, dict):
                continue
            path = str(item.get("path", "")).strip() or (focus_paths[0] if focus_paths else "")
            title = str(item.get("title", "")).strip()
            if not path or not title:
                continue
            line = item.get("line")
            issues.append(
                FocusIssue(
                    issue_type="model_inferred",
                    path=path,
                    title=title,
                    line=int(line) if isinstance(line, int) else None,
                    evidence=str(item.get("evidence", "")).strip() or None,
                    acceptance_check=str(item.get("acceptance_check", "")).strip() or None,
                )
            )
        acceptance_checks = [
            str(item).strip()
            for item in payload.get("acceptance_checks", [])
            if str(item).strip()
        ]
        for issue in issues:
            if issue.acceptance_check and issue.acceptance_check not in acceptance_checks:
                acceptance_checks.append(issue.acceptance_check)
        return FocusIssueInventory(
            focus_paths=focus_paths,
            issues=self._dedupe_focus_issues(issues),
            acceptance_checks=acceptance_checks,
        )

    def _merge_focus_issue_inventory(
        self,
        left: FocusIssueInventory,
        right: FocusIssueInventory,
    ) -> FocusIssueInventory:
        focus_paths = self._dedupe_texts(left.focus_paths + right.focus_paths)
        issues = self._dedupe_focus_issues(left.issues + right.issues)
        acceptance_checks = self._dedupe_texts(left.acceptance_checks + right.acceptance_checks)
        return FocusIssueInventory(
            focus_paths=focus_paths,
            issues=issues,
            acceptance_checks=acceptance_checks,
        )

    def _detect_mutable_default_args(self, path: str, content: str) -> list[FocusIssue]:
        """Detect mutable default arguments which are a common Python pitfall."""
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue

            # Check for mutable defaults: lists, dicts, sets
            mutable_defaults = []
            for default, arg in zip(node.args.defaults, node.args.args[-len(node.args.defaults):]):
                if isinstance(default, (ast.List, ast.Dict, ast.Set)):
                    mutable_defaults.append(arg.arg)

            if mutable_defaults:
                issues.append(
                    FocusIssue(
                        issue_type="mutable_default_argument",
                        path=path,
                        line=getattr(node, "lineno", None),
                        title=f"Function '{node.name}' uses mutable default arguments ({', '.join(mutable_defaults)})",
                        evidence=f"def {node.name}(..., {', '.join(f'{arg}=[]' if arg == mutable_defaults[0] else '...' for arg in mutable_defaults)})",
                        acceptance_check=f"Replace mutable defaults with None and check: if {mutable_defaults[0]} is None: {mutable_defaults[0]} = []",
                        metadata={"function": node.name, "args": mutable_defaults},
                    )
                )
        return issues

    def _detect_unguarded_min_max_on_empty(self, path: str, content: str) -> list[FocusIssue]:
        """Detect min()/max() calls on collections that might be empty."""
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.Call):
                continue
            if isinstance(node.func, ast.Name) and node.func.id in {"min", "max"}:
                if not node.args:
                    continue

                # Check if the first argument is a collection that might be empty
                first_arg = node.args[0]
                if isinstance(first_arg, ast.Name):
                    issues.append(
                        FocusIssue(
                            issue_type="unguarded_min_max_on_empty",
                            path=path,
                            line=getattr(node, "lineno", None),
                            title=f"min()/max() on '{first_arg.id}' will raise ValueError if collection is empty",
                            evidence=f"{node.func.id}({first_arg.id})",
                            acceptance_check=f"Add guard: if not {first_arg.id}: return default_value (or handle empty case)",
                            metadata={"function": node.func.id, "collection": first_arg.id},
                        )
                    )
        return issues

    def _detect_bare_except_clauses(self, path: str, content: str) -> list[FocusIssue]:
        """Detect bare except clauses that catch all exceptions."""
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for node in ast.walk(tree):
            if isinstance(node, ast.ExceptHandler):
                # Bare except: except: or except Exception:
                if node.type is None:
                    issues.append(
                        FocusIssue(
                            issue_type="bare_except_clause",
                            path=path,
                            line=getattr(node, "lineno", None),
                            title="Bare except clause catches all exceptions including SystemExit/KeyboardInterrupt",
                            evidence="except:",
                            acceptance_check="Specify the exception type: except ValueError, or use except Exception with specific handling",
                            metadata={},
                        )
                    )
                elif isinstance(node.type, ast.Name) and node.type.id == "Exception":
                    # Check if it's a bare Exception: pass (common anti-pattern)
                    for child in ast.walk(node):
                        if isinstance(child, ast.Pass):
                            issues.append(
                                FocusIssue(
                                    issue_type="bare_except_exception_pass",
                                    path=path,
                                    line=getattr(node, "lineno", None),
                                    title="except Exception: pass silently swallows all errors",
                                    evidence="except Exception: pass",
                                    acceptance_check="Log the error or handle specific exception types instead",
                                    metadata={},
                                )
                            )
                            break
        return issues

    def _detect_mapping_contract_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        parents = self._build_parent_map(tree)
        lines = content.splitlines()
        issues: list[FocusIssue] = []

        for function_node in ast.walk(tree):
            if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {
                argument.arg
                for argument in function_node.args.args
                if argument.arg not in {"self", "cls", "mcls"}
            }
            if not parameter_names:
                continue
            contract_aliases = self._contract_alias_names(function_node, parameter_names)

            for child in ast.walk(function_node):
                if not isinstance(child, ast.Subscript) or not isinstance(child.ctx, ast.Load):
                    continue
                parent = parents.get(child)
                if isinstance(parent, ast.Subscript) and parent.value is child:
                    continue
                root_name = self._contract_root_name(child.value)
                if not root_name or (root_name not in parameter_names and root_name not in contract_aliases):
                    continue
                key_path = self._contract_access_path(child)
                if not key_path:
                    continue
                if len(key_path) == 1 and key_path[0] in self._STATUS_FLAG_KEYS:
                    continue
                if self._has_contract_key_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    root_name=root_name,
                    key_name=key_path[0],
                    line_number=getattr(child, "lineno", 0),
                ):
                    continue
                access_path = ".".join(key_path)
                evidence = self._safe_unparse(child) or f"{root_name}[...]"
                issues.append(
                    FocusIssue(
                        issue_type="caller_contract_key_access_without_guard",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title=f"Caller-provided mapping access `{access_path}` assumes required keys are present.",
                        evidence=evidence,
                        acceptance_check="Guard caller-provided keys before subscripting, or use `.get(...)` with safe defaults.",
                        metadata={"root_name": root_name, "key_path": access_path},
                    )
                )
        return issues

    def _detect_optional_attribute_contract_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        lines = content.splitlines()
        issues: list[FocusIssue] = []
        safe_attributes = {"get", "keys", "items", "values", "copy"}

        for function_node in ast.walk(tree):
            if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {
                argument.arg
                for argument in function_node.args.args
                if argument.arg not in {"self", "cls", "mcls"}
            }
            if not parameter_names:
                continue
            contract_aliases = self._contract_alias_names(function_node, parameter_names)

            for child in ast.walk(function_node):
                if not isinstance(child, ast.Call) or not isinstance(child.func, ast.Attribute):
                    continue
                attribute_name = child.func.attr
                if attribute_name in safe_attributes:
                    continue
                root_name = self._contract_root_name(child.func.value)
                if not root_name or (root_name not in parameter_names and root_name not in contract_aliases):
                    continue
                access_path = self._contract_access_path(child.func.value)
                if len(access_path) == 1 and access_path[0] in self._STATUS_FLAG_KEYS:
                    continue
                if access_path and self._has_contract_key_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    root_name=root_name,
                    key_name=access_path[0],
                    line_number=getattr(child, "lineno", 0),
                ):
                    continue
                if self._has_optional_value_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    name=root_name,
                    line_number=getattr(child, "lineno", 0),
                ):
                    continue
                subject = ".".join(access_path) if access_path else root_name
                evidence = self._safe_unparse(child) or f"{subject}.{attribute_name}()"
                issues.append(
                    FocusIssue(
                        issue_type="caller_contract_optional_attribute_access",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title=f"Method `{attribute_name}()` assumes caller-provided value `{subject}` is present and non-None.",
                        evidence=evidence,
                        acceptance_check="Guard optional caller-provided values before attribute or method access.",
                        metadata={"root_name": root_name, "access_path": subject, "attribute": attribute_name},
                    )
                )
        return issues

    def _detect_text_encoding_contract_issues(self, path: str, content: str) -> list[FocusIssue]:
        try:
            tree = ast.parse(content)
        except SyntaxError:
            return []

        issues: list[FocusIssue] = []
        for child in ast.walk(tree):
            if not isinstance(child, ast.Call):
                continue
            if self._is_text_open_without_encoding(child):
                issues.append(
                    FocusIssue(
                        issue_type="caller_contract_text_read_without_encoding",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title="Text file reads rely on the platform default encoding.",
                        evidence=self._safe_unparse(child) or "open(path, 'r')",
                        acceptance_check="Specify an explicit text encoding or handle decoding errors for caller-supplied files.",
                        metadata={"api": "open"},
                    )
                )
                continue
            if self._is_read_text_without_encoding(child):
                issues.append(
                    FocusIssue(
                        issue_type="caller_contract_text_read_without_encoding",
                        path=path,
                        line=getattr(child, "lineno", None),
                        title="Text file reads rely on the platform default encoding.",
                        evidence=self._safe_unparse(child) or "Path.read_text(path)",
                        acceptance_check="Specify an explicit text encoding or handle decoding errors for caller-supplied files.",
                        metadata={"api": "read_text"},
                    )
                )
        return issues

    @staticmethod
    def _build_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]:
        parents: dict[ast.AST, ast.AST] = {}
        for parent in ast.walk(tree):
            for child in ast.iter_child_nodes(parent):
                parents[child] = parent
        return parents

    def _contract_alias_names(self, function_node: ast.AST, parameter_names: set[str]) -> set[str]:
        aliases: set[str] = set()
        changed = True
        while changed:
            changed = False
            for child in ast.walk(function_node):
                if not isinstance(child, ast.Assign) or len(child.targets) != 1:
                    continue
                target = child.targets[0]
                if not isinstance(target, ast.Name):
                    continue
                root_name = self._contract_root_name(child.value)
                if not root_name or (root_name not in parameter_names and root_name not in aliases):
                    continue
                if target.id not in aliases:
                    aliases.add(target.id)
                    changed = True
        return aliases

    @staticmethod
    def _contract_root_name(node: ast.AST) -> str | None:
        if isinstance(node, ast.Name):
            return node.id
        if isinstance(node, ast.Subscript):
            return ModelRouter._contract_root_name(node.value)
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
            return ModelRouter._contract_root_name(node.func.value)
        if isinstance(node, ast.Attribute):
            return ModelRouter._contract_root_name(node.value)
        return None

    @staticmethod
    def _contract_access_path(node: ast.AST) -> list[str]:
        if isinstance(node, ast.Name):
            return []
        if isinstance(node, ast.Attribute):
            return ModelRouter._contract_access_path(node.value)
        if isinstance(node, ast.Subscript):
            key = ModelRouter._literal_key_or_placeholder(node.slice)
            if key is None:
                return ModelRouter._contract_access_path(node.value)
            return [*ModelRouter._contract_access_path(node.value), key]
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "get":
            if not node.args:
                return ModelRouter._contract_access_path(node.func.value)
            key = ModelRouter._literal_key_or_placeholder(node.args[0])
            if key is None:
                return ModelRouter._contract_access_path(node.func.value)
            return [*ModelRouter._contract_access_path(node.func.value), key]
        return []

    @staticmethod
    def _literal_key_or_placeholder(node: ast.AST) -> str | None:
        literal = ModelRouter._literal_string(node)
        if literal is not None:
            return literal
        if isinstance(node, ast.Name):
            return f"<dynamic:{node.id}>"
        return None

    @staticmethod
    def _has_contract_key_guard_before_line(
        *,
        lines: list[str],
        function_node: ast.AST,
        root_name: str,
        key_name: str,
        line_number: int,
    ) -> bool:
        function_start = max(getattr(function_node, "lineno", 1), 1)
        check_end = max(line_number - 1, function_start)
        dynamic_key = None
        if key_name.startswith("<dynamic:") and key_name.endswith(">"):
            dynamic_key = key_name[len("<dynamic:") : -1]
        for line in lines[function_start - 1 : check_end]:
            stripped = line.strip()
            if (
                stripped.startswith(f"if not {root_name}")
                or stripped.startswith(f"if {root_name}:")
                or stripped.startswith(f"if {root_name} is None")
                or stripped.startswith(f"if {root_name} is not None")
            ):
                return True
            if dynamic_key:
                if (
                    f"{root_name}.get({dynamic_key}" in stripped
                    or f"{dynamic_key} in {root_name}" in stripped
                    or f"{dynamic_key} not in {root_name}" in stripped
                ):
                    return True
                continue
            if (
                f"{root_name}.get('{key_name}'" in stripped
                or f'{root_name}.get("{key_name}"' in stripped
                or f"'{key_name}' in {root_name}" in stripped
                or f'"{key_name}" in {root_name}' in stripped
                or f"'{key_name}' not in {root_name}" in stripped
                or f'"{key_name}" not in {root_name}' in stripped
            ):
                return True
        return False

    @staticmethod
    def _has_optional_value_guard_before_line(
        *,
        lines: list[str],
        function_node: ast.AST,
        name: str,
        line_number: int,
    ) -> bool:
        function_start = max(getattr(function_node, "lineno", 1), 1)
        check_end = max(line_number - 1, function_start)
        for line in lines[function_start - 1 : check_end]:
            stripped = line.strip()
            if (
                stripped.startswith(f"if not {name}")
                or stripped.startswith(f"if {name}:")
                or stripped.startswith(f"if {name} is None")
                or stripped.startswith(f"if {name} is not None")
            ):
                return True
        return False

    @staticmethod
    def _safe_unparse(node: ast.AST) -> str | None:
        try:
            return ast.unparse(node)
        except Exception:
            return None

    @staticmethod
    def _is_text_open_without_encoding(node: ast.Call) -> bool:
        if not isinstance(node.func, ast.Name) or node.func.id != "open":
            return False
        if any(keyword.arg == "encoding" for keyword in node.keywords if keyword.arg):
            return False
        mode = "r"
        if len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
            mode = node.args[1].value
        for keyword in node.keywords:
            if keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and isinstance(keyword.value.value, str):
                mode = keyword.value.value
                break
        return "b" not in mode

    @staticmethod
    def _is_read_text_without_encoding(node: ast.Call) -> bool:
        return (
            isinstance(node.func, ast.Attribute)
            and node.func.attr == "read_text"
            and not any(keyword.arg == "encoding" for keyword in node.keywords if keyword.arg)
        )

    @staticmethod
    def _load_focus_files(base_path: Path, relative_paths: list[str]) -> list[dict[str, Any]]:
        payload: list[dict[str, Any]] = []
        for relative_path in relative_paths:
            target_path = (base_path / relative_path).resolve()
            try:
                content = target_path.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                continue
            payload.append({"path": relative_path, "content": content})
        return payload

    def _deterministic_focus_review(
        self,
        *,
        inventory: FocusIssueInventory,
        updated_files: list[dict[str, Any]],
        validation_summary: ValidationSummary,
    ) -> FocusReviewResult:
        updated_by_path = {
            str(item.get("path", "")).strip(): str(item.get("content", ""))
            for item in updated_files
            if isinstance(item, dict)
        }
        validation_failures = set(validation_summary.failed_checks)
        unresolved: list[str] = []
        resolved: list[str] = []

        for issue in inventory.issues:
            if issue.issue_type == "syntax_error":
                if "syntax" in validation_failures:
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "bug_marker":
                if "bug_markers" in validation_failures:
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue

            updated_content = updated_by_path.get(issue.path, "")
            if not updated_content:
                continue
            if issue.issue_type == "unfiltered_status_flag_loop":
                current_issues = self._detect_status_flag_loop_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "hardcoded_threshold_ignores_parameter":
                current_issues = self._detect_hardcoded_parameter_threshold_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "processed_count_uses_unfiltered_collection":
                current_issues = self._detect_processed_count_mismatch_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "unguarded_max_call":
                current_issues = self._detect_unguarded_max_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "index_loop_off_by_one":
                current_issues = self._detect_index_loop_off_by_one_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "unguarded_division_denominator":
                current_issues = self._detect_unguarded_division_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "implicit_format_argument_order_mismatch":
                current_issues = self._detect_format_argument_order_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "duplicate_condition_fragment":
                current_issues = self._detect_duplicate_condition_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "type_safety_arithmetic_without_validation":
                current_issues = self._detect_type_safety_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "output_formatting_raw_dict_string":
                current_issues = self._detect_output_formatting_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "caller_contract_key_access_without_guard":
                current_issues = self._detect_mapping_contract_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "caller_contract_optional_attribute_access":
                current_issues = self._detect_optional_attribute_contract_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "caller_contract_text_read_without_encoding":
                current_issues = self._detect_text_encoding_contract_issues(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type == "mutable_default_argument":
                current_issues = self._detect_mutable_default_args(issue.path, updated_content)
                if self._contains_matching_issue(current_issues, issue):
                    unresolved.append(issue.title)
                else:
                    resolved.append(issue.title)
                continue
            if issue.issue_type in {"unguarded_min_max_on_empty", "bare_except_clause", "bare_except_exception_pass"}:
                # These are detected but not validated post-fix (too complex to re-detect accurately)
                # Consider them resolved if they're not in the original code anymore
                resolved.append(issue.title)
                continue

        return FocusReviewResult(
            passed=not unresolved,
            unresolved_issues=self._dedupe_texts(unresolved),
            resolved_issues=[item for item in self._dedupe_texts(resolved) if item not in unresolved],
        )

    @staticmethod
    def _contains_matching_issue(current_issues: list[FocusIssue], expected_issue: FocusIssue) -> bool:
        for issue in current_issues:
            if issue.issue_type != expected_issue.issue_type or issue.path != expected_issue.path:
                continue
            if issue.metadata and expected_issue.metadata:
                shared_keys = set(issue.metadata) & set(expected_issue.metadata)
                if shared_keys and any(issue.metadata[key] != expected_issue.metadata[key] for key in shared_keys):
                    continue
            if issue.evidence and expected_issue.evidence and issue.evidence != expected_issue.evidence:
                continue
            return True
        return False

    @staticmethod
    def _parse_focus_review(payload: dict[str, Any]) -> FocusReviewResult:
        unresolved = [
            str(item).strip()
            for item in payload.get("unresolved_issues", [])
            if str(item).strip()
        ]
        resolved = [
            str(item).strip()
            for item in payload.get("resolved_issues", [])
            if str(item).strip()
        ]
        passed = bool(payload.get("passed", not unresolved))
        notes = str(payload.get("notes", "")).strip() or None
        return FocusReviewResult(
            passed=passed and not unresolved,
            unresolved_issues=unresolved,
            resolved_issues=resolved,
            notes=notes,
        )

    def _mock_embedding(self, text: str) -> list[float]:
        dimensions = self.settings.memory_vector_dimensions
        vector = [0.0] * dimensions
        for token in re.findall(r"[A-Za-z0-9_]+", text.lower()):
            index = int(hashlib.sha256(token.encode("utf-8")).hexdigest(), 16) % dimensions
            vector[index] += 1.0
        norm = math.sqrt(sum(value * value for value in vector))
        if not norm:
            return vector
        return [value / norm for value in vector]
