/**
 * Loop Extension
 *
 * Provides a /loop command that starts a follow-up loop with a breakout condition.
 * The loop keeps sending a prompt on turn end until the agent calls the
 * signal_loop_success tool.
 */

import { Type } from "@sinclair/typebox";
import { complete, type Api, type Model, type UserMessage } from "@mariozechner/pi-ai";
import type { ExtensionAPI, ExtensionContext, SessionSwitchEvent } from "@mariozechner/pi-coding-agent";
import { compact } from "@mariozechner/pi-coding-agent";
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";

type LoopMode = "tests" | "custom" | "self";

type LoopStateData = {
	active: boolean;
	mode?: LoopMode;
	condition?: string;
	prompt?: string;
	summary?: string;
	loopCount?: number;
};

const LOOP_PRESETS = [
  { value: "tests", label: "Until tests pass", description: "" },
  { value: "custom", label: "Until custom condition", description: "" },
  { value: "self", label: "Self driven (agent decides)", description: "" },
] as const;

const LOOP_STATE_ENTRY = "loop-state";

const HAIKU_MODEL_ID = "claude-haiku-4-5";

const SUMMARY_SYSTEM_PROMPT = `You summarize loop breakout conditions for a status widget.
Return a concise phrase (max 6 words) that says when the loop should stop.
Use plain text only, no quotes, no punctuation, no prefix.

Form should be "breaks when ...", "loops until ...", "stops on ...", "runs until ...", or similar.
Use the best form that makes sense for the loop condition.
`;

function buildPrompt(mode: LoopMode, condition?: string): string {
  switch (mode) {
    case "tests":
      return (
        "Run all tests. If they are passing, call the signal_loop_success tool. " +
				"Otherwise continue until the tests pass."
      );
    case "custom": {
      const customCondition = condition?.trim() || "the custom condition is satisfied";
      return (
        `Continue until the following condition is satisfied: ${customCondition}. ` +
				"When it is satisfied, call the signal_loop_success tool."
      );
    }
    case "self":
      return "Continue until you are done. When finished, call the signal_loop_success tool.";
  }
}

function summarizeCondition(mode: LoopMode, condition?: string): string {
  switch (mode) {
    case "tests":
      return "tests pass";
    case "custom": {
      const summary = condition?.trim() || "custom condition";
      return summary.length > 48 ? `${summary.slice(0, 45)}...` : summary;
    }
    case "self":
      return "done";
  }
}

function getConditionText(mode: LoopMode, condition?: string): string {
  switch (mode) {
    case "tests":
      return "tests pass";
    case "custom":
      return condition?.trim() || "custom condition";
    case "self":
      return "you are done";
  }
}

async function selectSummaryModel(
  ctx: ExtensionContext,
): Promise<{ model: Model<Api>; apiKey: string } | null> {
  if (!ctx.model) {return null;}

  if (ctx.model.provider === "anthropic") {
    const haikuModel = ctx.modelRegistry.find("anthropic", HAIKU_MODEL_ID);
    if (haikuModel) {
      const apiKey = await ctx.modelRegistry.getApiKey(haikuModel);
      if (apiKey) {
        return { model: haikuModel, apiKey };
      }
    }
  }

  const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
  if (!apiKey) {return null;}
  return { model: ctx.model, apiKey };
}

async function summarizeBreakoutCondition(
  ctx: ExtensionContext,
  mode: LoopMode,
  condition?: string,
): Promise<string> {
  const fallback = summarizeCondition(mode, condition);
  const selection = await selectSummaryModel(ctx);
  if (!selection) {return fallback;}

  const conditionText = getConditionText(mode, condition);
  const userMessage: UserMessage = {
    role: "user",
    content: [{ type: "text", text: conditionText }],
    timestamp: Date.now(),
  };

  const response = await complete(
    selection.model,
    { systemPrompt: SUMMARY_SYSTEM_PROMPT, messages: [userMessage] },
    { apiKey: selection.apiKey },
  );

  if (response.stopReason === "aborted" || response.stopReason === "error") {
    return fallback;
  }

  const summary = response.content
    .filter((c): c is { type: "text"; text: string } => c.type === "text")
    .map((c) => c.text)
    .join(" ")
    .replace(/\s+/g, " ")
    .trim();

  if (!summary) {return fallback;}
  return summary.length > 60 ? `${summary.slice(0, 57)}...` : summary;
}

function getCompactionInstructions(mode: LoopMode, condition?: string): string {
  const conditionText = getConditionText(mode, condition);
  return `Loop active. Breakout condition: ${conditionText}. Preserve this loop state and breakout condition in the summary.`;
}

