/**
 * agent-session-runner.ts
 *
 * Upgraded Cloud Agent Executor for VibnCode.
 * Implements 4-level Smart Concurrency (parallel reads/lookups) and the
 * Ralph Loop (autonomous self-correction) entirely inside your secure Cloud VM.
 */

import { execSync } from "child_process";
import { callVibnChat } from "./llm/vibn-chat-model";
import { ChatMessage } from "./llm/gemini-chat";
import { AgentConfig } from "./agents";
import { executeTool, ToolContext } from "./tools";
import { resolvePrompt } from "./prompts/loader";

const MAX_TURNS = 80;

function runBuildVerification(
  repoRoot: string,
  appPath: string,
): { success: boolean; error?: string } {
  const fs = require("fs") as typeof import("fs");
  const path = require("path") as typeof import("path");
  const { execSync } = require("child_process");

  const absoluteAppPath = path.join(repoRoot, appPath);

  // Find all directories containing package.json (excluding node_modules, .git, .next, .vibncode, dist)
  const pkgDirs: string[] = [];

  function scan(dir: string) {
    try {
      const files = fs.readdirSync(dir);
      if (files.includes("package.json")) {
        pkgDirs.push(dir);
      }
      for (const file of files) {
        if (
          file === "node_modules" ||
          file === ".git" ||
          file === ".next" ||
          file === ".vibncode" ||
          file === "dist"
        ) {
          continue;
        }
        const full = path.join(dir, file);
        if (fs.statSync(full).isDirectory()) {
          scan(full);
        }
      }
    } catch {}
  }

  scan(absoluteAppPath);

  if (pkgDirs.length === 0) {
    return { success: true }; // No package.json anywhere, skip build check
  }

  for (const dir of pkgDirs) {
    const pkgJsonPath = path.join(dir, "package.json");
    try {
      const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
      // Skip if there's no build script or if it's the root container which is just a workspace wrapper
      if (!pkg.scripts || !pkg.scripts.build || pkg.name === "workspace") {
        continue;
      }

      console.log(
        `[Ralph Loop] Running automatic build verification: npm run build inside ${dir}...`,
      );
      execSync("npm run build", {
        cwd: dir,
        stdio: "pipe",
        timeout: 60000,
      });
    } catch (err: any) {
      const stderr = err.stderr
        ? err.stderr.toString()
        : err.message || String(err);
      console.warn(
        `[Ralph Loop] Build verification failed inside ${dir}:`,
        stderr,
      );
      return {
        success: false,
        error: `Build failed in directory "${path.relative(repoRoot, dir)}":\n${stderr.slice(-3000)}`,
      };
    }
  }

  return { success: true };
}

export interface OutputLine {
  ts: string;
  type: "step" | "stdout" | "stderr" | "info" | "error" | "done";
  text: string;
}

export interface SessionRunOptions {
  sessionId: string;
  projectId: string;
  vibnApiUrl: string; // e.g. https://vibnai.com
  appPath: string; // relative path within repo, e.g. "apps/admin"
  repoRoot?: string; // absolute path to the git repo root (for auto-commit)
  isStopped: () => boolean;
  autoApprove?: boolean;
  giteaRepo?: string; // e.g. "mark/sportsy"
  coolifyAppUuid?: string;
  coolifyApiUrl?: string;
  coolifyApiToken?: string;
}

// ── VIBN DB bridge ────────────────────────────────────────────────────────────

async function patchSession(
  opts: SessionRunOptions,
  payload: {
    status?: string;
    outputLine?: OutputLine;
    changedFile?: { path: string; status: string };
    error?: string;
  },
): Promise<void> {
  const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
  try {
    await fetch(url, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET ?? "",
      },
      body: JSON.stringify(payload),
    });
  } catch (err) {
    console.warn(
      "[session-runner] PATCH failed:",
      err instanceof Error ? err.message : err,
    );
  }
}

