"""Shared heuristics and evidence tracking for investigative turns."""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from typing import Any, Literal


TurnMode = Literal["simple", "investigative"]
TurnDomain = Literal["web", "workspace", "mixed"]

_INVESTIGATIVE_PATTERNS = (
    r"\b(analy[sz]e|analysis|investigate|research|debug|diagnose|review)\b",
    r"\b(root cause|why is|why does|how does|where is|how is)\b",
    r"\b(compare|comparison|versus|vs\.?|alternatives?|pros and cons|tradeoffs?)\b",
    r"\b(latest|current|today|recent|pricing|reviews?|best|top)\b",
    r"\b(find all|all usages|every usage|edge cases?|architecture|codebase|repo(?:sitory)?)\b",
)
_WEB_PATTERNS = (
    r"\b(latest|current|today|recent|pricing|price|cost|review|reviews|alternatives?)\b",
    r"\b(compare|comparison|versus|vs\.?|best|top|pros and cons)\b",
    r"\b(website|web|internet|online|source|sources|citation|citations)\b",
    r"https?://",
)
_WORKSPACE_PATTERNS = (
    r"\b(repo(?:sitory)?|codebase|project|workspace|implementation|module|class|function)\b",
    r"\b(file|files|directory|directories|tests?|stack trace|build|compile|failing)\b",
    r"\b(where is|find all|all usages|root cause|architecture|edge cases?)\b",
)
_BROAD_PATTERNS = (
    r"\b(architecture|codebase|repo(?:sitory)?-wide|system-wide)\b",
    r"\b(find all|all usages|every usage|trace .* flow|how does .* work)\b",
    r"\b(compare|alternatives?|survey|research options|review for edge cases)\b",
)
_SIMPLE_PATTERNS = (
    r"^(hi|hello|hey|thanks|thank you)\b",
    r"\b(rewrite|rephrase|translate|proofread|summarize this)\b",
)
_SINGLE_SOURCE_PATTERNS = (
    r"\b(single source|one source|this page only|this article only|this link only)\b",
    r"\b(from this page|from this article|from this link)\b",
)
_DIRECT_EXECUTION_PATTERNS = (
    r"\b(create|write|update|edit|implement|fix|refactor|rename|delete)\b",
    r"\b(run|execute|ship|build)\b",
)
_TEST_COMMAND_PATTERNS = (
    r"\b(pytest|nosetests|unittest|vitest|jest|playwright test|npm test|pnpm test|yarn test)\b",
    r"\b(go test|cargo test|mix test|rspec|bundle exec rspec)\b",
)


def _has_any(patterns: tuple[str, ...], text: str) -> bool:
    return any(re.search(pattern, text, flags=re.I) for pattern in patterns)


def _append_unique(values: list[str], value: str) -> None:
    cleaned = value.strip()
    if cleaned and cleaned not in values:
        values.append(cleaned)


@dataclass(slots=True, frozen=True)
class InvestigationProfile:
    """Classification for a single user turn."""

    mode: TurnMode
    domain: TurnDomain
    broad: bool = False
    single_source_ok: bool = False
    confidence: float = 1.0  # 0.0-1.0 confidence in classification

    @property
    def is_investigative(self) -> bool:
        return self.mode == "investigative"

    @property
    def required_web_sources(self) -> int:
        return 1 if self.single_source_ok else 2


@dataclass(slots=True)
class EvidenceLog:
    """Structured evidence collected from tool calls."""

    repo_paths: list[str] = field(default_factory=list)
    commands: list[str] = field(default_factory=list)
    tests: list[str] = field(default_factory=list)
    web_sources: list[str] = field(default_factory=list)
    tool_summaries: list[str] = field(default_factory=list)

    def merge(self, other: "EvidenceLog | None") -> "EvidenceLog":
        if other is None:
            return self
        for collection_name in ("repo_paths", "commands", "tests", "web_sources", "tool_summaries"):
            current = getattr(self, collection_name)
            for value in getattr(other, collection_name):
                _append_unique(current, value)
        return self

    @property
    def has_repo_evidence(self) -> bool:
        return bool(self.repo_paths or self.commands or self.tests)

    def summary_lines(self, limit: int = 3) -> list[str]:
        lines: list[str] = []
        if self.repo_paths:
            lines.append("Files checked: " + ", ".join(self.repo_paths[:limit]))
        if self.commands:
            lines.append("Commands run: " + ", ".join(self.commands[:limit]))
        if self.tests:
            lines.append("Tests run: " + ", ".join(self.tests[:limit]))
        if self.web_sources:
            lines.append("Sources consulted: " + ", ".join(self.web_sources[:limit]))
        if not lines and self.tool_summaries:
            lines.append("Tool activity: " + ", ".join(self.tool_summaries[:limit]))
        return lines


