import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { Hono } from "hono";
import type { Context } from "hono";
import type { Logger } from "pino";
import { z } from "zod";
import type { JiraClientImpl } from "@/jira/client.js";
import type { CursorCloudClient } from "@/agent/cursor-cloud-client.js";
import type { FixPipeline } from "@/pipeline/fix-pipeline.js";
import type { RepoRegistry } from "@/repo/registry.js";
import type { RepoSelector } from "@/repo/selector.js";
import type { JiraPoller } from "@/poller/jira-poller.js";
import type { JiraTicketReviewPoller } from "@/poller/jira-ticket-review-poller.js";
import type { PrCommentPoller } from "@/poller/pr-comment-poller.js";
import type { PrMentionPoller } from "@/poller/pr-mention-poller.js";
import type { SlackMentionPoller } from "@/poller/slack-mention-poller.js";
import type { DailyReportScheduler } from "@/scheduler/daily-report.js";
import { collectDailyReport } from "@/scheduler/daily-report.js";
import { GitHubClientImpl } from "@/github/client.js";
import { RepoManagerImpl, configureGitAuth } from "@/repo/manager.js";
import { detectPackageManager } from "@/utils/package-manager.js";
import type { IJobQueue, JiraIssue } from "@/types/index.js";
import { DatadogApiError, type DatadogLogsClient } from "@/datadog/client.js";
import { formatDatadogLogLines } from "@/datadog/format.js";

const execFileAsync = promisify(execFile);
import type { SlackNotifier } from "@/slack/client.js";
import { join } from "node:path";
import { existsSync, readFileSync, readdirSync, rmSync } from "node:fs";

const createSubtaskBodySchema = z.object({
  parentKey: z.string().min(1, "parentKey is required"),
  summary: z.string().min(1, "summary is required"),
  assigneeEmail: z.string().email("assigneeEmail must be a valid email"),
});

const createPrBodySchema = z.object({
  branch: z.string().min(1, "branch is required"),
  title: z.string().min(1, "title is required"),
  body: z.string().optional().default(""),
  base: z.string().optional().default("develop"),
});

const MAX_DATADOG_LOG_RANGE_MS = 24 * 60 * 60 * 1000;

const slackUsergroupSchema = z.object({
  id: z.string(),
  handle: z.string().optional(),
  name: z.string().optional(),
  description: z.string().optional(),
});

const slackUsergroupsListEnvelopeSchema = z.object({
  ok: z.boolean(),
  error: z.string().optional(),
  /** Comma-separated OAuth scopes Slack says are missing (e.g. for `missing_scope`). */
  needed: z.string().optional(),
  usergroups: z.array(z.record(z.unknown())).optional(),
});

const datadogLogsQuerySchema = z
  .object({
    query: z.string().min(1).optional().default("service:hu-agent"),
    window: z.enum(["15m", "24h"]).optional().default("15m"),
    from: z.string().optional(),
    to: z.string().optional(),
    /** Max log lines to return (paginated server-side). Omitted → 50 for 15m, 10_000 for 24h. */
    limit: z.coerce.number().int().min(1).max(50_000).optional(),
    /**
     * When true, drops log lines that contain cursor agent thinking blocks
     * (`thinkingMessage` JSON key). These are usually large and noisy.
     */
    hideThinking: z
      .string()
      .optional()
      .transform((v) => v === "true"),
  })
  .superRefine((data, ctx) => {
    const hasFrom = data.from != null && data.from.length > 0;
    const hasTo = data.to != null && data.to.length > 0;
    if (hasFrom !== hasTo) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "from and to must both be set or both omitted",
      });
    }
  });

function datadogWindowToMs(w: "15m" | "24h"): number {
  return w === "15m" ? 15 * 60 * 1000 : 24 * 60 * 60 * 1000;
}

// ── Setup Task Store (in-memory) ──────────────────────────────────────────────

interface SetupStep {
  step: string;
  ok: boolean;
  durationMs: number;
  output?: string;
}

interface SetupTask {
  id: string;
  repo: string;
  branch: string;
  status: "running" | "completed" | "failed";
  startedAt: string;
  finishedAt?: string;
  steps: SetupStep[];
  moduleVerification?: Array<{ module: string; exists: boolean; resolvedPath?: string }>;
  biomeConfig?: { ok: boolean; output: string } | null;
  success?: boolean;
}

const setupTasks = new Map<string, SetupTask>();

/** Remove tasks older than 1 hour to avoid unbounded memory growth. */
function pruneOldTasks(): void {
  const oneHourAgo = Date.now() - 60 * 60 * 1000;
  for (const [id, task] of setupTasks) {
    if (new Date(task.startedAt).getTime() < oneHourAgo) {
      setupTasks.delete(id);
    }
  }
}

interface SetupPipelineContext {
  localPath: string;
  branch: string;
  clean: boolean;
  repo: {
    name: string;
    url: string;
    installCmd?: string[];
    checkScripts?: string[];
    defaultBranch?: string;
  };
  manager: RepoManagerImpl;
  repoLogger: Logger;
  deps: DebugDeps;
}

