import express, { Request, Response, NextFunction } from "express";
import cors from "cors";
import * as fs from "fs";
import * as path from "path";
import * as crypto from "crypto";
import { execSync } from "child_process";
import { runSessionAgent } from "./agent-session-runner";
import { AGENTS } from "./agents";
import { ToolContext } from "./tools";

const app = express();
app.use(cors());

const startTime = new Date();

// Raw body capture for webhook HMAC — must come before express.json()
app.use("/webhook/gitea", express.raw({ type: "*/*" }));

app.use(express.json());

const PORT = process.env.PORT || 3333;

// ---------------------------------------------------------------------------
// Build ToolContext from environment variables
// ---------------------------------------------------------------------------

function ensureWorkspace(repo?: string, sessionId?: string): string {
  const base = process.env.WORKSPACE_BASE || "/workspaces";
  if (!repo) {
    const dir = path.join(base, "default");
    fs.mkdirSync(dir, { recursive: true });
    return dir;
  }
  const mainRepoDir = path.join(base, repo.replace("/", "_"));
  const gitea = {
    apiUrl: process.env.GITEA_API_URL || "",
    apiToken: process.env.GITEA_API_TOKEN || "",
    username: process.env.GITEA_USERNAME || "",
  };

  // 1. Ensure main repo clone exists
  if (!fs.existsSync(path.join(mainRepoDir, ".git"))) {
    fs.mkdirSync(mainRepoDir, { recursive: true });
    const authedUrl = `${gitea.apiUrl}/${repo}.git`.replace(
      "https://",
      `https://${gitea.username}:${gitea.apiToken}@`,
    );
    try {
      execSync(`git clone "${authedUrl}" "${mainRepoDir}"`, { stdio: "pipe" });
    } catch {
      // Repo may not exist yet — just init
      execSync(`git init`, { cwd: mainRepoDir, stdio: "pipe" });
      execSync(`git remote add origin "${authedUrl}"`, {
        cwd: mainRepoDir,
        stdio: "pipe",
      });
    }
  }

  // 2. If no sessionId, fall back to main repo clone directly
  if (!sessionId) {
    return mainRepoDir;
  }

  // 3. Isolated Worktree Directory per task session
  const taskWorktreePath = path.join(base, "tasks", sessionId);
  fs.mkdirSync(path.join(base, "tasks"), { recursive: true });

  // 4. Create isolated worktree if not yet active
  if (!fs.existsSync(path.join(taskWorktreePath, ".git"))) {
    // Clean up any stale directory from previous failed runs before adding worktree
    if (fs.existsSync(taskWorktreePath)) {
      try {
        fs.rmSync(taskWorktreePath, { recursive: true, force: true });
      } catch {}
    }

    try {
      console.log(
        `[worktree] Adding isolated git worktree for session ${sessionId} at ${taskWorktreePath}...`,
      );

      // Check if the branch task-sessionId already exists in the main repository
      let branchExists = false;
      try {
        const branches = execSync(`git branch --list "task-${sessionId}"`, {
          cwd: mainRepoDir,
        }).toString();
        branchExists = branches.trim().length > 0;
      } catch {
        branchExists = false;
      }

      if (branchExists) {
        // Checkout the existing branch into the new worktree path
        execSync(
          `git worktree add -f "${taskWorktreePath}" "task-${sessionId}"`,
          { cwd: mainRepoDir, stdio: "pipe" },
        );
      } else {
        // Create and checkout a new isolated branch
        execSync(
          `git worktree add -f -b "task-${sessionId}" "${taskWorktreePath}"`,
          { cwd: mainRepoDir, stdio: "pipe" },
        );
      }
    } catch (e: any) {
      console.error(
        "[worktree] Failed to add git worktree, falling back to main clone:",
        e.message || String(e),
      );
      return mainRepoDir;
    }
  }

  // 5. Sync active workspace edits from mainRepoDir (containing Monaco edits) to taskWorktreePath
  if (taskWorktreePath !== mainRepoDir) {
    try {
      console.log(
        `[worktree] Syncing active workspace edits from ${mainRepoDir} to ${taskWorktreePath}...`,
      );
      // Use rsync to copy active files while preserving structure and deleting files deleted in mainRepoDir
      // Exclude node_modules, .git, .next, .vibncode/settings.json, etc.
      execSync(
        `rsync -ar --delete --exclude="node_modules" --exclude=".git" --exclude=".next" --exclude=".vibncode/settings.json" "${mainRepoDir}/" "${taskWorktreePath}/"`,
        { stdio: "pipe" },
      );
    } catch (syncErr: any) {
      console.warn(
        "[worktree] rsync failed, falling back to cp:",
        syncErr.message || syncErr,
      );
    }
  }

  return taskWorktreePath;
}