@dataclass(slots=True)
class InvestigationFindings:
    """Isolated investigation result prepared for the main agent."""

    summary: str
    evidence: EvidenceLog = field(default_factory=EvidenceLog)


def classify_request(text: str) -> InvestigationProfile:
    """Classify a turn as simple or investigative, plus its dominant domain."""
    lowered = " ".join(str(text or "").split()).lower()
    if not lowered:
        return InvestigationProfile(mode="simple", domain="workspace")

    web_hits = _has_any(_WEB_PATTERNS, lowered)
    workspace_hits = _has_any(_WORKSPACE_PATTERNS, lowered)
    investigative_hits = _has_any(_INVESTIGATIVE_PATTERNS, lowered)
    simple_hits = _has_any(_SIMPLE_PATTERNS, lowered)
    broad_hits = _has_any(_BROAD_PATTERNS, lowered)

    if web_hits and workspace_hits:
        domain: TurnDomain = "mixed"
    elif web_hits:
        domain = "web"
    else:
        domain = "workspace"

    mode: TurnMode
    if investigative_hits:
        mode = "investigative"
    elif simple_hits and not (web_hits or workspace_hits):
        mode = "simple"
    else:
        mode = "simple"

    if broad_hits or (investigative_hits and len(lowered) > 120 and not _has_any(_DIRECT_EXECUTION_PATTERNS, lowered)):
        broad = True
    else:
        broad = False

    single_source_ok = _has_any(_SINGLE_SOURCE_PATTERNS, lowered)

    # Compute classification confidence
    hit_count = sum([
        investigative_hits,
        simple_hits and not (web_hits or workspace_hits),
        web_hits,
        workspace_hits,
    ])
    # High confidence when signals are clear and unambiguous
    if hit_count <= 1 and not investigative_hits and not simple_hits:
        confidence = 0.5  # Ambiguous — no strong signals
    elif investigative_hits and simple_hits:
        confidence = 0.6  # Conflicting signals
    else:
        confidence = 0.9  # Clear signals

    return InvestigationProfile(
        mode=mode,
        domain=domain,
        broad=broad,
        single_source_ok=single_source_ok,
        confidence=confidence,
    )


def should_auto_investigate(profile: InvestigationProfile, text: str) -> bool:
    """Return True when the request is broad enough for isolated investigation."""
    lowered = " ".join(str(text or "").split()).lower()
    if not profile.is_investigative or not profile.broad:
        return False
    if _has_any(_DIRECT_EXECUTION_PATTERNS, lowered) and not _has_any(_BROAD_PATTERNS, lowered):
        return False
    return True


def build_turn_policy(profile: InvestigationProfile) -> str | None:
    """Return a trusted turn-level policy block for investigative requests."""
    if not profile.is_investigative:
        return None

    lines = [
        "## Turn Execution Policy",
        "This request is investigative. Follow this loop: collect context -> analyze evidence -> act -> verify -> answer.",
        "Do not finalize with a polished generic answer before gathering concrete evidence.",
    ]
    if profile.domain in {"workspace", "mixed"}:
        lines.append("For repo or workspace questions, inspect files and/or command output before concluding.")
    if profile.domain in {"web", "mixed"}:
        lines.append(
            f"For web research, consult at least {profile.required_web_sources} concrete source(s) unless the user explicitly limited the scope."
        )
    lines.append("If evidence is thin, conflicting, or missing, say so explicitly and name the gap.")
    lines.append(
        "If you realize mid-task that you need more evidence than expected, "
        "escalate your investigation — use additional tools proactively rather "
        "than delivering incomplete results."
    )
    return "\n".join(lines)