function now(): string {
  return new Date().toISOString();
}

// ── File change tracking ──────────────────────────────────────────────────────

const FILE_WRITE_TOOLS = new Set([
  "write_file",
  "replace_in_file",
  "create_file",
  "fs_write",
  "fs_edit",
]);

function extractChangedFile(
  toolName: string,
  args: Record<string, unknown>,
  workspaceRoot: string,
  appPath: string,
): { path: string; status: string } | null {
  if (!FILE_WRITE_TOOLS.has(toolName)) return null;
  const rawPath = String(args.path ?? args.file_path ?? "");
  if (!rawPath) return null;

  // Make path relative to appPath for display
  const fullPrefix = `${workspaceRoot}/${appPath}/`;
  const appPrefix = `${appPath}/`;
  let displayPath = rawPath.replace(fullPrefix, "").replace(appPrefix, "");

  const fileStatus =
    toolName === "write_file" || toolName === "fs_write" ? "added" : "modified";
  return { path: displayPath, status: fileStatus };
}

// ── Auto-commit helper ────────────────────────────────────────────────────────

async function autoCommitAndDeploy(
  opts: SessionRunOptions,
  task: string,
  emit: (line: OutputLine) => Promise<void>,
): Promise<void> {
  const repoRoot = opts.repoRoot;
  if (!repoRoot || !opts.giteaRepo) {
    await emit({
      ts: now(),
      type: "info",
      text: "Auto-approve skipped — no repo root available.",
    });
    return;
  }

  const gitOpts = { cwd: repoRoot, stdio: "pipe" as const };
  const giteaApiUrl = process.env.GITEA_API_URL || "";
  const giteaUsername = process.env.GITEA_USERNAME || "agent";
  const giteaToken = process.env.GITEA_API_TOKEN || "";

  try {
    try {
      execSync('git config user.email "agent@vibnai.com"', gitOpts);
      execSync('git config user.name "VIBN Agent"', gitOpts);
    } catch {
      /* already set */
    }

    execSync("git add -A", gitOpts);

    const status = execSync("git status --porcelain", gitOpts)
      .toString()
      .trim();
    if (!status) {
      await emit({
        ts: now(),
        type: "info",
        text: "✓ No file changes to commit.",
      });
      await patchSession(opts, { status: "approved" });
      return;
    }

    const commitMsg = `agent: ${task.slice(0, 72)}`;
    const msgFile = require("path").join(
      opts.repoRoot || process.cwd(),
      ".git",
      "COMMIT_EDITMSG",
    );
    require("fs").writeFileSync(msgFile, commitMsg, "utf8");
    execSync("git commit -F .git/COMMIT_EDITMSG", gitOpts);
    try {
      require("fs").unlinkSync(msgFile);
    } catch {}
    await emit({
      ts: now(),
      type: "info",
      text: `✓ Committed: "${commitMsg}"`,
    });

    const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`.replace(
      "https://",
      `https://${giteaUsername}:${giteaToken}@`,
    );
    execSync(`git push "${authedUrl}" HEAD:main`, gitOpts);
    await emit({ ts: now(), type: "info", text: "✓ Pushed to Gitea." });

    // Optional Coolify deploy
    let deployed = false;
    if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) {
      try {
        const deployRes = await fetch(
          `${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`,
          {
            method: "POST",
            headers: { Authorization: `Bearer ${opts.coolifyApiToken}` },
          },
        );
        deployed = deployRes.ok;
        if (deployed)
          await emit({
            ts: now(),
            type: "info",
            text: "✓ Deployment triggered.",
          });
      } catch {
        /* best-effort */
      }
    }

    await patchSession(opts, {
      status: "approved",
      outputLine: {
        ts: now(),
        type: "done",
        text: `✓ Auto-committed & ${deployed ? "deployed" : "pushed"}. No approval needed.`,
      },
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    await emit({
      ts: now(),
      type: "error",
      text: `Auto-commit failed: ${msg}`,
    });
    // Fall back to done so user can manually approve
    await patchSession(opts, { status: "done" });
  }
}

