/**
 * BrowserPlanner — the agent loop.
 *
 *   observe  →  think (LLM)  →  act  →  repeat, until `done`, `abort`, or maxSteps.
 *
 * The LLM is prompted to emit a strict JSON object describing one of a small
 * set of primitive actions. We parse it, safety-gate it, execute it, and feed
 * the resulting observation back on the next turn.
 *
 * Cross-provider portable: no tool-calling APIs. Just JSON-in-text.
 */
import { completeSimple } from "@dotkaio/avis-ai";
import { formatSnapshot } from "./browser-observer.js";
import { BrowserActionDenied } from "./browser-session.js";
const SYSTEM_PROMPT = `You are Avis operating a real web browser on behalf of the user.

You will receive a snapshot of the current page that lists interactive elements
with stable numeric indexes like:

  [0] <a> href=/login Sign in
  [1] <input> type=email placeholder=Email "email"
  [2] <button> "Continue"

On each turn, output EXACTLY ONE JSON object describing the next action you
want to perform. Do NOT output anything else — no prose, no markdown, no
code fences. Just the JSON.

Valid actions:

  { "type": "navigate", "url": "https://…" }
  { "type": "click", "index": N }
  { "type": "dblclick", "index": N }
  { "type": "type", "index": N, "text": "…", "submit": false }

  # Password fields: use the credential placeholder — Avis resolves it from
  # the user's keychain at type-time and you (the LLM) never see the value:
  { "type": "type", "index": N, "text": "{{cred}}" }
  { "type": "select", "index": N, "option": "…" }
  { "type": "press", "keys": "Enter" | "Escape" | "Control+A" | … }
  { "type": "scroll", "direction": "up" | "down" | "top" | "bottom", "pixels": 600 }
  { "type": "wait", "seconds": 1 }
  { "type": "open_tab", "url": "https://…" }
  { "type": "switch_tab", "id": N }
  { "type": "ask_user", "question": "…" }
  { "type": "done", "summary": "what I did", "success": true }
  { "type": "abort", "reason": "why I cannot continue" }

You MAY include a "reasoning" field with one short sentence explaining your
choice. Do not include multiple actions.

Rules:
- Use the smallest action that makes progress.
- If the task is finished, emit {"type":"done", …}.
- If the task is impossible or unsafe, emit {"type":"abort", …}.
- If you need information from the user, emit {"type":"ask_user", …}.
- Indexes come from the MOST RECENT snapshot. Do not invent them.
- Never assume you are logged in unless the snapshot suggests it.
- Prefer visible text over selectors; you can only refer to elements by index.
- The browser may be running headless/in the background. Do not depend on the user seeing the page.
- If login, MFA, passkey, CAPTCHA, or any secure human approval is required, use {"type":"ask_user",...} instead of trying to bypass it.
- When auth signals are present, make the ask_user question explicit and short, e.g. "GitHub login required. Please complete sign-in, then reply done." or "2FA required. Complete verification, then reply done."
- Prefer reusing existing authenticated sessions/cookies from the persistent profile.
- Never attempt destructive actions (delete, unfollow, send, transfer, pay,
  archive, block, logout) without a clear user request. Safety gating may
  ask the user to confirm — that is expected and fine.
`;
// ───────────────────── helpers ─────────────────────
function stripCodeFences(s) {
    let t = s.trim();
    // Some models insist on fences despite instructions.
    if (t.startsWith("```")) {
        const firstNl = t.indexOf("\n");
        if (firstNl !== -1)
            t = t.slice(firstNl + 1);
        if (t.endsWith("```"))
            t = t.slice(0, -3);
    }
    return t.trim();
}
function tryParseAction(raw) {
    const s = stripCodeFences(raw);
    // Try whole string first, then progressively smaller JSON windows.
    const candidates = [s];
    const firstBrace = s.indexOf("{");
    const lastBrace = s.lastIndexOf("}");
    if (firstBrace !== -1 && lastBrace > firstBrace)
        candidates.push(s.slice(firstBrace, lastBrace + 1));
    for (const c of candidates) {
        try {
            const parsed = JSON.parse(c);
            if (parsed && typeof parsed === "object" && typeof parsed.type === "string") {
                return parsed;
            }
        }
        catch {
            // try next
        }
    }
    return null;
}
function buildObservationMessage(snap, task, stepNum, priorResults) {
    const history = priorResults.length > 0
        ? `\n\nPrior actions (most recent last):\n${priorResults.map((r, i) => `  ${i + 1}. ${r}`).join("\n")}`
        : "";
    return `Task: ${task}

Step: ${stepNum}${history}

Current page snapshot:
${formatSnapshot(snap)}

Respond with ONE JSON action object only.`;
}
// ───────────────────── executor ─────────────────────
async function executeAction(session, action, askUser) {
    switch (action.type) {
        case "navigate": {
            const s = await session.open(action.url);
            return `navigated to ${s.url}`;
        }
        case "click": {
            return session.click({ index: action.index });
        }
        case "dblclick": {
            return session.click({ index: action.index }, { dblclick: true });
        }
        case "type": {
            return session.type({ index: action.index }, action.text, { submit: action.submit });
        }
        case "select": {
            return session.select({ index: action.index }, action.option);
        }
        case "press": {
            return session.press(action.keys);
        }
        case "scroll": {
            return session.scroll({ direction: action.direction, pixels: action.pixels });
        }
        case "wait": {
            const secs = Math.max(0, Math.min(10, Math.floor(action.seconds)));
            await new Promise((r) => setTimeout(r, secs * 1000));
            return `waited ${secs}s`;
        }
        case "open_tab": {
            await session.openTabAndActivate(action.url);
            return `opened tab${action.url ? ` @ ${action.url}` : ""}`;
        }
        case "switch_tab": {
            await session.switchTab(action.id);
            return `switched to tab ${action.id}`;
        }
        case "ask_user": {
            if (!askUser)
                return "ask_user: no asker installed";
            const answer = await askUser(action.question);
            if (answer === null)
                throw new Error("user cancelled");
            return `user answered: ${answer}`;
        }
        case "done":
        case "abort":
            // Terminal actions are handled by the caller.
            return action.type;
    }
}
// ───────────────────── run ─────────────────────
async function captureFinalState(session, priorResults) {
    let pageText;
    let finalUrl;
    try {
        pageText = await session.getVisibleText();
        finalUrl = session.status().url;
    }
    catch {
        /* browser may have closed */
    }
    return { pageText, actionLog: [...priorResults], finalUrl };
}
export async function runPlanner(task, session, model, modelRegistry, opts = {}) {
    const maxSteps = opts.maxSteps ?? 20;
    const emit = (e) => opts.onEvent?.(e);
    const priorResults = [];
    const authRes = await modelRegistry.getApiKeyAndHeaders(model);
    if (!authRes.ok)
        throw new Error(`model auth failed: ${authRes.error}`);
    const apiKey = authRes.apiKey ?? "";
    const headers = authRes.headers;
    for (let step = 0; step < maxSteps; step++) {
        if (opts.signal?.aborted) {
            emit({ type: "aborted", step, reason: "signal aborted" });
            return { success: false, summary: "aborted by signal", steps: step };
        }
        // 1. Observe
        let snap;
        try {
            snap = await session.observe();
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            emit({ type: "error", step, error: `observe failed: ${msg}` });
            return { success: false, summary: `observe failed: ${msg}`, steps: step };
        }
        emit({
            type: "step_start",
            step,
            url: snap.url,
            title: snap.title,
            elementCount: snap.elements.length,
        });
        // 2. Think — with one parse retry
        emit({ type: "thinking", step });
        const userMessage = buildObservationMessage(snap, task, step, priorResults);
        let action = null;
        let rawReply = "";
        for (let attempt = 0; attempt < 2 && !action; attempt++) {
            const promptText = attempt === 0
                ? userMessage
                : `${userMessage}\n\nYour previous reply was not valid JSON. Return ONLY the JSON action object.`;
            try {
                const response = await completeSimple(model, {
                    systemPrompt: SYSTEM_PROMPT,
                    messages: [
                        {
                            role: "user",
                            content: [{ type: "text", text: promptText }],
                            timestamp: Date.now(),
                        },
                    ],
                }, {
                    maxTokens: 1024,
                    signal: opts.signal,
                    apiKey,
                    headers,
                });
                if (response.stopReason === "error") {
                    throw new Error(response.errorMessage || "LLM returned error stopReason");
                }
                rawReply = response.content
                    .filter((c) => c.type === "text")
                    .map((c) => c.text)
                    .join("")
                    .trim();
                action = tryParseAction(rawReply);
                if (!action)
                    emit({ type: "parse_error", step, raw: rawReply, attempt });
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                emit({ type: "error", step, error: `llm call failed: ${msg}` });
                return { success: false, summary: `llm call failed: ${msg}`, steps: step };
            }
        }
        if (!action) {
            emit({
                type: "error",
                step,
                error: `could not parse action after 2 attempts. last reply: ${rawReply.slice(0, 200)}`,
            });
            return { success: false, summary: "planner gave up: unparseable LLM output", steps: step };
        }
        emit({ type: "action_proposed", step, action });
        // 3. Terminal?
        if (action.type === "done") {
            emit({ type: "done", step, summary: action.summary, success: action.success ?? true });
            const finalState = await captureFinalState(session, priorResults);
            return { success: action.success ?? true, summary: action.summary, steps: step + 1, ...finalState };
        }
        if (action.type === "abort") {
            emit({ type: "aborted", step, reason: action.reason });
            const finalState = await captureFinalState(session, priorResults);
            return { success: false, summary: `aborted: ${action.reason}`, steps: step + 1, ...finalState };
        }
        // 4. Execute (safety is applied inside session methods)
        try {
            const result = await executeAction(session, action, opts.onAskUser);
            emit({ type: "action_result", step, action, result });
            priorResults.push(`${summarizeAction(action)} → ${result}`);
            // Small cooldown so pages can settle before next observe.
            await new Promise((r) => setTimeout(r, 300));
        }
        catch (err) {
            if (err instanceof BrowserActionDenied) {
                emit({ type: "action_denied", step, action, reason: err.reason, byUser: err.byUser });
                priorResults.push(`${summarizeAction(action)} → DENIED: ${err.reason}`);
                // Let the planner see the denial and adapt. Do not abort the run.
                continue;
            }
            const msg = err instanceof Error ? err.message : String(err);
            emit({ type: "error", step, error: `action failed: ${msg}` });
            priorResults.push(`${summarizeAction(action)} → ERROR: ${msg}`);
            // Keep going; planner may recover.
        }
    }
    emit({ type: "aborted", step: maxSteps, reason: `max steps (${maxSteps}) reached` });
    const finalState = await captureFinalState(session, priorResults);
    return { success: false, summary: `max steps (${maxSteps}) reached`, steps: maxSteps, ...finalState };
}
function summarizeAction(a) {
    switch (a.type) {
        case "navigate":
            return `navigate ${a.url}`;
        case "click":
            return `click[${a.index}]`;
        case "dblclick":
            return `dblclick[${a.index}]`;
        case "type":
            return `type[${a.index}] "${a.text.slice(0, 30)}"${a.submit ? " +submit" : ""}`;
        case "select":
            return `select[${a.index}] "${a.option}"`;
        case "press":
            return `press ${a.keys}`;
        case "scroll":
            return `scroll ${a.direction ?? "down"}${a.pixels ? ` ${a.pixels}px` : ""}`;
        case "wait":
            return `wait ${a.seconds}s`;
        case "open_tab":
            return `open_tab ${a.url ?? ""}`.trim();
        case "switch_tab":
            return `switch_tab ${a.id}`;
        case "ask_user":
            return `ask_user "${a.question.slice(0, 40)}"`;
        case "done":
            return "done";
        case "abort":
            return "abort";
    }
}
//# sourceMappingURL=browser-planner.js.map