def build_retry_instruction(
    profile: InvestigationProfile,
    *,
    missing: list[str],
    evidence: EvidenceLog,
) -> str:
    """Return a trusted follow-up instruction when the first answer was too shallow."""
    lines = [
        "[Runtime enforcement]",
        "Do not finalize yet. Gather more concrete evidence before answering.",
    ]
    if profile.domain in {"workspace", "mixed"}:
        lines.append("Inspect relevant files, search the repo, or run a read-only command/test if needed.")
    if profile.domain in {"web", "mixed"}:
        lines.append("Use web_search/web_fetch/browser tools to gather additional sources before concluding.")
    if missing:
        lines.append("Evidence gaps: " + "; ".join(missing))
    prior_checks = evidence.summary_lines(limit=2)
    if prior_checks:
        lines.append("What has already been checked:")
        lines.extend(f"- {line}" for line in prior_checks)
    lines.append(
        "If a tool fails, try an alternative approach: a different search query, "
        "a different URL, or a different tool entirely. Do not retry the same call."
    )
    return "\n".join(lines)


def build_provisional_response(
    draft: str | None,
    *,
    missing: list[str],
    evidence: EvidenceLog,
) -> str:
    """Return a bounded fallback when verification requirements were not met."""
    lines = [
        "I investigated this, but I do not have enough verified evidence to give a high-confidence final answer yet.",
    ]
    checked = evidence.summary_lines()
    if checked:
        lines.append("")
        lines.append("What I checked:")
        lines.extend(f"- {line}" for line in checked)
    if missing:
        lines.append("")
        lines.append("What is still missing:")
        lines.extend(f"- {item}" for item in missing)
    if draft:
        lines.append("")
        lines.append("Current best provisional answer:")
        lines.append(draft.strip())
    return "\n".join(lines).strip()


def verify_investigation(
    profile: InvestigationProfile,
    evidence: EvidenceLog,
) -> tuple[bool, list[str]]:
    """Return whether a final investigative answer is sufficiently grounded."""
    if not profile.is_investigative:
        return True, []

    missing: list[str] = []
    if profile.domain in {"workspace", "mixed"} and not evidence.has_repo_evidence:
        missing.append("inspect repository files or command output")
    if profile.domain in {"web", "mixed"} and len(evidence.web_sources) < profile.required_web_sources:
        missing.append(f"consult at least {profile.required_web_sources} distinct web source(s)")
    return not missing, missing


def record_tool_event(
    evidence: EvidenceLog,
    *,
    tool_name: str,
    arguments: dict[str, Any],
    result: str,
) -> None:
    """Track repo evidence, tests, and web sources from executed tools."""
    primary = ""
    if tool_name in {"read_file", "list_dir", "write_file", "edit_file"}:
        primary = str(arguments.get("path") or arguments.get("directory") or "").strip()
        if tool_name in {"read_file", "list_dir"}:
            _append_unique(evidence.repo_paths, primary)
    elif tool_name == "exec":
        command = str(arguments.get("command") or "").strip()
        primary = command
        _append_unique(evidence.commands, command)
        if _has_any(_TEST_COMMAND_PATTERNS, command):
            _append_unique(evidence.tests, command)
    elif tool_name == "web_fetch":
        primary = str(arguments.get("url") or "").strip()
        _append_unique(evidence.web_sources, primary)
    elif tool_name == "browser_navigate":
        primary = str(arguments.get("url") or "").strip()
        _append_unique(evidence.web_sources, primary)
    elif tool_name == "web_search":
        primary = str(arguments.get("query") or "").strip()
        try:
            payload = json.loads(result)
        except Exception:
            payload = {}
        for item in payload.get("sources", []) if isinstance(payload, dict) else []:
            if isinstance(item, dict):
                url = str(item.get("finalUrl") or item.get("url") or "").strip()
                _append_unique(evidence.web_sources, url)

    summary = primary or next(
        (
            str(value).strip()
            for key, value in arguments.items()
            if key in {"query", "url", "path", "command", "ref", "selector"} and str(value).strip()
        ),
        "",
    )
    _append_unique(
        evidence.tool_summaries,
        f"{tool_name}({summary})" if summary else tool_name,
    )