function buildContext(repo?: string, sessionId?: string): ToolContext {
  const workspaceRoot = ensureWorkspace(repo, sessionId);

  return {
    workspaceRoot,
    gitea: {
      apiUrl: process.env.GITEA_API_URL || "",
      apiToken: process.env.GITEA_API_TOKEN || "",
      username: process.env.GITEA_USERNAME || "",
    },
    coolify: {
      apiUrl: process.env.COOLIFY_API_URL || "",
      apiToken: process.env.COOLIFY_API_TOKEN || "",
    },
    mcpToken: "",
    vibnApiUrl: process.env.VIBN_API_URL ?? "https://vibnai.com",
    memoryUpdates: [],
  };
}

function cleanupWorkspace(repo: string, sessionId: string) {
  const base = process.env.WORKSPACE_BASE || "/workspaces";
  const mainRepoDir = path.join(base, repo.replace("/", "_"));
  const taskWorktreePath = path.join(base, "tasks", sessionId);

  if (fs.existsSync(taskWorktreePath)) {
    try {
      console.log(
        `[worktree] Pruning and removing git worktree for session ${sessionId}...`,
      );
      // 1. Tell git to remove the worktree references
      execSync(`git worktree remove --force "${taskWorktreePath}"`, {
        cwd: mainRepoDir,
        stdio: "pipe",
      });
      // 2. Delete the temporary branch from the main repository index
      execSync(`git branch -D "task-${sessionId}"`, {
        cwd: mainRepoDir,
        stdio: "pipe",
      });
      // 3. Force clean directory
      if (fs.existsSync(taskWorktreePath)) {
        fs.rmSync(taskWorktreePath, { recursive: true, force: true });
      }
    } catch (e: any) {
      console.warn(
        `[worktree] Non-fatal cleanup error for session ${sessionId}:`,
        e.message || String(e),
      );
    }
  }
}

// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------

// Health check
app.get("/health", (_req: Request, res: Response) => {
  res.json({ status: "ok", timestamp: new Date().toISOString() });
});

// ---------------------------------------------------------------------------
// GitHub mirror — clone a public GitHub repo and push to Gitea as-is
// ---------------------------------------------------------------------------