async function runSetupPipeline(task: SetupTask, ctx: SetupPipelineContext): Promise<void> {
  const { localPath, branch, clean, repo, manager, repoLogger, deps } = ctx;

  const runStep = async (name: string, fn: () => Promise<string | void>): Promise<boolean> => {
    const t0 = Date.now();
    try {
      const out = await fn();
      task.steps.push({
        step: name,
        ok: true,
        durationMs: Date.now() - t0,
        output: out ?? undefined,
      });
      return true;
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      task.steps.push({ step: name, ok: false, durationMs: Date.now() - t0, output: msg });
      return false;
    }
  };

  // 1. Git auth
  await runStep("git_auth", async () => {
    await configureGitAuth(deps.getGitHubToken, repoLogger);
  });

  // 2. Clone / fetch
  await runStep("clone", async () => {
    await manager.ensureCloned();
  });

  // 3. Checkout base branch
  await runStep("checkout", async () => {
    await manager.checkoutBase(branch);
    return `checked out ${branch}`;
  });

  // 4. Optional clean
  if (clean) {
    await runStep("clean_node_modules", async () => {
      const nmPath = join(localPath, "node_modules");
      if (existsSync(nmPath)) {
        rmSync(nmPath, { recursive: true, force: true });
        return "removed node_modules";
      }
      return "node_modules not present, skipped";
    });
  }

  // 5. Detect package manager
  const pm = detectPackageManager(localPath, repo.installCmd);
  task.steps.push({
    step: "detect_package_manager",
    ok: true,
    durationMs: 0,
    output: `${pm.cmd} ${pm.installArgs.join(" ")}`,
  });

  // 6. Install dependencies (with ENOTDIR retry)
  await runStep("install_dependencies", async () => {
    if (!existsSync(join(localPath, "package.json"))) {
      return "no package.json found, skipped";
    }

    const runInstall = async () => {
      const proc = Bun.spawn([pm.cmd, ...pm.installArgs], {
        cwd: localPath,
        stdout: "pipe",
        stderr: "pipe",
        env: { ...process.env, npm_config_engine_strict: "false" },
      });
      const [stdout, stderr] = await Promise.all([
        new Response(proc.stdout).text(),
        new Response(proc.stderr).text(),
      ]);
      const exitCode = await proc.exited;
      return { exitCode, output: `${stdout}\n${stderr}`.trim() };
    };

    const first = await runInstall();
    if (first.exitCode === 0) return first.output.slice(-1000);

    if (first.output.includes("ENOTDIR") || first.output.includes("EISDIR")) {
      const nmPath = join(localPath, "node_modules");
      if (existsSync(nmPath)) rmSync(nmPath, { recursive: true, force: true });
      if (pm.cmd === "npm") {
        const cache = Bun.spawn(["npm", "cache", "clean", "--force"], {
          cwd: localPath,
          stdout: "pipe",
          stderr: "pipe",
        });
        await cache.exited;
      }
      const retry = await runInstall();
      if (retry.exitCode === 0) return `(retried after ENOTDIR) ${retry.output.slice(-800)}`;
      throw new Error(`exit code ${retry.exitCode} (after retry)\n${retry.output.slice(-1000)}`);
    }

    throw new Error(`exit code ${first.exitCode}\n${first.output.slice(-1000)}`);
  });

  // 7. Verify critical modules exist in node_modules
  const verifyModules: Array<{ module: string; exists: boolean; resolvedPath?: string }> = [];
  try {
    const pkgPath = join(localPath, "package.json");
    if (existsSync(pkgPath)) {
      const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
        dependencies?: Record<string, string>;
        devDependencies?: Record<string, string>;
      };
      const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };

      const toCheck = Object.entries(allDeps)
        .filter(
          ([name, spec]) =>
            typeof spec === "string" &&
            (spec.startsWith("github:") ||
              spec.startsWith("git+") ||
              spec.includes("github.com") ||
              ["typescript", "biome", "@biomejs/biome"].includes(name) ||
              name.includes("biome-config")),
        )
        .map(([name]) => name);

      if (allDeps.typescript && !toCheck.includes("typescript")) {
        toCheck.push("typescript");
      }

      for (const mod of toCheck) {
        const modPath = join(localPath, "node_modules", mod);
        const modExists = existsSync(modPath);
        verifyModules.push({
          module: mod,
          exists: modExists,
          resolvedPath: modExists ? modPath : undefined,
        });
      }
    }
  } catch {
    /* ignore parse errors */
  }

  // 8. Run `npm ls` for missing modules
  const missingModules = verifyModules.filter((m) => !m.exists);
  if (missingModules.length > 0 && existsSync(join(localPath, "package.json"))) {
    const modNames = missingModules.map((m) => m.module);
    await runStep("npm_ls_missing_modules", async () => {
      const proc = Bun.spawn([pm.cmd, "ls", ...modNames], {
        cwd: localPath,
        stdout: "pipe",
        stderr: "pipe",
        env: { ...process.env, npm_config_engine_strict: "false" },
      });
      const [stdout, stderr] = await Promise.all([
        new Response(proc.stdout).text(),
        new Response(proc.stderr).text(),
      ]);
      await proc.exited;
      return `${stdout}\n${stderr}`.trim().slice(-2000);
    });
  }

  // 9. Check biome config resolution
  let biomeCheck: { ok: boolean; output: string } | undefined;
  const biomeJsonc = join(localPath, "biome.jsonc");
  const biomeJson = join(localPath, "biome.json");
  if (existsSync(biomeJsonc) || existsSync(biomeJson)) {
    const biomeFile = existsSync(biomeJsonc) ? biomeJsonc : biomeJson;
    try {
      const content = readFileSync(biomeFile, "utf-8");
      const extendsMatch = content.match(/"extends"\s*:\s*\["([^"]+)"/);
      if (extendsMatch) {
        const extendsFrom = extendsMatch[1]!;
        const pkgName = extendsFrom.split("/")[0]!;
        const resolvedPath = join(localPath, "node_modules", pkgName);
        const resolved = existsSync(resolvedPath);
        biomeCheck = {
          ok: resolved,
          output: resolved
            ? `biome extends "${extendsFrom}" → resolved at ${resolvedPath}`
            : `biome extends "${extendsFrom}" → MISSING: ${resolvedPath} not found`,
        };
      }
    } catch {
      biomeCheck = { ok: false, output: "failed to parse biome config" };
    }
  }

  // Finalize task
  task.moduleVerification = verifyModules;
  task.biomeConfig = biomeCheck ?? null;
  task.success = task.steps.every((s) => s.ok) && verifyModules.every((m) => m.exists);
  task.status = task.success ? "completed" : "failed";
  task.finishedAt = new Date().toISOString();
}

export interface DebugDeps {
  jiraClient: JiraClientImpl;
  cursorCloudClient: CursorCloudClient;
  pipeline: FixPipeline;
  repoRegistry: RepoRegistry;
  repoSelector: RepoSelector;
  getGitHubToken: () => Promise<string>;
  jiraDomain: string;
  workdirPath: string;
  githubBotUser: string;
  queue: IJobQueue;
  logger: Logger;
  datadogLogsClient: DatadogLogsClient | null;
  slackNotifier: SlackNotifier | undefined;
  /** Bot token for Slack Web API (e.g. `usergroups.list`). */
  slackBotToken: string | undefined;
  /** Resolved daily channel: dedicated or default Slack channel. */
  slackDailyReportChannelId: string | undefined;
  squadScope: string[];
  botEmail: string;
  /** Live poller + scheduler instances; manually triggerable via the `/api/pollers/...` routes. */
  jiraPoller: JiraPoller;
  ticketReviewPoller: JiraTicketReviewPoller;
  prCommentPoller: PrCommentPoller;
  prMentionPoller: PrMentionPoller;
  slackMentionPoller: SlackMentionPoller | undefined;
  dailyReportScheduler: DailyReportScheduler | undefined;
}