// ── Main streaming execution loop ─────────────────────────────────────────────

interface TaskItem {
  text: string;
  filePath: string;
  lineIndex: number;
  isChecked: boolean;
  fileName: string;
}

function parseTaskItems(repoRoot: string): TaskItem[] {
  const fs = require("fs") as typeof import("fs");
  const path = require("path") as typeof import("path");
  const tasksDir = path.join(repoRoot, ".vibncode", "tasks");
  if (!fs.existsSync(tasksDir)) return [];

  const items: TaskItem[] = [];
  try {
    const files = fs
      .readdirSync(tasksDir)
      .filter((f: string) => f.endsWith(".md"));
    files.sort();

    for (const file of files) {
      const filePath = path.join(tasksDir, file);
      const content = fs.readFileSync(filePath, "utf8");
      const lines = content.split("\n");
      lines.forEach((line: string, lineIndex: number) => {
        const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
        if (match && match[2] !== undefined && match[3] !== undefined) {
          items.push({
            text: match[3].trim(),
            filePath,
            lineIndex,
            isChecked: match[2].toLowerCase() === "x",
            fileName: file,
          });
        }
      });
    }
  } catch (err) {
    console.error("[Orchestrator] Error parsing task items:", err);
  }
  return items;
}

function toggleTaskOnDisk(task: TaskItem): void {
  const fs = require("fs") as typeof import("fs");
  const content = fs.readFileSync(task.filePath, "utf8");
  const lines = content.split("\n");
  const line = lines[task.lineIndex];
  if (line) {
    const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
    if (match && match[1] !== undefined && match[3] !== undefined) {
      lines[task.lineIndex] = `${match[1]}- [x] ${match[3]}`;
      fs.writeFileSync(task.filePath, lines.join("\n"), "utf8");
    }
  }
}

async function generateBacklogFromPrompt(
  taskPrompt: string,
  repoRoot: string,
): Promise<void> {
  const fs = require("fs") as typeof import("fs");
  const path = require("path") as typeof import("path");
  const tasksDir = path.join(repoRoot, ".vibncode", "tasks");
  fs.mkdirSync(tasksDir, { recursive: true });

  const prompt = `You are an elite Software Engineering Orchestrator.
Your goal is to break down the user's high-level objective into a highly detailed, sequential checklist of concrete, atomic, self-contained implementation tasks.

High-Level Objective:
"${taskPrompt}"

Please output a standard Markdown file containing:
1. A brief 1-sentence overview.
2. A list of tasks, where each task MUST be formatted as a standard Markdown checkbox starting with "- [ ] ":
- [ ] Implement database schema changes for ...
- [ ] Add endpoint handler for ...
- [ ] Write tests ...

Be extremely thorough and break the objective down into small, digestible units of work (e.g. 5-15 tasks).
Do NOT include any extra conversational text or explanations. Just output the clean markdown.`;

  const resp = await callVibnChat({
    systemPrompt:
      "You are a precise technical orchestrator who only outputs markdown checklist files.",
    messages: [{ role: "user", content: prompt }],
    temperature: 0.1,
  });

  const content = resp.text || `# Delegated Backlog\n\n- [ ] ${taskPrompt}`;
  const backlogPath = path.join(tasksDir, "00-delegated-backlog.md");
  fs.writeFileSync(backlogPath, content, "utf8");
}

function commitTaskProgress(task: TaskItem, repoRoot: string) {
  const { execSync } = require("child_process");
  try {
    console.log(`[Orchestrator] Committing task progress: ${task.text}`);
    execSync("git add -A", { cwd: repoRoot, stdio: "pipe" });
    const msg = `feat(tasks): [Completed] ${task.text}`;
    execSync(`git commit -m "${msg.replace(/"/g, '\\"')}"`, {
      cwd: repoRoot,
      stdio: "pipe",
    });
  } catch (err) {
    // If nothing to commit, that's fine
  }
}

