from __future__ import annotations

from backend.config import _normalize_tool_name


def evaluate_tool_policy(
    tool_name: str,
    *,
    blocked_tools: set[str] | None = None,
    disabled_tools: set[str] | None = None,
    approval_tools: set[str] | None = None,
    auto_approved_tools: set[str] | None = None,
    enforce_approval_policy: bool = True,
    approval_override: bool = False,
) -> dict[str, object]:
    normalized_tool = _normalize_tool_name(str(tool_name or "").strip())
    blocked = set(blocked_tools or ())
    disabled = set(disabled_tools or ())
    approval = set(approval_tools or ())
    auto_approved = set(auto_approved_tools or ())

    is_disabled = normalized_tool in disabled
    is_blocked = normalized_tool in blocked
    approval_required = bool(
        normalized_tool
        and not is_disabled
        and not is_blocked
        and (approval_override or (enforce_approval_policy and normalized_tool in approval and normalized_tool not in auto_approved))
    )
    status = "allowed"
    if is_disabled:
        status = "disabled"
    elif is_blocked:
        status = "blocked"
    elif approval_required:
        status = "approval_required"

    return {
        "tool_name": normalized_tool,
        "status": status,
        "is_disabled": is_disabled,
        "is_blocked": is_blocked,
        "approval_required": approval_required,
        "is_auto_approved": normalized_tool in auto_approved,
        "allowed": status == "allowed",
    }


def _load_main_globals() -> None:
    import backend.main as main

    globals().update({name: value for name, value in vars(main).items() if not name.startswith("__")})