export function createDebugRoutes(deps: DebugDeps): Hono {
  const app = new Hono();

  // ── Jira ──────────────────────────────────────────────

  app.get("/api/jira/issue", async (c) => {
    const issueKey = c.req.query("issueKey");
    if (!issueKey) {
      return c.json({ error: "issueKey query param is required" }, 400);
    }
    try {
      const issue = await deps.jiraClient.getIssue(issueKey);
      return c.json(issue);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ issueKey, error: message }, "debug_jira_issue_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/jira/search", async (c) => {
    const maxResults = parseInt(c.req.query("maxResults") ?? "20", 10);

    const rawAssignee = c.req.query("assignee") ?? null;
    const rawReporter = c.req.query("reporter") ?? null;

    const [assignee, reporter] = await Promise.all([
      resolveUser(rawAssignee, deps),
      resolveUser(rawReporter, deps),
    ]);

    const params: SearchParams = {
      project: splitCsv(c.req.query("project")),
      issueType: splitCsv(c.req.query("issueType")),
      excludeTypes: splitCsv(c.req.query("excludeTypes")),
      status: c.req.query("status") ?? null,
      assignee,
      reporter,
      priority: splitCsv(c.req.query("priority")),
      labels: splitCsv(c.req.query("labels")),
      component: splitCsv(c.req.query("component")),
      sprint: c.req.query("sprint") ?? null,
      resolution: c.req.query("resolution") ?? null,
      parent: c.req.query("parent") ?? null,
      text: c.req.query("text") ?? null,
      createdAfter: c.req.query("createdAfter") ?? null,
      createdBefore: c.req.query("createdBefore") ?? null,
      updatedAfter: c.req.query("updatedAfter") ?? null,
      updatedBefore: c.req.query("updatedBefore") ?? null,
      orderBy: c.req.query("orderBy") ?? "created DESC",
    };

    const jql = c.req.query("jql") ?? buildSearchJql(params);

    try {
      const result = await deps.jiraClient.searchIssues(jql, maxResults);
      return c.json({ jql, ...result });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ jql, error: message }, "debug_jira_search_error");
      return c.json({ error: message }, 502);
    }
  });

  const jiraProjectsHandler = async (c: Context) => {
    const startAtRaw = parseInt(c.req.query("startAt") ?? "0", 10);
    const maxResultsRaw = parseInt(c.req.query("maxResults") ?? "50", 10);
    const startAt = Number.isFinite(startAtRaw) && startAtRaw >= 0 ? startAtRaw : 0;
    const maxResults = Number.isFinite(maxResultsRaw) && maxResultsRaw > 0 ? maxResultsRaw : 50;
    try {
      const result = await deps.jiraClient.listProjects({ startAt, maxResults });
      return c.json(result);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "debug_jira_projects_error");
      return c.json({ error: message }, 502);
    }
  };

  /** Jira projects (keys/names); alias "spaces" for discoverability. */
  app.get("/api/jira/projects", jiraProjectsHandler);
  app.get("/api/jira/spaces", jiraProjectsHandler);

  // ── Repos ─────────────────────────────────────────────

  app.get("/api/repos", (c) => {
    const repos = deps.repoRegistry.getAll();
    return c.json({ count: repos.length, repos });
  });

  app.post("/api/repos/match", async (c) => {
    const body = await c.req.json<{ summary?: string; description?: string }>();
    const summary = body.summary ?? "";
    const description = body.description ?? "";

    const syntheticIssue: JiraIssue = {
      id: "0",
      key: "DEBUG-0",
      summary,
      description: description || null,
      issueType: "Bug",
      status: "To Do",
      created: null,
      updated: null,
      acceptanceCriteria: null,
      stacktrace: null,
      flow: null,
      assignee: null,
      parent: null,
      subtasks: [],
      children: [],
      attachments: [],
      comments: [],
      components: [],
    };

    const selected = deps.repoSelector.select(syntheticIssue);
    return c.json({
      selected: selected ? { name: selected.name, url: selected.url } : null,
      input: { summary, description },
    });
  });

  // ── Repo Test Runner ────────────────────────────────────

  app.post("/api/repos/check", async (c) => {
    const body = await c.req.json<{
      repo: string;
      install?: boolean;
      clean?: boolean;
      branch?: string;
    }>();
    const repoName = body.repo;

    if (!repoName) {
      return c.json({ error: "repo is required" }, 400);
    }

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    const localPath = join(deps.workdirPath, repo.name);
    const repoLogger = deps.logger.child({ repo: repo.name, endpoint: "test" });
    const manager = new RepoManagerImpl(repo.url, localPath, repoLogger);

    try {
      await configureGitAuth(deps.getGitHubToken, repoLogger);
      await manager.ensureCloned();
      await manager.checkoutBase(body.branch ?? repo.defaultBranch ?? "develop");

      if (body.clean) {
        const nmPath = join(localPath, "node_modules");
        if (existsSync(nmPath)) {
          repoLogger.info("cleaning_node_modules");
          rmSync(nmPath, { recursive: true, force: true });
        }
      }

      if (body.install !== false) {
        await manager.installDependencies();
      }

      const result = await manager.validateBuild(repo.checkScripts);
      return c.json({
        repo: repoName,
        localPath,
        branch: body.branch ?? repo.defaultBranch ?? "develop",
        cleaned: body.clean ?? false,
        ...result,
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      repoLogger.error({ error: message }, "debug_repo_test_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Repo Setup (async — returns taskId immediately) ───

  app.post("/api/repos/setup", async (c) => {
    const body = await c.req.json<{
      repo: string;
      branch?: string;
      clean?: boolean;
    }>();
    const repoName = body.repo;

    if (!repoName) {
      return c.json({ error: "repo is required" }, 400);
    }

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    pruneOldTasks();

    const taskId = crypto.randomUUID();
    const branch = body.branch ?? repo.defaultBranch ?? "develop";
    const task: SetupTask = {
      id: taskId,
      repo: repoName,
      branch,
      status: "running",
      startedAt: new Date().toISOString(),
      steps: [],
    };
    setupTasks.set(taskId, task);

    const localPath = join(deps.workdirPath, repo.name);
    const repoLogger = deps.logger.child({ repo: repo.name, endpoint: "setup", taskId });
    const manager = new RepoManagerImpl(repo.url, localPath, repoLogger, repo.installCmd);

    // Fire-and-forget: run the setup pipeline in the background
    runSetupPipeline(task, {
      localPath,
      branch,
      clean: body.clean ?? false,
      repo,
      manager,
      repoLogger,
      deps,
    }).catch((err: unknown) => {
      repoLogger.error({ error: String(err) }, "setup_task_unhandled_error");
      task.status = "failed";
      task.finishedAt = new Date().toISOString();
    });

    return c.json({ taskId, status: "running", poll: `/api/repos/setup/${taskId}` }, 202);
  });

  // ── Poll setup task status ───────────────────────────

  app.get("/api/repos/setup/:taskId", (c) => {
    const taskId = c.req.param("taskId");
    const task = setupTasks.get(taskId);
    if (!task) {
      return c.json({ error: "task not found" }, 404);
    }
    return c.json(task);
  });

  // ── List setup tasks ─────────────────────────────────

  app.get("/api/repos/setup", (c) => {
    const repo = c.req.query("repo");
    const tasks = [...setupTasks.values()]
      .filter((t) => !repo || t.repo === repo)
      .sort((a, b) => b.startedAt.localeCompare(a.startedAt));
    return c.json(tasks);
  });

  // ── Repo Checks (lint, tsc, tests) ──────────────────

  app.post("/api/repos/run-checks", async (c) => {
    const body = await c.req.json<{
      repo: string;
      scripts?: string[];
      branch?: string;
      install?: boolean;
    }>();
    const repoName = body.repo;

    if (!repoName) {
      return c.json({ error: "repo is required" }, 400);
    }

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    const localPath = join(deps.workdirPath, repo.name);
    const repoLogger = deps.logger.child({ repo: repo.name, endpoint: "run-checks" });
    const manager = new RepoManagerImpl(repo.url, localPath, repoLogger, repo.installCmd);

    try {
      await configureGitAuth(deps.getGitHubToken, repoLogger);
      await manager.ensureCloned();
      await manager.checkoutBase(body.branch ?? repo.defaultBranch ?? "develop");

      if (body.install !== false) {
        await manager.installDependencies();
      }

      // Determine which scripts to run
      const requestedScripts = body.scripts?.length ? body.scripts : repo.checkScripts;

      // If no scripts specified, discover available ones from package.json
      let availableScripts: string[] = [];
      const pkgPath = join(localPath, "package.json");
      if (existsSync(pkgPath)) {
        try {
          const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as {
            scripts?: Record<string, string>;
          };
          availableScripts = Object.keys(pkg.scripts ?? {});
        } catch {
          /* ignore */
        }
      }

      const scriptsToRun = requestedScripts?.length
        ? requestedScripts
        : availableScripts.filter((s) =>
            [
              "lint",
              "build",
              "tsc",
              "tsc:strict",
              "tsc:strict:diff",
              "test",
              "check",
              "biome:lint",
              "biome:check",
              "biome:format",
            ].includes(s),
          );

      const pm = detectPackageManager(localPath, repo.installCmd);
      const results: Array<{
        script: string;
        ok: boolean;
        exitCode: number;
        durationMs: number;
        output: string;
      }> = [];

      for (const script of scriptsToRun) {
        if (!availableScripts.includes(script)) {
          results.push({
            script,
            ok: false,
            exitCode: -1,
            durationMs: 0,
            output: `script "${script}" not found in package.json`,
          });
          continue;
        }

        repoLogger.info({ script }, "running_check_script");
        const t0 = Date.now();
        const proc = Bun.spawn([pm.cmd, "run", script], {
          cwd: localPath,
          stdout: "pipe",
          stderr: "pipe",
          env: { ...process.env, npm_config_engine_strict: "false" },
        });
        const [stdout, stderr] = await Promise.all([
          new Response(proc.stdout).text(),
          new Response(proc.stderr).text(),
        ]);
        const exitCode = await proc.exited;
        const output = `${stdout}\n${stderr}`.trim();
        const durationMs = Date.now() - t0;

        results.push({
          script,
          ok: exitCode === 0,
          exitCode,
          durationMs,
          output: output.slice(-2000),
        });
      }

      const allPassed = results.every((r) => r.ok);

      return c.json({
        repo: repoName,
        branch: body.branch ?? repo.defaultBranch ?? "develop",
        localPath,
        success: allPassed,
        availableScripts,
        configuredCheckScripts: repo.checkScripts ?? [],
        results,
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      repoLogger.error({ error: message }, "debug_repo_checks_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Prompts (per-job artifacts) ────────────────────────

  app.get("/api/prompts/:issueKey", (c) => {
    const issueKey = c.req.param("issueKey");
    const huBotDir = join(deps.workdirPath, ".hu-bot");

    if (!existsSync(huBotDir)) {
      return c.json({ issueKey, jobs: [] });
    }

    const prefix = `${issueKey}-`;
    const jobDirs = readdirSync(huBotDir, { withFileTypes: true })
      .filter((d) => d.isDirectory() && d.name.startsWith(prefix))
      .map((d) => d.name)
      .sort()
      .reverse();

    const jobs = jobDirs.map((dirName) => {
      const abs = join(huBotDir, dirName);
      const hasPrompt = existsSync(join(abs, "prompt.md"));
      const assetsDir = join(abs, "assets");
      const assets = existsSync(assetsDir)
        ? readdirSync(assetsDir).filter((f) => !f.startsWith("."))
        : [];
      return { dir: `.hu-bot/${dirName}`, hasPrompt, assets };
    });

    return c.json({ issueKey, jobs });
  });

  app.get("/api/prompts/:issueKey/latest/prompt", (c) => {
    const issueKey = c.req.param("issueKey");
    const huBotDir = join(deps.workdirPath, ".hu-bot");

    if (!existsSync(huBotDir)) {
      return c.json({ error: "no job directory found" }, 404);
    }

    const prefix = `${issueKey}-`;
    const latest = readdirSync(huBotDir, { withFileTypes: true })
      .filter((d) => d.isDirectory() && d.name.startsWith(prefix))
      .map((d) => d.name)
      .sort()
      .pop();

    if (!latest) {
      return c.json({ error: `no job directory for ${issueKey}` }, 404);
    }

    const promptPath = join(huBotDir, latest, "prompt.md");
    if (!existsSync(promptPath)) {
      return c.json({ error: "prompt.md not found in latest job dir" }, 404);
    }

    c.header("Content-Type", "text/markdown; charset=utf-8");
    return c.body(readFileSync(promptPath, "utf-8"));
  });

  // ── Cursor ────────────────────────────────────────────

  app.get("/api/cursor/models", async (c) => {
    try {
      const models = await deps.cursorCloudClient.listModels();
      return c.json(models);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "debug_cursor_models_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/cursor/agents", async (c) => {
    const agentId = c.req.query("id");
    if (!agentId) {
      return c.json({ error: "id query param is required" }, 400);
    }
    try {
      const agent = await deps.cursorCloudClient.getAgentDetails(agentId);
      return c.json(agent);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ agentId, error: message }, "debug_cursor_agent_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/cursor/agents/conversation", async (c) => {
    const agentId = c.req.query("id");
    if (!agentId) {
      return c.json({ error: "id query param is required" }, 400);
    }
    try {
      const conversation = await deps.cursorCloudClient.getConversation(agentId);
      return c.json({ agentId, conversation });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ agentId, error: message }, "debug_cursor_conversation_error");
      return c.json({ error: message }, 502);
    }
  });

  // ── Datadog ───────────────────────────────────────────

  app.get("/api/datadog/logs", async (c) => {
    if (!deps.datadogLogsClient) {
      return c.json(
        { error: "Datadog not configured: set DATADOG_API_KEY and DATADOG_APP_KEY" },
        503,
      );
    }

    const qs = Object.fromEntries(new URL(c.req.url).searchParams.entries());
    const parsed = datadogLogsQuerySchema.safeParse(qs);
    if (!parsed.success) {
      const flat = parsed.error.flatten();
      const msg =
        Object.entries(flat.fieldErrors)
          .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`)
          .join("; ") ||
        flat.formErrors.join("; ") ||
        parsed.error.message;
      return c.json({ error: msg }, 400);
    }

    const { query, window, hideThinking } = parsed.data;
    const limit = parsed.data.limit ?? (parsed.data.window === "24h" ? 1_000 : 50);
    let fromIso: string;
    let toIso: string;

    if (parsed.data.from && parsed.data.to) {
      const from = new Date(parsed.data.from);
      const to = new Date(parsed.data.to);
      if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
        return c.json({ error: "from and to must be valid ISO-8601 datetimes" }, 400);
      }
      if (to.getTime() <= from.getTime()) {
        return c.json({ error: "to must be after from" }, 400);
      }
      if (to.getTime() - from.getTime() > MAX_DATADOG_LOG_RANGE_MS) {
        return c.json({ error: "time range must not exceed 24 hours" }, 400);
      }
      fromIso = from.toISOString();
      toIso = to.toISOString();
    } else {
      const now = Date.now();
      toIso = new Date(now).toISOString();
      fromIso = new Date(now - datadogWindowToMs(window)).toISOString();
    }

    try {
      const result = await deps.datadogLogsClient.searchLogs({
        query,
        from: fromIso,
        to: toIso,
        limit,
      });
      const logs = formatDatadogLogLines(result.logs, { hideThinking });
      return c.json({
        logs,
        from: fromIso,
        to: toIso,
        total: result.total,
        ...(result.truncated ? { truncated: true } : {}),
        ...(hideThinking ? { hideThinking: true } : {}),
      });
    } catch (err) {
      if (err instanceof DatadogApiError) {
        deps.logger.error(
          { status: err.status, snippet: err.responseBody.slice(0, 200) },
          "debug_datadog_logs_error",
        );
        return c.json({ error: err.message }, 502);
      }
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "debug_datadog_logs_error");
      return c.json({ error: message }, 502);
    }
  });

  // ── Datadog by ticket ─────────────────────────────────

  const datadogByTicketSchema = z
    .object({
      ticketCode: z.string().min(1, "ticketCode is required"),
      window: z.enum(["15m", "1h", "24h"]).optional().default("24h"),
      from: z.string().optional(),
      to: z.string().optional(),
      limit: z.coerce.number().int().min(1).max(50_000).optional(),
      hideThinking: z
        .string()
        .optional()
        .transform((v) => v === "true"),
    })
    .superRefine((data, ctx) => {
      const hasFrom = data.from != null && data.from.length > 0;
      const hasTo = data.to != null && data.to.length > 0;
      if (hasFrom !== hasTo) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "from and to must both be set or both omitted",
        });
      }
    });

  app.get("/api/datadog/logs/by-ticket", async (c) => {
    if (!deps.datadogLogsClient) {
      return c.json(
        { error: "Datadog not configured: set DATADOG_API_KEY and DATADOG_APP_KEY" },
        503,
      );
    }

    const qs = Object.fromEntries(new URL(c.req.url).searchParams.entries());
    const parsed = datadogByTicketSchema.safeParse(qs);
    if (!parsed.success) {
      const flat = parsed.error.flatten();
      const msg =
        Object.entries(flat.fieldErrors)
          .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`)
          .join("; ") ||
        flat.formErrors.join("; ") ||
        parsed.error.message;
      return c.json({ error: msg }, 400);
    }

    const { ticketCode, window, hideThinking } = parsed.data;
    const limit = parsed.data.limit ?? 1_000;
    const query = `service:hu-agent (@issueKey:"${ticketCode}" OR "[${ticketCode}]")`;
    let fromIso: string;
    let toIso: string;

    if (parsed.data.from && parsed.data.to) {
      const from = new Date(parsed.data.from);
      const to = new Date(parsed.data.to);
      if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime())) {
        return c.json({ error: "from and to must be valid ISO-8601 datetimes" }, 400);
      }
      if (to.getTime() <= from.getTime()) {
        return c.json({ error: "to must be after from" }, 400);
      }
      if (to.getTime() - from.getTime() > MAX_DATADOG_LOG_RANGE_MS) {
        return c.json({ error: "time range must not exceed 24 hours" }, 400);
      }
      fromIso = from.toISOString();
      toIso = to.toISOString();
    } else {
      const now = Date.now();
      toIso = new Date(now).toISOString();
      const windowMs =
        window === "15m" ? 15 * 60 * 1000 : window === "1h" ? 60 * 60 * 1000 : 24 * 60 * 60 * 1000;
      fromIso = new Date(now - windowMs).toISOString();
    }

    try {
      const result = await deps.datadogLogsClient.searchLogs({
        query,
        from: fromIso,
        to: toIso,
        limit,
      });
      const logs = formatDatadogLogLines(result.logs, { hideThinking });
      return c.json({
        ticketCode,
        logs,
        from: fromIso,
        to: toIso,
        total: result.total,
        ...(result.truncated ? { truncated: true } : {}),
        ...(hideThinking ? { hideThinking: true } : {}),
      });
    } catch (err) {
      if (err instanceof DatadogApiError) {
        deps.logger.error(
          { ticketCode, status: err.status, snippet: err.responseBody.slice(0, 200) },
          "debug_datadog_ticket_logs_error",
        );
        return c.json({ error: err.message }, 502);
      }
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ ticketCode, error: message }, "debug_datadog_ticket_logs_error");
      return c.json({ error: message }, 502);
    }
  });

  // ── Connectivity ──────────────────────────────────────

  app.get("/api/connectivity", async (c) => {
    type CheckResult =
      | { ok: true; [key: string]: unknown }
      | { ok: false; error: string; [key: string]: unknown };

    async function checkJira(): Promise<CheckResult> {
      const t0 = Date.now();
      try {
        const user = await deps.jiraClient.getMyself();
        const u = user as unknown as Record<string, unknown>;
        return {
          ok: true,
          account: u["emailAddress"] ?? u["displayName"],
          durationMs: Date.now() - t0,
        };
      } catch (err) {
        return {
          ok: false,
          error: err instanceof Error ? err.message : String(err),
          durationMs: Date.now() - t0,
        };
      }
    }

    async function checkCursor(): Promise<CheckResult> {
      const t0 = Date.now();
      try {
        const models = await deps.cursorCloudClient.listModels();
        const count = Array.isArray(models)
          ? models.length
          : ((models as Record<string, unknown[]>)?.data?.length ?? "?");
        return { ok: true, modelCount: count, durationMs: Date.now() - t0 };
      } catch (err) {
        return {
          ok: false,
          error: err instanceof Error ? err.message : String(err),
          durationMs: Date.now() - t0,
        };
      }
    }

    async function checkGitHubToken(): Promise<CheckResult> {
      const t0 = Date.now();
      try {
        const token = await deps.getGitHubToken();
        const res = await fetch("https://api.github.com/rate_limit", {
          headers: {
            Authorization: `Bearer ${token}`,
            Accept: "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
          },
        });
        if (!res.ok) {
          const body = await res.text();
          return {
            ok: false,
            error: `GitHub ${res.status}: ${body.slice(0, 200)}`,
            durationMs: Date.now() - t0,
          };
        }
        const body = (await res.json()) as Record<string, unknown>;
        const expiration = res.headers.get("github-authentication-token-expiration") ?? null;
        const expiresAt = expiration ? new Date(expiration).toISOString() : null;
        const expiresInMin = expiresAt
          ? Math.round((new Date(expiresAt).getTime() - Date.now()) / 60_000)
          : null;
        const core = (body.resources as Record<string, unknown>)?.core as
          | Record<string, number>
          | undefined;
        return {
          ok: true,
          tokenType: token.startsWith("ghs_") ? "github-app-installation" : "pat",
          tokenPreview: `${token.slice(0, 8)}…`,
          expiresAt,
          expiresInMin,
          rateLimit: core ? { limit: core.limit, remaining: core.remaining } : undefined,
          durationMs: Date.now() - t0,
        };
      } catch (err) {
        return {
          ok: false,
          error: err instanceof Error ? err.message : String(err),
          durationMs: Date.now() - t0,
        };
      }
    }

    async function checkGitHubRepoAccess(): Promise<CheckResult> {
      const t0 = Date.now();
      const repos = deps.repoRegistry.getAll();
      if (repos.length === 0) return { ok: false, error: "No repos configured", durationMs: 0 };

      try {
        const token = await deps.getGitHubToken();
        const results = await Promise.all(
          repos.map(async (repo) => {
            const match = repo.url.match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?$/);
            if (!match?.[1] || !match[2])
              return { repo: repo.name, ok: false, error: "Cannot parse URL" };
            const [, owner, repoName] = match;
            const res = await fetch(`https://api.github.com/repos/${owner}/${repoName}`, {
              headers: {
                Authorization: `Bearer ${token}`,
                Accept: "application/vnd.github+json",
                "X-GitHub-Api-Version": "2022-11-28",
              },
            });
            if (!res.ok) return { repo: repo.name, ok: false, error: `HTTP ${res.status}` };
            const body = (await res.json()) as Record<string, unknown>;
            const perms = body.permissions as Record<string, boolean> | undefined;
            return {
              repo: repo.name,
              ok: true,
              canPush: perms?.push ?? null,
              canAdmin: perms?.admin ?? null,
              defaultBranch: body.default_branch,
            };
          }),
        );
        const allOk = results.every((r) => r.ok);
        if (allOk) {
          return { ok: true as const, repos: results, durationMs: Date.now() - t0 };
        }
        return {
          ok: false as const,
          error: "One or more repos failed",
          repos: results,
          durationMs: Date.now() - t0,
        };
      } catch (err) {
        return {
          ok: false,
          error: err instanceof Error ? err.message : String(err),
          durationMs: Date.now() - t0,
        };
      }
    }

    async function checkGitCloneAccess(): Promise<CheckResult> {
      const t0 = Date.now();
      const repos = deps.repoRegistry.getAll();
      if (repos.length === 0) return { ok: false, error: "No repos configured", durationMs: 0 };

      try {
        const results = await Promise.all(
          repos.map(async (repo) => {
            const repoT0 = Date.now();
            try {
              await execFileAsync("git", ["ls-remote", "--heads", repo.url, "HEAD"], {
                timeout: 15_000,
                env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
              });
              return { repo: repo.name, ok: true, durationMs: Date.now() - repoT0 };
            } catch (err) {
              const msg = err instanceof Error ? err.message : String(err);
              const short = msg.includes("SSL_ERROR_SYSCALL")
                ? "SSL_ERROR_SYSCALL"
                : msg.includes("Could not resolve host")
                  ? "DNS resolution failed"
                  : msg.includes("timed out")
                    ? "timeout"
                    : msg.slice(0, 120);
              return { repo: repo.name, ok: false, error: short, durationMs: Date.now() - repoT0 };
            }
          }),
        );
        const allOk = results.every((r) => r.ok);
        if (allOk) {
          return { ok: true as const, repos: results, durationMs: Date.now() - t0 };
        }
        return {
          ok: false as const,
          error: `${results.filter((r) => !r.ok).length}/${results.length} repos unreachable`,
          repos: results,
          durationMs: Date.now() - t0,
        };
      } catch (err) {
        return {
          ok: false,
          error: err instanceof Error ? err.message : String(err),
          durationMs: Date.now() - t0,
        };
      }
    }

    const [jira, cursor, githubToken, githubRepos, gitClone] = await Promise.allSettled([
      checkJira(),
      checkCursor(),
      checkGitHubToken(),
      checkGitHubRepoAccess(),
      checkGitCloneAccess(),
    ]);

    const settle = (r: PromiseSettledResult<CheckResult>): CheckResult =>
      r.status === "fulfilled"
        ? r.value
        : { ok: false, error: r.reason instanceof Error ? r.reason.message : String(r.reason) };

    const checks = {
      jira: settle(jira),
      cursor: settle(cursor),
      githubToken: settle(githubToken),
      githubRepos: settle(githubRepos),
      gitCloneAccess: settle(gitClone),
    };

    const allOk = Object.values(checks).every((c) => c.ok);
    const anyFailed = Object.values(checks).some((c) => !c.ok);

    return c.json(
      {
        overall: allOk ? "ok" : anyFailed ? "degraded" : "ok",
        checks,
      },
      allOk ? 200 : 207,
    );
  });

  // ── GitHub ────────────────────────────────────────────

  app.get("/api/github/org-repos", async (c) => {
    const org = (c.req.query("org") ?? "HumandDev").trim();
    const checkGit = c.req.query("git") === "true";

    try {
      const token = await deps.getGitHubToken();
      const headers = {
        Authorization: `Bearer ${token}`,
        Accept: "application/vnd.github+json",
        "X-GitHub-Api-Version": "2022-11-28",
      };

      // Paginate all org repos
      const allRepos: Array<Record<string, unknown>> = [];
      let page = 1;
      while (true) {
        const res = await fetch(
          `https://api.github.com/orgs/${org}/repos?per_page=100&page=${page}&sort=full_name`,
          { headers },
        );
        if (!res.ok) {
          const body = await res.text();
          return c.json(
            { error: `GitHub ${res.status}: ${body.slice(0, 300)}` },
            res.status as 400,
          );
        }
        const batch = (await res.json()) as Array<Record<string, unknown>>;
        if (batch.length === 0) break;
        allRepos.push(...batch);
        page++;
      }

      const registeredNames = new Set(deps.repoRegistry.getAll().map((r) => r.name));

      const repos = await Promise.all(
        allRepos.map(async (r) => {
          const perms = r.permissions as Record<string, boolean> | undefined;
          const name = r.name as string;
          const entry: Record<string, unknown> = {
            name,
            fullName: r.full_name,
            private: r.private,
            archived: r.archived,
            defaultBranch: r.default_branch,
            canPush: perms?.push ?? null,
            canAdmin: perms?.admin ?? null,
            inRegistry: registeredNames.has(name),
          };

          if (checkGit) {
            const t0 = Date.now();
            try {
              await execFileAsync("git", ["ls-remote", "--heads", r.clone_url as string, "HEAD"], {
                timeout: 10_000,
                env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
              });
              entry.gitAccess = true;
              entry.gitDurationMs = Date.now() - t0;
            } catch (err) {
              const msg = err instanceof Error ? err.message : String(err);
              entry.gitAccess = false;
              entry.gitError = msg.includes("SSL_ERROR_SYSCALL")
                ? "SSL_ERROR_SYSCALL"
                : msg.includes("Could not resolve host")
                  ? "DNS resolution failed"
                  : msg.includes("timed out")
                    ? "timeout"
                    : msg.slice(0, 120);
              entry.gitDurationMs = Date.now() - t0;
            }
          }

          return entry;
        }),
      );

      const summary = {
        total: repos.length,
        withPush: repos.filter((r) => r.canPush).length,
        inRegistry: repos.filter((r) => r.inRegistry).length,
        archived: repos.filter((r) => r.archived).length,
        ...(checkGit
          ? {
              gitOk: repos.filter((r) => r.gitAccess === true).length,
              gitFailed: repos.filter((r) => r.gitAccess === false).length,
            }
          : {}),
      };

      return c.json({ org, summary, repos });
    } catch (err) {
      return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
    }
  });

  app.get("/api/github/auth", async (c) => {
    try {
      const token = await deps.getGitHubToken();

      // GET /rate_limit is the lightest call that validates a token without
      // needing any repo access and is not counted against the rate limit itself.
      const res = await fetch("https://api.github.com/rate_limit", {
        headers: {
          Authorization: `Bearer ${token}`,
          Accept: "application/vnd.github+json",
          "X-GitHub-Api-Version": "2022-11-28",
        },
      });

      const body = (await res.json()) as Record<string, unknown>;

      if (!res.ok) {
        return c.json(
          {
            authenticated: false,
            status: res.status,
            tokenPreview: `${token.slice(0, 8)}…`,
            github: body,
          },
          502,
        );
      }

      // Surface expiry info from the response headers (GitHub App tokens include x-github-token-expiration).
      const expiration = res.headers.get("github-authentication-token-expiration") ?? null;
      const expiresAt = expiration ? new Date(expiration).toISOString() : null;
      const expiresInMs = expiresAt ? new Date(expiresAt).getTime() - Date.now() : null;
      const expiresInMin = expiresInMs !== null ? Math.round(expiresInMs / 60_000) : null;

      return c.json({
        authenticated: true,
        tokenPreview: `${token.slice(0, 8)}…`,
        tokenType: token.startsWith("ghs_") ? "github-app-installation" : "pat",
        expiresAt,
        expiresInMin,
        rateLimit: body,
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "debug_github_auth_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const branch = c.req.query("branch");

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);

      if (branch) {
        const pr = await ghClient.getPrForBranch(branch);
        return c.json({ repo: repoName, branch, pr });
      }

      const pulls = await ghClient.listOpenPrs();
      return c.json({ repo: repoName, total: pulls.length, pulls });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ repoName, error: message }, "debug_github_pulls_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls/diff", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const prNumber = parseInt(c.req.query("pr") ?? "", 10);

    if (isNaN(prNumber)) {
      return c.json({ error: "pr query param is required and must be a number" }, 400);
    }

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const diff = await ghClient.getPrDiff(prNumber);
      return c.text(diff);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ repoName, prNumber, error: message }, "debug_github_diff_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls/comments", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const prNumber = parseInt(c.req.query("pr") ?? "", 10);
    if (isNaN(prNumber)) {
      return c.json({ error: "pr query param is required and must be a number" }, 400);
    }
    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }
    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const perPage = c.req.query("per_page");
      const page = c.req.query("page");
      const options =
        perPage !== undefined || page !== undefined
          ? {
              perPage: perPage ? parseInt(perPage, 10) : undefined,
              page: page ? parseInt(page, 10) : undefined,
            }
          : undefined;
      const comments = await ghClient.listIssueComments(prNumber, options);
      return c.json({ repo: repoName, prNumber, total: comments.length, comments });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ repoName, prNumber, error: message }, "debug_github_comments_error");
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls/review-comments", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const prNumber = parseInt(c.req.query("pr") ?? "", 10);
    if (isNaN(prNumber)) {
      return c.json({ error: "pr query param is required and must be a number" }, 400);
    }
    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }
    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const perPage = c.req.query("per_page");
      const page = c.req.query("page");
      const options =
        perPage !== undefined || page !== undefined
          ? {
              perPage: perPage ? parseInt(perPage, 10) : undefined,
              page: page ? parseInt(page, 10) : undefined,
            }
          : undefined;
      const reviewComments = await ghClient.listPullReviewComments(prNumber, options);
      return c.json({ repo: repoName, prNumber, total: reviewComments.length, reviewComments });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error(
        { repoName, prNumber, error: message },
        "debug_github_review_comments_error",
      );
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls/review-comment", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const commentId = parseInt(c.req.query("commentId") ?? "", 10);
    if (isNaN(commentId)) {
      return c.json({ error: "commentId query param is required and must be a number" }, 400);
    }
    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }
    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const comment = await ghClient.getPullReviewComment(commentId);
      return c.json({ repo: repoName, commentId, comment });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error(
        { repoName, commentId, error: message },
        "debug_github_review_comment_error",
      );
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/github/pulls/reviews", async (c) => {
    const repoName = c.req.query("repo");
    if (!repoName) {
      return c.json({ error: "repo query param is required" }, 400);
    }
    const prNumber = parseInt(c.req.query("pr") ?? "", 10);
    if (isNaN(prNumber)) {
      return c.json({ error: "pr query param is required and must be a number" }, 400);
    }
    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }
    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const perPage = c.req.query("per_page");
      const page = c.req.query("page");
      const options =
        perPage !== undefined || page !== undefined
          ? {
              perPage: perPage ? parseInt(perPage, 10) : undefined,
              page: page ? parseInt(page, 10) : undefined,
            }
          : undefined;

      const [reviews, inlineComments] = await Promise.all([
        ghClient.listPullReviews(prNumber, options),
        ghClient.listPullReviewComments(prNumber),
      ]);

      const rootComments = inlineComments.filter((c) => c.in_reply_to_id === null);
      const repliesByRoot = new Map<number, typeof inlineComments>();
      for (const c of inlineComments) {
        if (c.in_reply_to_id === null) continue;
        const group = repliesByRoot.get(c.in_reply_to_id) ?? [];
        group.push(c);
        repliesByRoot.set(c.in_reply_to_id, group);
      }

      type Thread = {
        rootCommentId: number;
        path: string;
        line: number | null;
        messages: Array<{
          id: number;
          user: string | null;
          body: string;
          created_at: string;
        }>;
      };

      const threadsByReview = new Map<number, Thread[]>();
      for (const root of rootComments) {
        const replies = repliesByRoot.get(root.id) ?? [];
        const thread: Thread = {
          rootCommentId: root.id,
          path: root.path,
          line: root.line,
          messages: [
            root,
            ...replies.sort(
              (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime(),
            ),
          ].map((m) => ({
            id: m.id,
            user: m.user?.login ?? null,
            body: m.body,
            created_at: m.created_at,
          })),
        };

        const reviewId = root.pull_request_review_id;
        if (reviewId !== null) {
          const group = threadsByReview.get(reviewId) ?? [];
          group.push(thread);
          threadsByReview.set(reviewId, group);
        }
      }

      const enriched = reviews.map((r) => ({
        ...r,
        threads: threadsByReview.get(r.id) ?? [],
      }));

      return c.json({ repo: repoName, prNumber, total: enriched.length, reviews: enriched });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ repoName, prNumber, error: message }, "debug_github_reviews_error");
      return c.json({ error: message }, 502);
    }
  });

  app.post("/api/github/pulls", async (c) => {
    const parsed = createPrBodySchema
      .extend({ repo: z.string().min(1, "repo is required") })
      .safeParse(await c.req.json());
    if (!parsed.success) {
      return c.json({ error: parsed.error.flatten().fieldErrors }, 400);
    }
    const { repo: repoName, branch, title, body, base } = parsed.data;

    const repo = deps.repoRegistry.getByName(repoName);
    if (!repo) {
      return c.json({ error: `Repo '${repoName}' not found in registry` }, 404);
    }

    try {
      const token = await deps.getGitHubToken();
      const ghClient = new GitHubClientImpl(token, repo.url, deps.jiraDomain, deps.logger);
      const pr = await ghClient.createPr(branch, title, body, base);
      return c.json({ repo: repoName, pr: { number: pr.number, url: pr.url } });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ repoName, branch, error: message }, "debug_github_create_pr_error");
      return c.json({ error: message }, 502);
    }
  });

  // ── Jira User ───────────────────────────────────────

  app.get("/api/jira/myself", async (c) => {
    try {
      const user = await deps.jiraClient.getMyself();
      return c.json(user);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  app.get("/api/jira/user", async (c) => {
    const email = c.req.query("email");
    if (!email) {
      return c.json({ error: "email query param is required" }, 400);
    }

    try {
      const user = await deps.jiraClient.findUserByEmail(email);
      if (!user) {
        return c.json({ error: `No user found for ${email}` }, 404);
      }
      return c.json(user);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  // ── Jira Create Subtask (from story/bug) ──────────────────────────────

  const createSubtaskHandler = async (
    parentKey: string,
    body: z.infer<typeof createSubtaskBodySchema>,
  ) => {
    const user = await deps.jiraClient.findUserByEmail(body.assigneeEmail);
    if (!user) {
      return { status: 404 as const, json: { error: `No user found for ${body.assigneeEmail}` } };
    }
    const parentComponents = await deps.jiraClient.getIssueComponents(parentKey);
    const result = await deps.jiraClient.createSubtask(
      parentKey,
      body.summary,
      user.accountId,
      parentComponents.length ? parentComponents : undefined,
    );
    return { status: 201 as const, json: result };
  };

  app.post("/api/jira/subtask", async (c) => {
    const parseResult = createSubtaskBodySchema.safeParse(await c.req.json());
    if (!parseResult.success) {
      const first = parseResult.error.flatten().fieldErrors;
      const message =
        Object.entries(first)
          .map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(", ") : v}`)
          .join("; ") || "Invalid request body";
      return c.json({ error: message }, 400);
    }
    const body = parseResult.data;

    try {
      const out = await createSubtaskHandler(body.parentKey, body);
      return c.json(out.json, out.status);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  // ── Jira List Transitions ─────────────────────────────

  app.get("/api/jira/transitions", async (c) => {
    const issueKey = c.req.query("issueKey");
    if (!issueKey) {
      return c.json({ error: "issueKey query param is required" }, 400);
    }

    try {
      const transitions = await deps.jiraClient.getTransitions(issueKey);
      return c.json({ issueKey, transitions });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  // ── Jira Add Comment ─────────────────────────────────

  app.post("/api/jira/comment", async (c) => {
    const body = await c.req.json<{ issueKey?: string; body?: string }>();

    if (!body.issueKey || !body.body) {
      return c.json({ error: "issueKey and body are required" }, 400);
    }

    try {
      const result = await deps.jiraClient.addComment(body.issueKey, body.body);
      return c.json({ success: true, issueKey: body.issueKey, commentId: result.id });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  // ── Jira Transition Issue ──────────────────────────────

  app.post("/api/jira/transition", async (c) => {
    const body = await c.req.json<{
      issueKey?: string;
      targetStatus?: string;
    }>();

    if (!body.issueKey || !body.targetStatus) {
      return c.json({ error: "issueKey and targetStatus are required" }, 400);
    }

    try {
      await deps.jiraClient.transitionIssue(body.issueKey, body.targetStatus);
      return c.json({ success: true, issueKey: body.issueKey, status: body.targetStatus });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      return c.json({ error: message }, 502);
    }
  });

  // ── Pipeline triage ─────────────────────────────────

  app.post("/api/pipeline/triage", async (c) => {
    const body = await c.req.json<{ issueKey?: string }>();
    const issueKey = body.issueKey;

    if (!issueKey || typeof issueKey !== "string") {
      return c.json({ error: "issueKey is required" }, 400);
    }

    try {
      const result = await deps.pipeline.triage(issueKey);
      return c.json({ issueKey, ...result });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ issueKey, error: message }, "debug_triage_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Pipeline fix ──────────────────────────────────────

  app.post("/api/pipeline/fix", async (c) => {
    const body = await c.req.json<{ issueKey?: string }>();
    const issueKey = body.issueKey;

    if (!issueKey || typeof issueKey !== "string") {
      return c.json({ error: "issueKey is required" }, 400);
    }

    try {
      const issue = await deps.jiraClient.getIssue(issueKey);

      await deps.queue.enqueue({
        jobKind: "jira",
        issueId: issue.id,
        issueKey,
        summary: issue.summary,
        description: issue.description ?? "",
      });

      return c.json({ accepted: true, issueKey }, 202);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ issueKey, error: message }, "debug_fix_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Pipeline trigger ──────────────────────────────────

  app.post("/api/pipeline/trigger", async (c) => {
    const body = await c.req.json<{ issueKey?: string }>();
    const issueKey = body.issueKey;

    if (!issueKey || typeof issueKey !== "string") {
      return c.json({ error: "issueKey is required" }, 400);
    }

    try {
      const issue = await deps.jiraClient.getIssue(issueKey);

      await deps.queue.enqueue({
        jobKind: "jira",
        issueId: issue.id,
        issueKey,
        summary: issue.summary,
        description: issue.description ?? "",
      });

      return c.json({ accepted: true, issueKey }, 202);
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ issueKey, error: message }, "debug_trigger_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Daily Report ──────────────────────────────────────

  app.get("/api/reports/daily", async (c) => {
    try {
      const skipSlackRaw = c.req.query("skip_slack");
      const skipSlack = skipSlackRaw === "true" || skipSlackRaw === "1";

      const report = await collectDailyReport({
        repoRegistry: deps.repoRegistry,
        getGitHubToken: deps.getGitHubToken,
        githubBotUser: deps.githubBotUser,
        logger: deps.logger,
        jiraClient: deps.jiraClient,
        jiraDomain: deps.jiraDomain,
        squadScope: deps.squadScope,
        botEmail: deps.botEmail,
      });

      let slackSent = false;
      if (
        !skipSlack &&
        deps.slackNotifier &&
        deps.slackDailyReportChannelId &&
        report.totalTickets > 0
      ) {
        await deps.slackNotifier.notifyDailyReport(
          {
            ticketsByTeam: report.ticketsByTeam,
          },
          deps.slackDailyReportChannelId,
        );
        slackSent = true;
      }

      return c.json({ ...report, slackSent });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "debug_daily_report_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Slack ─────────────────────────────────────────────

  app.get("/api/slack/usergroups", async (c) => {
    if (!deps.slackBotToken) {
      return c.json({ error: "SLACK_BOT_TOKEN is not configured" }, 503);
    }
    try {
      const response = await fetch("https://slack.com/api/usergroups.list", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${deps.slackBotToken}`,
          "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
        },
        body: new URLSearchParams({ include_users: "false" }),
      });

      if (!response.ok) {
        return c.json({ error: `Slack HTTP ${response.status}` }, 502);
      }

      const raw: unknown = await response.json();
      const envelope = slackUsergroupsListEnvelopeSchema.safeParse(raw);
      if (!envelope.success) {
        deps.logger.warn({ issues: envelope.error.issues }, "slack_usergroups_unexpected_shape");
        return c.json({ error: "Unexpected Slack response shape" }, 502);
      }
      if (!envelope.data.ok) {
        const slackError = envelope.data.error ?? "unknown_error";
        const payload: Record<string, string> = { error: slackError };
        if (envelope.data.needed != null && envelope.data.needed.length > 0) {
          payload.needed = envelope.data.needed;
        }
        if (slackError === "missing_scope") {
          payload.hint =
            "Add Bot scope usergroups:read in Slack app → OAuth & Permissions, reinstall the app to the workspace, then update SLACK_BOT_TOKEN if the installer issued a new token.";
        }
        deps.logger.warn(
          { slackError, needed: envelope.data.needed },
          "slack_usergroups_api_error",
        );
        return c.json(payload, 502);
      }

      const groups = envelope.data.usergroups ?? [];
      const usergroups = groups.map((g) => {
        const parsed = slackUsergroupSchema.safeParse(g);
        if (!parsed.success) {
          return {
            id: String((g as { id?: unknown }).id ?? ""),
            handle: "",
            name: "",
            description: "",
          };
        }
        return {
          id: parsed.data.id,
          handle: parsed.data.handle ?? "",
          name: parsed.data.name ?? "",
          description: parsed.data.description ?? "",
        };
      });

      return c.json({ usergroups });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ error: message }, "slack_usergroups_error");
      return c.json({ error: message }, 500);
    }
  });

  // ── Manual poller / scheduler triggers ────────────────
  // Run a single tick on demand. Useful in dev (where pollers are force-disabled)
  // and in prod (skip the next interval wait).

  async function runTick(c: Context, name: string, fn: () => Promise<unknown>): Promise<Response> {
    deps.logger.info({ poller: name }, "debug_poller_tick_triggered");
    try {
      const result = await fn();
      return c.json({
        ok: true,
        poller: name,
        ...(typeof result === "number" ? { enqueued: result } : {}),
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      deps.logger.error({ poller: name, error: message }, "debug_poller_tick_error");
      return c.json({ ok: false, poller: name, error: message }, 500);
    }
  }

  app.post("/api/pollers/jira/tick", (c) => runTick(c, "jira", () => deps.jiraPoller.poll()));

  app.post("/api/pollers/jira-ticket-review/tick", (c) =>
    runTick(c, "jira-ticket-review", () => deps.ticketReviewPoller.poll()),
  );

  app.post("/api/pollers/pr-comment/tick", (c) =>
    runTick(c, "pr-comment", () => deps.prCommentPoller.poll()),
  );

  app.post("/api/pollers/pr-mention/tick", (c) =>
    runTick(c, "pr-mention", () => deps.prMentionPoller.poll()),
  );

  app.post("/api/pollers/slack-mention/tick", (c) => {
    if (!deps.slackMentionPoller) {
      return c.json(
        { ok: false, poller: "slack-mention", reason: "slack-mention poller not initialized" },
        503,
      );
    }
    const poller = deps.slackMentionPoller;
    return runTick(c, "slack-mention", () => poller.poll());
  });

  app.post("/api/scheduler/daily-report/run", (c) => {
    if (!deps.dailyReportScheduler) {
      return c.json(
        { ok: false, scheduler: "daily-report", reason: "daily-report scheduler not initialized" },
        503,
      );
    }
    const scheduler = deps.dailyReportScheduler;
    return runTick(c, "daily-report", () => scheduler.triggerNow());
  });

  return app;
}

interface SearchParams {
  project: string[];
  issueType: string[];
  excludeTypes: string[];
  status: string | null;
  assignee: string | null;
  reporter: string | null;
  priority: string[];
  labels: string[];
  component: string[];
  sprint: string | null;
  resolution: string | null;
  parent: string | null;
  text: string | null;
  createdAfter: string | null;
  createdBefore: string | null;
  updatedAfter: string | null;
  updatedBefore: string | null;
  orderBy: string;
}

async function resolveUser(value: string | null, deps: DebugDeps): Promise<string | null> {
  if (!value || value === "unassigned") return value;
  if (!value.includes("@")) return value;
  const user = await deps.jiraClient.findUserByEmail(value);
  if (!user) {
    deps.logger.warn({ email: value }, "Could not resolve Jira user by email, using raw value");
    return value;
  }
  return user.accountId;
}

function splitCsv(value: string | undefined): string[] {
  if (!value) return [];
  return value
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
}

function q(value: string): string {
  return /^[a-zA-Z0-9_-]+$/.test(value) ? value : `"${value}"`;
}

function buildSearchJql(p: SearchParams): string {
  const clauses: string[] = [];

  if (p.project.length > 0) {
    clauses.push(`project in (${p.project.join(", ")})`);
  }
  if (p.issueType.length > 0) {
    clauses.push(`issuetype in (${p.issueType.map(q).join(", ")})`);
  }
  if (p.excludeTypes.length > 0) {
    clauses.push(`issuetype not in (${p.excludeTypes.map(q).join(", ")})`);
  }
  if (p.status) {
    clauses.push(`status = ${q(p.status)}`);
  }
  if (p.assignee) {
    clauses.push(p.assignee === "unassigned" ? "assignee is EMPTY" : `assignee = ${q(p.assignee)}`);
  }
  if (p.reporter) {
    clauses.push(`reporter = ${q(p.reporter)}`);
  }
  if (p.priority.length > 0) {
    clauses.push(`priority in (${p.priority.map(q).join(", ")})`);
  }
  if (p.labels.length > 0) {
    clauses.push(p.labels.map((l) => `labels = ${q(l)}`).join(" AND "));
  }
  if (p.component.length > 0) {
    clauses.push(`component in (${p.component.map(q).join(", ")})`);
  }
  if (p.sprint) {
    if (["openSprints()", "closedSprints()", "futureSprints()"].includes(p.sprint)) {
      clauses.push(`sprint in ${p.sprint}`);
    } else {
      clauses.push(`sprint = ${q(p.sprint)}`);
    }
  }
  if (p.resolution) {
    clauses.push(
      p.resolution === "Unresolved" ? "resolution = Unresolved" : `resolution = ${q(p.resolution)}`,
    );
  }
  if (p.parent) {
    clauses.push(`parent = ${p.parent}`);
  }
  if (p.text) {
    clauses.push(`text ~ ${q(p.text)}`);
  }
  if (p.createdAfter) {
    clauses.push(`created >= "${p.createdAfter}"`);
  }
  if (p.createdBefore) {
    clauses.push(`created <= "${p.createdBefore}"`);
  }
  if (p.updatedAfter) {
    clauses.push(`updated >= "${p.updatedAfter}"`);
  }
  if (p.updatedBefore) {
    clauses.push(`updated <= "${p.updatedBefore}"`);
  }

  const orderBy = p.orderBy || "created DESC";
  return clauses.length > 0
    ? `${clauses.join(" AND ")} ORDER BY ${orderBy}`
    : `ORDER BY ${orderBy}`;
}