async function runSingleSubTask(
  task: TaskItem,
  config: AgentConfig,
  ctx: ToolContext,
  opts: SessionRunOptions,
  emit: (line: OutputLine) => Promise<void>,
): Promise<boolean> {
  const path = require("path") as typeof import("path");
  const fs = require("fs") as typeof import("fs");
  const basePrompt = resolvePrompt(config.promptId);

  const scopedPrompt = `${basePrompt}

## ACTIVE SUBTASK OBJECTIVE
You are working on a single task in your task queue:
TASK: "${task.text}"
File: "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" (line ${task.lineIndex + 1})

## CRITICAL EXECUTION CONSTRAINTS
1. 🎯 STAY HIGHLY FOCUSED: Your only objective is to implement this specific task. Do NOT wander, do NOT explore other unrelated parts of the codebase, and do NOT attempt unrelated tasks.
2. 🚫 NO EXPLORATION COMMANDS: DO NOT execute generic orientation/search commands like 'ls', 'find', 'pwd', 'grep', 'git diff', 'git status'. You already know the repository structure. Go straight to editing or reading the targeted files.
3. 🛠️ TOGGLE CHECKBOX: Once your implementation is done, you MUST read and rewrite "${path.relative(opts.repoRoot ?? ctx.workspaceRoot, task.filePath)}" at line ${task.lineIndex + 1} to change "- [ ]" to "- [x]".
4. 🔴 NO COMMITS: Do NOT run 'git commit' or 'git push'. The platform handles committing automatically after you finish.
5. 🟢 COMPLETED SIGNAL: When you are finished, verify the build compiles clean using the Ralph Loop checks. If successful, stop executing tools and end your response.
`;

  const userPrompt = `Please implement the following task: "${task.text}" and then check it off in the task list.`;
  const history: ChatMessage[] = [{ role: "user", content: userPrompt }];

  let subTurn = 0;
  const SUB_MAX_TURNS = 40;
  let toolCallsSinceText = 0;
  let roundsSinceText = 0;
  const toolFingerprints: string[] = [];
  let ralphIteration = 0;

  function fingerprintToolCall(tc: any) {
    const name = tc.name;
    const args = tc.args ?? {};

    if (name === "shell_exec") {
      const cmd = String(args.command ?? "").trim();
      const firstWord = cmd.split(/\s+/)[0] ?? "shell";
      return `shell_exec:${firstWord}`;
    }

    // Determine target based on most common descriptive parameter keys
    const target =
      args.path ??
      args.pattern ??
      args.command ??
      args.commandId ??
      args.appUuid ??
      args.uuid ??
      "";
    if (target) {
      return `${name}:${target}`;
    }

    // Filter out common metadata like projectId, and use first real argument
    const keys = Object.keys(args).filter((k) => k !== "projectId");
    if (keys.length > 0) {
      return `${name}:${args[keys[0]]}`;
    }

    return `${name}:default`;
  }

  while (subTurn < SUB_MAX_TURNS) {
    if (opts.isStopped()) {
      await emit({ ts: now(), type: "info", text: "Stopped by user." });
      return false;
    }

    subTurn++;

    const isSilent = roundsSinceText >= 8 || toolCallsSinceText >= 12;
    const extraSystem = isSilent
      ? "\n\n[STATUS NUDGE] Focus on completing the current task. Do not make any more tool calls without a short sentence explaining what you are working on."
      : "";

    let resp: any;
    try {
      resp = await callVibnChat({
        systemPrompt: scopedPrompt + extraSystem,
        messages: history as any[],
        tools: config.tools,
        temperature: 0.1,
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      await emit({
        ts: now(),
        type: "error",
        text: `LLM sub-session error: ${msg}`,
      });
      return false;
    }

    if (resp.error) {
      await emit({
        ts: now(),
        type: "error",
        text: `LLM sub-session error: ${resp.error}`,
      });
      return false;
    }

    if (resp.text) {
      await emit({ ts: now(), type: "info", text: resp.text });
      roundsSinceText = 0;
      toolCallsSinceText = 0;
    } else if (resp.toolCalls.length) {
      roundsSinceText++;
      toolCallsSinceText += resp.toolCalls.length;
    }

    if (!resp.toolCalls.length) {
      if (opts.repoRoot && ralphIteration < 3) {
        await emit({
          ts: now(),
          type: "info",
          text: "🔍 [Ralph Loop] Verifying build for this task...",
        });

        const verification = runBuildVerification(opts.repoRoot, opts.appPath);
        if (!verification.success) {
          ralphIteration++;
          await emit({
            ts: now(),
            type: "error",
            text: `❌ [Ralph Loop] Build failed (iteration ${ralphIteration}/3) for this task.`,
          });

          history.push({
            role: "user",
            content: `Your previous edits completed, but the project's build check failed with compilation errors.

=========================================
🚨 SURGICAL HEALING PROTOCOL ACTIVE 🚨
=========================================
The project's compilation/build has failed. You are currently in an autonomous, auto-correcting healing loop and must fix this compilation error immediately.

To prevent cognitive loop spirals and command limits, you MUST follow this strict, non-negotiable troubleshooting protocol:

1. 🚫 STRICTLY BLOCK EXPLORATION: DO NOT execute general directory exploration or orientation commands such as 'ls', 'find', 'pwd', 'grep', 'git status', 'git diff', or other search commands. You do not need to look around.
2. 🎯 SURGICAL TARGETING: Scan the compiler error logs below to locate the EXACT filename, line number, and column where the compilation failed.
3. 🛠️ IMMEDIATE CORRECTION: Read that file immediately using your specific file-reading tool (using precise start/end lines if it is large) and apply a targeted, surgical edit to correct the exact syntax or type error. Do not write a placeholder or partial fix.

Here are the precise compilation errors from the compiler:
\`\`\`text
${verification.error}
\`\`\`

Implement the exact fix directly in the code now.`,
          });
          continue;
        } else {
          await emit({
            ts: now(),
            type: "info",
            text: "🟢 [Ralph Loop] Build passed successfully! 0 errors.",
          });
        }
      }

      let diskChecked = false;
      try {
        const fileContent = fs.readFileSync(task.filePath, "utf8");
        const lines = fileContent.split("\n");
        const line = lines[task.lineIndex];
        if (line) {
          const match = line.match(/^(\s*)-\s*\[([ xX])\]\s+(.+)$/);
          if (match && match[2].toLowerCase() === "x") {
            diskChecked = true;
          }
        }
      } catch {}

      if (!diskChecked) {
        await emit({
          ts: now(),
          type: "info",
          text: `✍️ [Orchestrator] Task implementation completed. Automatically checking off task on disk.`,
        });
        toggleTaskOnDisk(task);
      }

      return true;
    }

    for (const tc of resp.toolCalls) {
      toolFingerprints.push(fingerprintToolCall(tc));
    }
    const window = toolFingerprints.slice(-12);
    const counts = new Map<string, number>();
    for (const fp of window) counts.set(fp, (counts.get(fp) ?? 0) + 1);

    let maxRepeats = 0;
    let repeatedCmd = "";
    for (const [fp, n] of counts.entries()) {
      if (n > maxRepeats) {
        maxRepeats = n;
        repeatedCmd = fp;
      }
    }

    if (maxRepeats >= 6) {
      await emit({
        ts: now(),
        type: "error",
        text: `Loop detected in subtask execution (repeated "${repeatedCmd}" ${maxRepeats}x in last 12 calls), breaking loop.`,
      });
      return false;
    }

    history.push({
      role: "assistant",
      content: resp.text,
      toolCalls: resp.toolCalls,
    });

    for (const tc of resp.toolCalls) {
      if (opts.isStopped()) return false;

      await emit({
        ts: now(),
        type: "step",
        text: `Running ${tc.name}...`,
      });

      let result: any;
      try {
        result = await executeTool(tc.name, tc.args, ctx);
      } catch (err) {
        result = { error: err instanceof Error ? err.message : String(err) };
      }

      const resultStr =
        typeof result === "string" ? result : JSON.stringify(result, null, 2);
      history.push({
        role: "tool",
        content: resultStr,
        toolCallId: tc.id,
        toolName: tc.name,
      });
    }
  }

  await emit({
    ts: now(),
    type: "error",
    text: `Subtask exceeded maximum turns limit of ${SUB_MAX_TURNS}.`,
  });
  return false;
}

export async function runSessionAgent(
  config: AgentConfig,
  task: string,
  ctx: ToolContext,
  opts: SessionRunOptions,
): Promise<void> {
  const emit = async (line: OutputLine) => {
    console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
    await patchSession(opts, { outputLine: line });
  };

  await emit({
    ts: now(),
    type: "info",
    text: `Agent started offline delegation orchestrator in ${opts.appPath}`,
  });

  const repoRoot = opts.repoRoot ?? ctx.workspaceRoot;

  let tasks = parseTaskItems(repoRoot);
  if (tasks.length === 0) {
    await emit({
      ts: now(),
      type: "info",
      text: "🤖 [Orchestrator] No active tasks backlog found on disk. Analyzing prompt to plan atomic execution backlog...",
    });
    try {
      await generateBacklogFromPrompt(task, repoRoot);
      tasks = parseTaskItems(repoRoot);
    } catch (err: any) {
      await emit({
        ts: now(),
        type: "error",
        text: `❌ [Orchestrator] Failed to generate backlog: ${err.message || String(err)}`,
      });
      await patchSession(opts, {
        status: "failed",
        error: "Backlog generation failed",
      });
      return;
    }
  }

  const openTasks = tasks.filter((t) => !t.isChecked);
  if (openTasks.length === 0) {
    await emit({
      ts: now(),
      type: "info",
      text: "🟢 [Orchestrator] All tasks in the queue are already completed!",
    });
    await patchSession(opts, { status: "completed" });
    return;
  }

  await emit({
    ts: now(),
    type: "info",
    text: `🤖 [Orchestrator] Found ${openTasks.length} open tasks. Executing task-by-task Meta-Loop...`,
  });

  for (let i = 0; i < openTasks.length; i++) {
    const currentTask = openTasks[i];
    await emit({
      ts: now(),
      type: "info",
      text: `🚀 [Orchestrator] Task ${i + 1}/${openTasks.length}: "${currentTask.text}"`,
    });

    const success = await runSingleSubTask(
      currentTask,
      config,
      ctx,
      opts,
      emit,
    );
    if (!success) {
      await emit({
        ts: now(),
        type: "error",
        text: `❌ [Orchestrator] Bailed out! Task execution failed on: "${currentTask.text}". Rolling back modifications for this task to keep the repository green...`,
      });

      try {
        const { execSync } = require("child_process");
        execSync("git checkout -- . && git clean -fd", {
          cwd: repoRoot,
          stdio: "pipe",
        });
      } catch (rollbackErr: any) {
        console.error("Rollback failed:", rollbackErr.message || rollbackErr);
      }

      await patchSession(opts, {
        status: "failed",
        error: `Delegation loop halted at task: "${currentTask.text}"`,
      });
      return;
    }

    commitTaskProgress(currentTask, repoRoot);
  }

  await emit({
    ts: now(),
    type: "info",
    text: `🎉 [Orchestrator] All delegated tasks completed successfully with green compilation builds!`,
  });

  if (opts.autoApprove) {
    await autoCommitAndDeploy(opts, task, emit);
  } else {
    await patchSession(opts, { status: "completed" });
  }
}