function updateStatus(ctx: ExtensionContext, state: LoopStateData): void {
  if (!ctx.hasUI) {return;}
  if (!state.active || !state.mode) {
    ctx.ui.setWidget("loop", undefined);
    return;
  }
  const loopCount = state.loopCount ?? 0;
  const turnText = `(turn ${loopCount})`;
  const summary = state.summary?.trim();
  const text = summary
    ? `Loop active: ${summary} ${turnText}`
    : `Loop active ${turnText}`;
  ctx.ui.setWidget("loop", [ctx.ui.theme.fg("accent", text)]);
}

async function loadState(ctx: ExtensionContext): Promise<LoopStateData> {
  const entries = ctx.sessionManager.getEntries();
  for (let i = entries.length - 1; i >= 0; i--) {
    const entry = entries[i] as { type: string; customType?: string; data?: LoopStateData };
    if (entry.type === "custom" && entry.customType === LOOP_STATE_ENTRY && entry.data) {
      return entry.data;
    }
  }
  return { active: false };
}

export default function loopExtension(pi: ExtensionAPI): void {
  let loopState: LoopStateData = { active: false };

  function persistState(state: LoopStateData): void {
    pi.appendEntry(LOOP_STATE_ENTRY, state);
  }

  function setLoopState(state: LoopStateData, ctx: ExtensionContext): void {
    loopState = state;
    persistState(state);
    updateStatus(ctx, state);
  }

  function clearLoopState(ctx: ExtensionContext): void {
    const cleared: LoopStateData = { active: false };
    loopState = cleared;
    persistState(cleared);
    updateStatus(ctx, cleared);
  }

  function breakLoop(ctx: ExtensionContext): void {
    clearLoopState(ctx);
    ctx.ui.notify("Loop ended", "info");
  }

  function wasLastAssistantAborted(messages: Array<{ role?: string; stopReason?: string }>): boolean {
    for (let i = messages.length - 1; i >= 0; i--) {
      const message = messages[i];
      if (message?.role === "assistant") {
        return message.stopReason === "aborted";
      }
    }
    return false;
  }

  function triggerLoopPrompt(ctx: ExtensionContext): void {
    if (!loopState.active || !loopState.mode || !loopState.prompt) {return;}
    if (ctx.hasPendingMessages()) {return;}

    const loopCount = (loopState.loopCount ?? 0) + 1;
    loopState = { ...loopState, loopCount };
    persistState(loopState);
    updateStatus(ctx, loopState);

    pi.sendMessage({
      customType: "loop",
      content: loopState.prompt,
      display: true
    }, {
      deliverAs: "followUp",
      triggerTurn: true
    });
  }

  async function showLoopSelector(ctx: ExtensionContext): Promise<LoopStateData | null> {
    const items: SelectItem[] = LOOP_PRESETS.map((preset) => ({
      value: preset.value,
      label: preset.label,
      description: preset.description,
    }));

    const selection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
      const container = new Container();
      container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
      container.addChild(new Text(theme.fg("accent", theme.bold("Select a loop preset"))));

      const selectList = new SelectList(items, Math.min(items.length, 10), {
        selectedPrefix: (text) => theme.fg("accent", text),
        selectedText: (text) => theme.fg("accent", text),
        description: (text) => theme.fg("muted", text),
        scrollInfo: (text) => theme.fg("dim", text),
        noMatch: (text) => theme.fg("warning", text),
      });

      selectList.onSelect = (item) => done(item.value);
      selectList.onCancel = () => done(null);

      container.addChild(selectList);
      container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
      container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));

      return {
        render(width: number) {
          return container.render(width);
        },
        invalidate() {
          container.invalidate();
        },
        handleInput(data: string) {
          selectList.handleInput(data);
          tui.requestRender();
        },
      };
    });

    if (!selection) {return null;}

    switch (selection) {
      case "tests":
        return { active: true, mode: "tests", prompt: buildPrompt("tests") };
      case "self":
        return { active: true, mode: "self", prompt: buildPrompt("self") };
      case "custom": {
        const condition = await ctx.ui.editor("Enter loop breakout condition:", "");
        if (!condition?.trim()) {return null;}
        return {
          active: true,
          mode: "custom",
          condition: condition.trim(),
          prompt: buildPrompt("custom", condition.trim()),
        };
      }
      default:
        return null;
    }
  }

  function parseArgs(args: string | undefined): LoopStateData | null {
    if (!args?.trim()) {return null;}
    const parts = args.trim().split(/\s+/);
    const mode = parts[0]?.toLowerCase();

    switch (mode) {
      case "tests":
        return { active: true, mode: "tests", prompt: buildPrompt("tests") };
      case "self":
        return { active: true, mode: "self", prompt: buildPrompt("self") };
      case "custom": {
        const condition = parts.slice(1).join(" ").trim();
        if (!condition) {return null;}
        return {
          active: true,
          mode: "custom",
          condition,
          prompt: buildPrompt("custom", condition),
        };
      }
      default:
        return null;
    }
  }

  pi.registerTool({
    name: "signal_loop_success",
    label: "Signal Loop Success",
    description: "Stop the active loop when the breakout condition is satisfied. Only call this tool when explicitly instructed to do so by the user, tool or system prompt.",
    parameters: Type.Object({}),
    async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
      if (!loopState.active) {
        return {
          content: [{ type: "text", text: "No active loop is running." }],
          details: { active: false },
        };
      }

      clearLoopState(ctx);

      return {
        content: [{ type: "text", text: "Loop ended." }],
        details: { active: false },
      };
    },
  });

  pi.registerCommand("loop", {
    description: "Start a follow-up loop until a breakout condition is met",
    handler: async(args, ctx) => {
      let nextState = parseArgs(args);
      if (!nextState) {
        if (!ctx.hasUI) {
          ctx.ui.notify("Usage: /loop tests | /loop custom <condition> | /loop self", "warning");
          return;
        }
        nextState = await showLoopSelector(ctx);
      }

      if (!nextState) {
        ctx.ui.notify("Loop cancelled", "info");
        return;
      }

      if (loopState.active) {
        const confirm = ctx.hasUI
          ? await ctx.ui.confirm("Replace active loop?", "A loop is already active. Replace it?")
          : true;
        if (!confirm) {
          ctx.ui.notify("Loop unchanged", "info");
          return;
        }
      }

      const summarizedState: LoopStateData = { ...nextState, summary: undefined, loopCount: 0 };
      setLoopState(summarizedState, ctx);
      ctx.ui.notify("Loop active", "info");
      triggerLoopPrompt(ctx);

      const mode = nextState.mode!;
      const condition = nextState.condition;
      void (async() => {
        const summary = await summarizeBreakoutCondition(ctx, mode, condition);
        if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition) {return;}
        loopState = { ...loopState, summary };
        persistState(loopState);
        updateStatus(ctx, loopState);
      })();
    },
  });

  pi.on("agent_end", async(event, ctx) => {
    if (!loopState.active) {return;}

    if (ctx.hasUI && wasLastAssistantAborted(event.messages)) {
      const confirm = await ctx.ui.confirm(
        "Break active loop?",
        "Operation aborted. Break out of the loop?",
      );
      if (confirm) {
        breakLoop(ctx);
        return;
      }
    }

    triggerLoopPrompt(ctx);
  });

  pi.on("session_before_compact", async(event, ctx) => {
    if (!loopState.active || !loopState.mode || !ctx.model) {return;}
    const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
    if (!apiKey) {return;}

    const instructionParts = [event.customInstructions, getCompactionInstructions(loopState.mode, loopState.condition)]
      .filter(Boolean)
      .join("\n\n");

    try {
      const compaction = await compact(event.preparation, ctx.model, apiKey, instructionParts, event.signal);
      return { compaction };
    } catch (error) {
      if (ctx.hasUI) {
        const message = error instanceof Error ? error.message : String(error);
        ctx.ui.notify(`Loop compaction failed: ${message}`, "warning");
      }
      return;
    }
  });

  async function restoreLoopState(ctx: ExtensionContext): Promise<void> {
    loopState = await loadState(ctx);
    updateStatus(ctx, loopState);

    if (loopState.active && loopState.mode && !loopState.summary) {
      const mode = loopState.mode;
      const condition = loopState.condition;
      void (async() => {
        const summary = await summarizeBreakoutCondition(ctx, mode, condition);
        if (!loopState.active || loopState.mode !== mode || loopState.condition !== condition) {return;}
        loopState = { ...loopState, summary };
        persistState(loopState);
        updateStatus(ctx, loopState);
      })();
    }
  }

  pi.on("session_start", async(_event, ctx) => {
    await restoreLoopState(ctx);
  });

  pi.on("session_switch", async(_event: SessionSwitchEvent, ctx) => {
    await restoreLoopState(ctx);
  });
}