def build_hypothesis_runtime_helpers(*, goal, session, run_id, run_budget, manager, websocket, _hypothesis_runtime, _policy_budget_mode, get_memory, get_steps, use_internet, get_blocked_tools=None, get_disabled_tools=None, get_approval_tools=None):
    _load_main_globals()
    if True:
        def _human_hypothesis_event_text(
            event: dict,
            tool_name: str,
            *,
            prev_conf: float,
            new_conf: float,
            decision_mode: str,
            remediation_mode: str,
            step_index: int,
        ) -> str:
            outcome = str(event.get("outcome") or "")
            switched_to = str(event.get("switched_to") or "")
            if outcome == "support":
                signal_text = "result supports current hypothesis"
            elif outcome == "weak":
                signal_text = "evidence is limited / weak"
            elif outcome == "error":
                signal_text = "error signal lowers confidence"
            else:
                signal_text = "neutral signal"
            decision_text = decision_mode or "unknown"
            if switched_to:
                decision_text += f"; switch to {switched_to}"
            adjustment_text = remediation_mode or "no extra adjustment"
            return (
                "Situation: hypothesis evidence after tool step.\n"
                f"Signal: step={step_index}, tool={tool_name or 'unknown'}, {signal_text}, confidence {prev_conf:.2f} -> {new_conf:.2f}.\n"
                f"Decision: mode={decision_text}.\n"
                f"Adjustment: {adjustment_text}."
            )
    
        def _human_run_kpi_text(run_kpis: dict) -> str:
            totals = dict(run_kpis.get("totals") or {})
            budget = dict(run_kpis.get("budget_snapshot") or {})
            completed = bool(run_kpis.get("completed"))
            halted = bool(run_kpis.get("halted"))
            steps = int(totals.get("steps") or 0)
            calls = int(totals.get("tool_calls") or 0)
            issues = int(totals.get("issues") or 0)
            tokens = int(totals.get("tokens") or 0)
            first_pass = bool(run_kpis.get("first_pass_success"))
            corr_rate = float(run_kpis.get("correction_rate") or 0.0)
            eff = float(run_kpis.get("tool_call_efficiency") or 0.0)
            runtime_ms = int(run_kpis.get("time_to_solution_ms") or 0)
            max_steps = int(budget.get("max_steps") or 0)
            max_calls = int(budget.get("max_tool_calls") or 0)
            if completed and not halted:
                return (
                    "Situation: run completed successfully.\n"
                    f"Signal: steps={steps}, tool_calls={calls}, runtime_ms={runtime_ms}, first_pass={first_pass}, correction_rate={corr_rate:.2f}, tool_efficiency={eff:.2f}.\n"
                    "Decision: result is sufficient to finish.\n"
                    f"Adjustment: used budget max_steps={max_steps}, max_tool_calls={max_calls}, tokens={tokens}."
                )
            if halted:
                reason = str(run_kpis.get("halt_reason") or "unknown reason")
                return (
                    "Situation: run stopped early.\n"
                    f"Signal: halt_reason={reason}, steps={steps}, tool_calls={calls}, issues={issues}, tokens={tokens}.\n"
                    "Decision: stop due to policy/budget boundary.\n"
                    f"Adjustment: steer next runs using correction_rate={corr_rate:.2f}, tool_efficiency={eff:.2f}."
                )
            return (
                "Situation: KPI midpoint snapshot recorded.\n"
                f"Signal: steps={steps}, tool_calls={calls}, issues={issues}, tokens={tokens}.\n"
                "Decision: run still in progress.\n"
                f"Adjustment: keep monitoring correction_rate={corr_rate:.2f}, tool_efficiency={eff:.2f}."
            )
    
        def _human_learning_text(playbook_row: dict, policy_row: dict) -> str:
            status = str(playbook_row.get("status") or "updated")
            task_type = str(policy_row.get("task_type") or "generic")
            attempts = int(playbook_row.get("attempts") or 0)
            success_rate = float(playbook_row.get("success_rate") or 0.0)
            first_pass_rate = float(playbook_row.get("first_pass_rate") or 0.0)
            d_steps = int(policy_row.get("delta_steps") or 0)
            d_calls = int(policy_row.get("delta_tool_calls") or 0)
            d_runtime = int(policy_row.get("delta_runtime_seconds") or 0)
            d_tokens = int(policy_row.get("delta_tokens") or 0)
            return (
                "Situation: learning feedback stored.\n"
                f"Signal: task_type={task_type}, attempts={attempts}, success_rate={success_rate:.2f}, first_pass_rate={first_pass_rate:.2f}.\n"
                f"Decision: playbook_status={status}.\n"
                f"Adjustment: delta_steps={d_steps}, delta_tool_calls={d_calls}, delta_runtime_seconds={d_runtime}, delta_tokens={d_tokens}."
            )
    
        def _record_hypothesis_evidence(step_index: int, tool_name: str, tool_result: str, tool_details: dict | None = None) -> None:
            if not str(tool_name or "").strip():
                return
            _before_summary = summarize_hypothesis_runtime(_hypothesis_runtime)
            _before_conf = float(_before_summary.get("active_confidence") or 0.0)
            _before_active = str(_before_summary.get("active_hypothesis_id") or "")
            event = append_hypothesis_evidence(
                _hypothesis_runtime,
                tool_name=tool_name,
                tool_result=tool_result,
                step_index=step_index,
                tool_details=tool_details,
            )
            session["hypothesis_runtime"] = _hypothesis_runtime
            session["hypothesis_summary"] = summarize_hypothesis_runtime(_hypothesis_runtime)
            _after_summary = dict(session.get("hypothesis_summary") or {})
            _after_conf = float(_after_summary.get("active_confidence") or 0.0)
            _after_active = str(_after_summary.get("active_hypothesis_id") or "")
            session["hypothesis_decision"] = hypothesis_decision(_hypothesis_runtime)
            _dec_mode = str((session.get("hypothesis_decision") or {}).get("mode") or "")
            remediation = ""
            if _dec_mode == "switch_or_widen":
                _tools_left = max(0, int(run_budget.get("max_tool_calls", 0)) - int(run_budget.get("tool_calls", 0)))
                _steps_left = max(0, int(run_budget.get("max_steps", 0)) - int(run_budget.get("steps_executed", 0)))
                remediation = "widen_search"
                if _tools_left <= 1 and _steps_left <= 0:
                    remediation = "escalate_response_with_known_evidence"
                session["auto_remediation"] = {
                    "mode": remediation,
                    "tools_left": _tools_left,
                    "steps_left": _steps_left,
                    "source": "hypothesis_decision",
                }
            _goal_preview = str((_hypothesis_runtime.get("goal_preview") or ""))
            log_system_event(
                "hypothesis",
                "Evidence event",
                {
                    "run_id": run_id,
                    "event_id": str(event.get("event_id") or ""),
                    "tool": str(tool_name or ""),
                    "outcome": str(event.get("outcome") or ""),
                    "active_hypothesis": str(_hypothesis_runtime.get("active_hypothesis_id") or ""),
                    "goal_preview": _goal_preview,
                    "step_index": str(step_index),
                    "active_before": _before_active,
                    "active_after": _after_active,
                    "confidence_before": f"{_before_conf:.4f}",
                    "confidence_after": f"{_after_conf:.4f}",
                    "decision_mode": _dec_mode,
                    "remediation_mode": remediation,
                    "uitleg": _human_hypothesis_event_text(
                        event,
                        str(tool_name or ""),
                        prev_conf=_before_conf,
                        new_conf=_after_conf,
                        decision_mode=_dec_mode,
                        remediation_mode=remediation,
                        step_index=step_index,
                    ),
                },
            )
            if bool(event.get("switched")):
                log_system_event(
                    "hypothesis",
                    "Active hypothesis switched",
                    {
                        "run_id": run_id,
                        "switched_to": str(event.get("switched_to") or ""),
                        "mode": str((session.get("hypothesis_decision") or {}).get("mode") or "switch_or_widen"),
                        "active_before": _before_active,
                        "active_after": _after_active,
                        "confidence_before": f"{_before_conf:.4f}",
                        "confidence_after": f"{_after_conf:.4f}",
                        "uitleg": (
                            "Situation: active hypothesis switched.\n"
                            f"Signal: {_before_active} ({_before_conf:.2f}) -> {_after_active} ({_after_conf:.2f}).\n"
                            f"Decision: mode={str((session.get('hypothesis_decision') or {}).get('mode') or 'switch_or_widen')}.\n"
                            f"Adjustment: {remediation or 'no explicit adjustment'}."
                        ),
                    },
                )
    
        async def _apply_auto_remediation(step_index: int) -> bool:
            """Apply hypothesis remediation decisions to runtime behavior.
    
            Returns True when execution should stop early and report with known evidence.
            """
            remediation = dict(session.get("auto_remediation") or {})
            if str(remediation.get("source") or "") != "hypothesis_decision":
                return False
            mode = str(remediation.get("mode") or "").strip().lower()
            if not mode:
                return False
            if str(remediation.get("applied_for_step") or "") == str(step_index):
                return False
    
            remediation["applied_for_step"] = str(step_index)
            remediation["applied_at"] = datetime.now().isoformat()
            stop_early = False
    
            if mode == "widen_search":
                if not bool(run_budget.get("dynamic_unlimited")):
                    old_max_steps = int(run_budget.get("max_steps") or 0)
                    old_max_calls = int(run_budget.get("max_tool_calls") or 0)
                    run_budget["max_steps"] = min(30, old_max_steps + 1)
                    run_budget["max_tool_calls"] = min(80, old_max_calls + 2)
    
                current_steps = list(get_steps() or [])
                if use_internet and (step_index + 1) < len(current_steps):
                    next_step = current_steps[step_index + 1]
                    if isinstance(next_step, dict):
                        next_step["needs_search"] = True
                        hint = str(next_step.get("search_hint") or "").strip()
                        widen_hint = "widen search scope: include alternate provider/source, broader time range, and synonym terms"
                        if widen_hint not in hint.lower():
                            next_step["search_hint"] = f"{hint} | {widen_hint}" if hint else widen_hint
    
                await manager.send_message(
                    {
                        "type": "info",
                        "content": (
                            "Auto-remediation applied: widening search strategy and granting a small temporary budget extension."
                        ),
                    },
                    websocket,
                )
    
            elif mode == "escalate_response_with_known_evidence":
                stop_early = True
                await manager.send_message(
                    {
                        "type": "info",
                        "content": (
                            "Auto-remediation applied: switching to evidence-first response mode due to low confidence and low remaining budget."
                        ),
                    },
                    websocket,
                )
    
            session["auto_remediation"] = remediation
            return stop_early
    
        def _set_run_kpis_snapshot(*, completed: bool) -> None:
            current_memory = list(get_memory() or [])
            run_kpis = build_run_kpis(
                current_memory,
                run_budget,
                completed=completed,
                halted=bool(run_budget.get("halted")),
                halt_reason=str(run_budget.get("halt_reason") or ""),
            )
            session["run_kpis"] = run_kpis
            session["hypothesis_runtime"] = _hypothesis_runtime
            session["hypothesis_summary"] = summarize_hypothesis_runtime(_hypothesis_runtime)
            log_system_event(
                "run_kpi",
                "Run KPI snapshot",
                {
                    "run_id": run_id,
                    "completed": str(run_kpis.get("completed")),
                    "halted": str(run_kpis.get("halted")),
                    "steps": str((run_kpis.get("totals") or {}).get("steps", 0)),
                    "tool_calls": str((run_kpis.get("totals") or {}).get("tool_calls", 0)),
                    "first_pass_success": str(run_kpis.get("first_pass_success")),
                    "correction_rate": str(run_kpis.get("correction_rate")),
                    "tool_call_efficiency": str(run_kpis.get("tool_call_efficiency")),
                    "time_to_solution_ms": str(run_kpis.get("time_to_solution_ms")),
                    "halt_reason": str(run_kpis.get("halt_reason") or ""),
                    "uitleg": _human_run_kpi_text(run_kpis),
                },
            )
    
        def _persist_learning_feedback() -> None:
            run_kpis = dict(session.get("run_kpis") or {})
            if not run_kpis:
                return
            hypothesis_summary = dict(session.get("hypothesis_summary") or {})
            playbook_row = update_playbook_stats(
                goal=goal,
                task_state=session.get("task_state") if isinstance(session.get("task_state"), dict) else None,
                run_kpis=run_kpis,
                hypothesis_summary=hypothesis_summary,
            )
            policy_row = update_policy_feedback_from_kpis(
                session.get("task_state") if isinstance(session.get("task_state"), dict) else None,
                run_kpis,
            )
            session["learning_feedback"] = {
                "playbook": playbook_row,
                "policy": policy_row,
            }
            log_system_event(
                "learning",
                "Playbook and policy feedback updated",
                {
                    "run_id": run_id,
                    "playbook_key": str(playbook_row.get("key") or ""),
                    "playbook_status": str(playbook_row.get("status") or ""),
                    "task_type": str(policy_row.get("task_type") or ""),
                    "attempts": str(playbook_row.get("attempts") or 0),
                    "success_rate": str(playbook_row.get("success_rate") or 0.0),
                    "first_pass_rate": str(playbook_row.get("first_pass_rate") or 0.0),
                    "delta_steps": str(policy_row.get("delta_steps") or 0),
                    "delta_tool_calls": str(policy_row.get("delta_tool_calls") or 0),
                    "delta_runtime_seconds": str(policy_row.get("delta_runtime_seconds") or 0),
                    "delta_tokens": str(policy_row.get("delta_tokens") or 0),
                    "uitleg": _human_learning_text(playbook_row, policy_row),
                },
            )
    
        auto_approved_tools: set[str] = set()

        def _tool_policy_state(
            tool_name: str,
            *,
            enforce_approval_policy: bool = True,
            approval_override: bool = False,
        ) -> dict[str, object]:
            blocked = set(get_blocked_tools() or ()) if callable(get_blocked_tools) else set()
            disabled = set(get_disabled_tools() or ()) if callable(get_disabled_tools) else set()
            approval = set(get_approval_tools() or ()) if callable(get_approval_tools) else set()
            return evaluate_tool_policy(
                tool_name,
                blocked_tools=blocked,
                disabled_tools=disabled,
                approval_tools=approval,
                auto_approved_tools=auto_approved_tools,
                enforce_approval_policy=enforce_approval_policy,
                approval_override=approval_override,
            )
    
        async def _halt_run(reason_code: str, message: str):
            if run_budget["halted"]:
                return
            run_budget["halted"] = True
            run_budget["halt_reason"] = reason_code
            manager.active_runs[run_id] = False
            await manager.send_message({
                "type": "run_stopped", "reason_code": reason_code, "content": message,
            }, websocket)
            await manager.send_message({"type": "error", "content": message}, websocket)
    
        async def _check_runtime(where: str = "") -> bool:
            if run_budget["halted"]:
                return False
            # Detect user kill (active_runs set to False by stop handler)
            if not manager.active_runs.get(run_id, False):
                await _halt_run("USER_STOPPED", "Workflow stopped by user.")
                return False
            elapsed = int(time.monotonic() - run_budget["started_at"])
            limit = run_budget["max_runtime_seconds"]
            if elapsed > limit:
                suffix = f" ({where})" if where else ""
                await _halt_run("BUDGET_RUNTIME", f"Run stopped: runtime budget exceeded after {elapsed}s (limit {limit}s){suffix}.")
                return False
            return True
    
        async def _request_tool_approval(step_index: int, tool_name: str, tool_args: dict) -> tuple[bool, bool]:
            """Returns (approved, always). When always=True the caller should skip future prompts for this tool."""
            if tool_name in auto_approved_tools:
                return True, True
            ap_event = asyncio.Event()
            manager.pending_approvals[run_id] = {"event": ap_event, "response": None, "note": ""}
            await manager.send_message({
                "type": "approval_needed", "step_index": step_index,
                "step": f"Tool approval needed: {tool_name}",
                "tool": tool_name, "args": tool_args,
            }, websocket)
            try:
                await asyncio.wait_for(ap_event.wait(), timeout=180)
            except asyncio.TimeoutError:
                manager.pending_approvals.pop(run_id, None)
                return False, False
            ap = dict(manager.pending_approvals.pop(run_id, {}) or {})
            resp = ap.get("response", "reject")
            if resp == "reject":
                return False, False
            always = resp == "approve_always"
            if always:
                auto_approved_tools.add(tool_name)
            return True, always

        async def _authorize_tool_call(
            step_index: int,
            tool_name: str,
            tool_args: dict,
            *,
            enforce_approval_policy: bool = True,
            approval_override: bool = False,
            prompt_on_approval: bool = True,
            disabled_message: str | None = None,
            blocked_message: str | None = None,
            approval_message: str | None = None,
        ) -> tuple[bool, str, dict[str, object]]:
            policy = _tool_policy_state(
                tool_name,
                enforce_approval_policy=enforce_approval_policy,
                approval_override=approval_override,
            )
            normalized_tool = str(policy.get("tool_name") or str(tool_name or "").strip())
            if bool(policy.get("is_disabled")):
                return False, (disabled_message or f"TOOL_DISABLED: Skill '{normalized_tool}' is disabled in Settings > Skills."), policy
            if bool(policy.get("is_blocked")):
                return False, (blocked_message or f"POLICY_DENIED: Skill '{normalized_tool}' is blocked by policy."), policy
            if not bool(policy.get("approval_required")):
                return True, "", policy
            if not prompt_on_approval:
                return False, (approval_message or f"HITL_REQUIRED: Skill '{normalized_tool}' was not approved by user."), policy
            approved, _always = await _request_tool_approval(step_index, normalized_tool, tool_args)
            if not approved:
                return False, (approval_message or f"HITL_REQUIRED: Skill '{normalized_tool}' was not approved by user."), policy
            return True, "", dict(policy, status="allowed", allowed=True)
    return {
        "record_hypothesis_evidence": _record_hypothesis_evidence,
        "apply_auto_remediation": _apply_auto_remediation,
        "set_run_kpis_snapshot": _set_run_kpis_snapshot,
        "persist_learning_feedback": _persist_learning_feedback,
        "halt_run": _halt_run,
        "check_runtime": _check_runtime,
        "tool_policy_state": _tool_policy_state,
        "authorize_tool_call": _authorize_tool_call,
        "request_tool_approval": _request_tool_approval,
        "auto_approved_tools": auto_approved_tools,
    }