app.post("/api/mirror", async (req: Request, res: Response) => {
  const { github_url, gitea_repo, project_name, github_token } = req.body as {
    github_url?: string;
    gitea_repo?: string; // e.g. "mark/opsos"
    project_name?: string;
    github_token?: string; // PAT for private repos
  };

  if (!github_url || !gitea_repo) {
    res
      .status(400)
      .json({ error: '"github_url" and "gitea_repo" are required' });
    return;
  }

  const { execSync } = await import("child_process");
  const fs = await import("fs");
  const path = await import("path");
  const os = await import("os");

  const mirrorId = `mirror_${Date.now()}`;
  const tmpDir = path.join(os.tmpdir(), mirrorId);

  const gitea = {
    apiUrl: process.env.GITEA_API_URL || "",
    apiToken: process.env.GITEA_API_TOKEN || "",
    username: process.env.GITEA_USERNAME || "",
  };

  try {
    // Build authenticated Gitea push URL
    // GITEA_API_URL is like https://git.vibnai.com — strip /api/v1 if present
    const giteaBase = gitea.apiUrl.replace(/\/api\/v1\/?$/, "");
    const authedPushUrl = `${giteaBase}/${gitea_repo}.git`.replace(
      "https://",
      `https://${gitea.username}:${gitea.apiToken}@`,
    );

    console.log(`[mirror] Cloning ${github_url} → ${tmpDir}`);
    fs.mkdirSync(tmpDir, { recursive: true });

    // Build authenticated clone URL for private repos
    let cloneUrl = github_url;
    if (github_token) {
      cloneUrl = github_url.replace("https://", `https://${github_token}@`);
    }

    // Mirror-clone the GitHub repo (preserves all branches + tags)
    execSync(`git clone --mirror "${cloneUrl}" "${tmpDir}/.git"`, {
      stdio: "pipe",
      timeout: 120_000,
    });
    execSync(`git config --bool core.bare false`, {
      cwd: tmpDir,
      stdio: "pipe",
    });
    execSync(`git checkout`, { cwd: tmpDir, stdio: "pipe" });

    // Point origin at Gitea and push all refs
    execSync(`git remote set-url origin "${authedPushUrl}"`, {
      cwd: tmpDir,
      stdio: "pipe",
    });
    execSync(`git push --mirror origin`, {
      cwd: tmpDir,
      stdio: "pipe",
      timeout: 120_000,
    });

    console.log(`[mirror] Pushed ${gitea_repo} successfully`);
    res.json({ success: true, gitea_repo, github_url });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    console.error(`[mirror] Failed:`, msg);
    res.status(500).json({ error: "Mirror failed", details: msg });
  } finally {
    // Clean up temp dir
    try {
      const { execSync: rm } = await import("child_process");
      rm(`rm -rf "${tmpDir}"`, { stdio: "pipe" });
    } catch {
      /* best effort */
    }
  }
});

// List available agents
app.get("/api/agents", (_req: Request, res: Response) => {
  const agents = Object.values(AGENTS).map((a) => ({
    name: a.name,
    description: a.description,
    tools: a.tools.map((t) => t.name),
  }));
  res.json(agents);
});

const activeSessions = new Map<string, { stopped: boolean }>();

app.post("/agent/execute", async (req: Request, res: Response) => {
  const {
    sessionId,
    projectId,
    appName,
    appPath: rawAppPath,
    giteaRepo,
    task,
    continueTask,
    autoApprove,
    coolifyAppUuid,
    mcpToken,
  } = req.body as {
    sessionId?: string;
    projectId?: string;
    appName?: string;
    appPath?: string;
    giteaRepo?: string;
    task?: string;
    continueTask?: string;
    autoApprove?: boolean;
    coolifyAppUuid?: string;
    mcpToken?: string;
  };

  const appPath =
    rawAppPath === undefined || rawAppPath === null || rawAppPath === ""
      ? "."
      : rawAppPath;

  if (!sessionId || !projectId || !appPath || !task) {
    res
      .status(400)
      .json({ error: "sessionId, projectId, appPath and task are required" });
    return;
  }

  const vibnApiUrl = process.env.VIBN_API_URL ?? "https://vibnai.com";

  const patchHeaders = {
    "Content-Type": "application/json",
    ...(process.env.AGENT_RUNNER_SECRET
      ? { "x-agent-runner-secret": process.env.AGENT_RUNNER_SECRET }
      : {}),
  };

  // Register session as active
  const sessionState = { stopped: false };
  activeSessions.set(sessionId, sessionState);

  // Respond immediately — execution is async
  res.status(202).json({ sessionId, status: "running" });

  // Build workspace context — clone/update the Gitea repo if provided
  let ctx: ReturnType<typeof buildContext>;
  try {
    ctx = buildContext(giteaRepo, sessionId);
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    console.error("[agent/execute] buildContext failed:", msg);
    // Notify VIBN DB of failure
    fetch(
      `${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`,
      {
        method: "PATCH",
        headers: patchHeaders,
        body: JSON.stringify({ status: "failed", error: msg }),
      },
    ).catch(() => {});
    activeSessions.delete(sessionId);
    return;
  }

  // Capture repo root before scoping to appPath — needed for git commit in auto-approve
  const repoRoot = ctx.workspaceRoot;

  // Wire the ToolContext so its tools can call back into the VIBN frontend MCP
  // with the right URL and auth. buildContext() defaults these to safe values,
  // but the authoritative ones come from env (VIBN_API_URL) and the frontend
  // (mcpToken passed in the /agent/execute body). Without this, tools fetch
  // http://localhost:3000 with no token and fail with "fetch failed".
  ctx.vibnApiUrl = vibnApiUrl;
  ctx.mcpToken = mcpToken ?? ctx.mcpToken;
  ctx.projectId = projectId;

  // Scope workspace to the app subdirectory so the agent works there naturally
  if (appPath) {
    const path = require("path") as typeof import("path");
    ctx.workspaceRoot = path.join(ctx.workspaceRoot, appPath);
    const fs = require("fs") as typeof import("fs");
    fs.mkdirSync(ctx.workspaceRoot, { recursive: true });
  }

  const agentConfig = AGENTS["Coder"];
  if (!agentConfig) {
    fetch(
      `${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`,
      {
        method: "PATCH",
        headers: patchHeaders,
        body: JSON.stringify({
          status: "failed",
          error: "Coder agent not registered",
        }),
      },
    ).catch(() => {});
    activeSessions.delete(sessionId);
    return;
  }

  // If continuing a previous task, combine into a single prompt so the agent
  // understands what was already attempted.
  const effectiveTask = continueTask
    ? `Original task: ${task}\n\nFollow-up instruction: ${continueTask}`
    : task!;

  // Run the streaming agent loop (fire and forget)
  runSessionAgent(agentConfig, effectiveTask, ctx, {
    sessionId,
    projectId,
    vibnApiUrl,
    appPath,
    repoRoot,
    isStopped: () => sessionState.stopped,
    autoApprove: autoApprove ?? true,
    giteaRepo,
    coolifyAppUuid,
    coolifyApiUrl: process.env.COOLIFY_API_URL,
    coolifyApiToken: process.env.COOLIFY_API_TOKEN,
  })
    .catch((err) => {
      const msg = err instanceof Error ? err.message : String(err);
      console.error(`[agent/execute] session ${sessionId} crashed:`, msg);
      fetch(
        `${vibnApiUrl}/api/projects/${projectId}/agent/sessions/${sessionId}`,
        {
          method: "PATCH",
          headers: patchHeaders,
          body: JSON.stringify({ status: "failed", error: msg }),
        },
      ).catch(() => {});
    })
    .finally(() => {
      activeSessions.delete(sessionId);
      if (giteaRepo && sessionId) {
        cleanupWorkspace(giteaRepo, sessionId);
      }
    });
});

app.post("/agent/stop", (req: Request, res: Response) => {
  const { sessionId } = req.body as { sessionId?: string };
  if (!sessionId) {
    res.status(400).json({ error: "sessionId required" });
    return;
  }
  const session = activeSessions.get(sessionId);
  if (session) {
    session.stopped = true;
    res.json({ status: "stopped" });
  } else {
    res.status(404).json({ error: "session not found or not running" });
  }
});

app.listen(PORT, () => {
  console.log(`AgentRunner listening on port ${PORT}`);
  console.log(`Agents available: ${Object.keys(AGENTS).join(", ")}`);
  if (!process.env.GOOGLE_API_KEY) {
    console.warn("WARNING: GOOGLE_API_KEY is not set — agents will fail");
  }
});
