const PR_STATUS_CACHE_TTL_MS = 90_000;
const PR_STATUS_CACHE_MAX_ENTRIES = 200;
// Upper bound for resolving a single PR status. resolveGitHubPrStatus makes many
// serial GitHub API calls; under GitHub secondary-rate-limiting a single request
// can otherwise hang 20s+. We bound it so the route fails fast instead of holding
// the response (and a client socket) open — the client keeps its last-known
// status on error, and a later poll fills it in.
const PR_STATUS_RESOLVE_TIMEOUT_MS = 12_000;
const prStatusCache = new Map();

function withTimeout(promise, timeoutMs, label) {
  let timer;
  const timeout = new Promise((_resolve, reject) => {
    timer = setTimeout(() => {
      const error = new Error(`${label} timed out after ${timeoutMs}ms`);
      error.code = 'ETIMEDOUT';
      reject(error);
    }, timeoutMs);
    if (typeof timer.unref === 'function') timer.unref();
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

function getRequestedRepo(req) {
  const owner = typeof req.query?.owner === 'string' ? req.query.owner.trim() : '';
  const repo = typeof req.query?.repo === 'string' ? req.query.repo.trim() : '';
  return owner && repo ? { owner, repo } : null;
}

async function resolveRepoForRequest(octokit, directory, requestedRepo) {
  const { resolveGitHubRepoFromDirectory } = await import('./index.js');
  const { repo } = await resolveGitHubRepoFromDirectory(directory);
  if (!requestedRepo) {
    return repo;
  }
  if (repo?.owner === requestedRepo.owner && repo?.repo === requestedRepo.repo) {
    return requestedRepo;
  }

  const { resolveRepoNetwork } = await import('./repo/fork-detection.js');
  const network = await resolveRepoNetwork(octokit, directory).catch(() => null);
  const allowed = Array.isArray(network)
    ? network.some((item) => item?.owner === requestedRepo.owner && item?.repo === requestedRepo.repo)
    : false;
  return allowed ? requestedRepo : null;
}

function setPrStatusCache(key, data, fetchedAt) {
  // Evict oldest entry when cache exceeds max size
  if (prStatusCache.size >= PR_STATUS_CACHE_MAX_ENTRIES && !prStatusCache.has(key)) {
    const oldest = prStatusCache.entries().next().value;
    if (oldest) {
      prStatusCache.delete(oldest[0]);
    }
  }
  prStatusCache.set(key, { data, fetchedAt });
}

export function registerGitHubRoutes(app) {
  let githubLibraries = null;
  const getGitHubLibraries = async () => {
    if (!githubLibraries) {
      githubLibraries = await import('./index.js');
    }
    return githubLibraries;
  };

  const getGitHubUserSummary = async (octokit) => {
    const me = await octokit.rest.users.getAuthenticated();

    let email = typeof me.data.email === 'string' ? me.data.email : null;
    if (!email) {
      try {
        const emails = await octokit.rest.users.listEmailsForAuthenticatedUser({ per_page: 100 });
        const list = Array.isArray(emails?.data) ? emails.data : [];
        const primaryVerified = list.find((e) => e && e.primary && e.verified && typeof e.email === 'string');
        const anyVerified = list.find((e) => e && e.verified && typeof e.email === 'string');
        email = primaryVerified?.email || anyVerified?.email || null;
      } catch {
        // ignore (scope might be missing)
      }
    }

    return {
      login: me.data.login,
      id: me.data.id,
      avatarUrl: me.data.avatar_url,
      name: typeof me.data.name === 'string' ? me.data.name : null,
      email,
    };
  };

  const isGitHubAuthInvalid = (error) => error?.status === 401 || error?.status === 403;
  const isGitHubResourceUnavailable = (error) => error?.status === 403 || error?.status === 404;

  app.get('/api/github/auth/status', async (_req, res) => {
    try {
      const { getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliActive, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries();
      const { getGhCliToken } = await import('./gh-cli-credential.js');

      const auth = getGitHubAuth();
      let accounts = getGitHubAuthAccounts();
      const ghCliDisabled = isGhCliDisabled();
      const ghCliActive = isGhCliActive();
      const ghToken = getGhCliToken();
      const usingOwnToken = Boolean(auth?.accessToken);
      let ghCliUser = null;

      if (ghToken !== null && !ghCliDisabled) {
        try {
          const { createOctokit } = await import('./octokit.js');
          ghCliUser = await getGitHubUserSummary(createOctokit(ghToken));
        } catch {
          ghCliUser = null;
        }
      }
      if (ghCliActive && !ghCliUser) {
        setGhCliActive(false);
      }

      const ghCliCurrent = ghToken !== null && !ghCliDisabled && Boolean(ghCliUser) && (ghCliActive || !usingOwnToken);
      if (ghCliUser) {
        accounts = accounts
          .map((account) => ({ ...account, current: ghCliCurrent ? false : Boolean(account.current) }))
          .concat({
            id: GH_CLI_ACCOUNT_ID,
            user: ghCliUser,
            current: ghCliCurrent,
            source: 'gh-cli',
          });
      }

      const buildGhCli = (activeUser = null) => ({
        available: ghToken !== null,
        disabled: ghCliDisabled,
        active: ghCliCurrent,
        ...(!ghCliDisabled && (activeUser || ghCliUser) ? { user: activeUser || ghCliUser } : {}),
      });

      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false, accounts, ghCli: buildGhCli() });
      }

      let user = null;
      try {
        user = await getGitHubUserSummary(octokit);
      } catch (error) {
        if (isGitHubAuthInvalid(error)) {
          if (usingOwnToken) clearGitHubAuth();
          return res.json({ connected: false, accounts: getGitHubAuthAccounts(), ghCli: buildGhCli() });
        }
      }

      const fallback = usingOwnToken ? auth.user : null;
      const mergedUser = user || fallback;
      return res.json({
        connected: true,
        user: mergedUser,
        scope: ghCliCurrent ? undefined : auth?.scope,
        accounts,
        ghCli: buildGhCli(ghCliCurrent ? mergedUser : null),
      });
    } catch (error) {
      console.error('Failed to get GitHub auth status:', error);
      return res.status(500).json({ error: error.message || 'Failed to get GitHub auth status' });
    }
  });

  app.post('/api/github/auth/gh-cli', async (req, res) => {
    try {
      const { setGhCliDisabled, isGhCliDisabled } = await getGitHubLibraries();
      const { clearGhCliTokenCache } = await import('./gh-cli-credential.js');
      const disabled = Boolean(req.body?.disabled);
      setGhCliDisabled(disabled);
      clearGhCliTokenCache();
      return res.json({ disabled: isGhCliDisabled() });
    } catch (error) {
      console.error('Failed to update gh CLI setting:', error);
      return res.status(500).json({ error: error.message || 'Failed to update gh CLI setting' });
    }
  });

  app.post('/api/github/auth/start', async (_req, res) => {
    try {
      const { getGitHubClientId, getGitHubScopes, startDeviceFlow } = await getGitHubLibraries();
      const clientId = getGitHubClientId();
      if (!clientId) {
        return res.status(400).json({
          error: 'GitHub OAuth client not configured. Set OPENCHAMBER_GITHUB_CLIENT_ID.',
        });
      }

      const scope = getGitHubScopes();

      const payload = await startDeviceFlow({
        clientId,
        scope,
      });

      return res.json({
        deviceCode: payload.device_code,
        userCode: payload.user_code,
        verificationUri: payload.verification_uri,
        verificationUriComplete: payload.verification_uri_complete,
        expiresIn: payload.expires_in,
        interval: payload.interval,
        scope,
      });
    } catch (error) {
      console.error('Failed to start GitHub device flow:', error);
      return res.status(500).json({ error: error.message || 'Failed to start GitHub device flow' });
    }
  });

  app.post('/api/github/auth/complete', async (req, res) => {
    try {
      const { getGitHubClientId, exchangeDeviceCode, setGitHubAuth, getGitHubAuthAccounts } = await getGitHubLibraries();
      const clientId = getGitHubClientId();
      if (!clientId) {
        return res.status(400).json({
          error: 'GitHub OAuth client not configured. Set OPENCHAMBER_GITHUB_CLIENT_ID.',
        });
      }

      const deviceCode = typeof req.body?.deviceCode === 'string'
        ? req.body.deviceCode
        : (typeof req.body?.device_code === 'string' ? req.body.device_code : '');

      if (!deviceCode) {
        return res.status(400).json({ error: 'deviceCode is required' });
      }

      const payload = await exchangeDeviceCode({ clientId, deviceCode });

      if (payload?.error) {
        return res.json({
          connected: false,
          status: payload.error,
          error: payload.error_description || payload.error,
        });
      }

      const accessToken = payload?.access_token;
      if (!accessToken) {
        return res.status(500).json({ error: 'Missing access_token from GitHub' });
      }

      const { createOctokit } = await import('./octokit.js');
      const octokit = createOctokit(accessToken);
      const user = await getGitHubUserSummary(octokit);

      setGitHubAuth({
        accessToken,
        scope: typeof payload.scope === 'string' ? payload.scope : '',
        tokenType: typeof payload.token_type === 'string' ? payload.token_type : 'bearer',
        user,
      });

      return res.json({
        connected: true,
        user,
        scope: typeof payload.scope === 'string' ? payload.scope : '',
        accounts: getGitHubAuthAccounts(),
      });
    } catch (error) {
      console.error('Failed to complete GitHub device flow:', error);
      return res.status(500).json({ error: error.message || 'Failed to complete GitHub device flow' });
    }
  });

  app.post('/api/github/auth/activate', async (req, res) => {
    try {
      const { activateGitHubAuth, getGitHubAuth, getOctokitOrNull, clearGitHubAuth, getGitHubAuthAccounts, GH_CLI_ACCOUNT_ID, isGhCliDisabled, setGhCliActive } = await getGitHubLibraries();
      const accountId = typeof req.body?.accountId === 'string' ? req.body.accountId : '';
      if (!accountId) {
        return res.status(400).json({ error: 'accountId is required' });
      }
      if (accountId === GH_CLI_ACCOUNT_ID) {
        const { getGhCliToken } = await import('./gh-cli-credential.js');
        const ghToken = !isGhCliDisabled() ? getGhCliToken() : null;
        if (!ghToken) {
          return res.status(404).json({ error: 'GitHub CLI account not found' });
        }

        const { createOctokit } = await import('./octokit.js');
        const user = await getGitHubUserSummary(createOctokit(ghToken));
        setGhCliActive(true);
        const accounts = getGitHubAuthAccounts()
          .map((account) => ({ ...account, current: false }))
          .concat({ id: GH_CLI_ACCOUNT_ID, user, current: true, source: 'gh-cli' });
        return res.json({
          connected: true,
          user,
          accounts,
          ghCli: {
            available: true,
            disabled: false,
            active: true,
            user,
          },
        });
      }

      const activated = activateGitHubAuth(accountId);
      if (!activated) {
        return res.status(404).json({ error: 'GitHub account not found' });
      }

      const auth = getGitHubAuth();
      let accounts = getGitHubAuthAccounts();
      if (!auth?.accessToken) {
        return res.json({ connected: false, accounts });
      }

      const { getGhCliToken } = await import('./gh-cli-credential.js');
      const ghCliDisabled = isGhCliDisabled();
      const ghToken = !ghCliDisabled ? getGhCliToken() : null;
      let ghCliUser = null;
      if (ghToken) {
        try {
          const { createOctokit } = await import('./octokit.js');
          ghCliUser = await getGitHubUserSummary(createOctokit(ghToken));
          accounts = accounts.concat({
            id: GH_CLI_ACCOUNT_ID,
            user: ghCliUser,
            current: false,
            source: 'gh-cli',
          });
        } catch {
          ghCliUser = null;
        }
      }

      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false, accounts, ghCli: { available: ghToken !== null, disabled: ghCliDisabled, active: false, ...(ghCliUser ? { user: ghCliUser } : {}) } });
      }

      let user = auth.user || null;
      try {
        user = await getGitHubUserSummary(octokit);
      } catch (error) {
        if (isGitHubAuthInvalid(error)) {
          clearGitHubAuth();
          return res.json({ connected: false, accounts: getGitHubAuthAccounts() });
        }
      }

      return res.json({
        connected: true,
        user,
        scope: auth.scope,
        accounts,
        ghCli: {
          available: ghToken !== null,
          disabled: ghCliDisabled,
          active: false,
          ...(ghCliUser ? { user: ghCliUser } : {}),
        },
      });
    } catch (error) {
      console.error('Failed to activate GitHub account:', error);
      return res.status(500).json({ error: error.message || 'Failed to activate GitHub account' });
    }
  });

  app.delete('/api/github/auth', async (_req, res) => {
    try {
      const { clearGitHubAuth } = await getGitHubLibraries();
      const removed = clearGitHubAuth();
      return res.json({ success: true, removed });
    } catch (error) {
      console.error('Failed to disconnect GitHub:', error);
      return res.status(500).json({ error: error.message || 'Failed to disconnect GitHub' });
    }
  });

  app.get('/api/github/me', async (_req, res) => {
    try {
      const { getOctokitOrNull, clearGitHubAuth } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.status(401).json({ error: 'GitHub not connected' });
      }
      let user;
      try {
        user = await getGitHubUserSummary(octokit);
      } catch (error) {
        if (isGitHubAuthInvalid(error)) {
          clearGitHubAuth();
          return res.status(401).json({ error: 'GitHub token expired or revoked' });
        }
        throw error;
      }
      return res.json(user);
    } catch (error) {
      console.error('Failed to fetch GitHub user:', error);
      return res.status(500).json({ error: error.message || 'Failed to fetch GitHub user' });
    }
  });

  // ================= GitHub PR APIs =================

  app.get('/api/github/pr/status', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const branch = typeof req.query?.branch === 'string' ? req.query.branch.trim() : '';
      const remote = typeof req.query?.remote === 'string' ? req.query.remote.trim() : 'origin';
      const force = req.query?.force === 'true' || req.query?.force === '1';
      if (!directory || !branch) {
        return res.status(400).json({ error: 'directory and branch are required' });
      }

      // Check cache (skip when force=true to allow manual refresh bypass)
      const cacheKey = `${directory}::${branch}::${remote}`;
      const cached = prStatusCache.get(cacheKey);
      if (!force && cached && Date.now() - cached.fetchedAt < PR_STATUS_CACHE_TTL_MS) {
        return res.json(cached.data);
      }

      // If GitHub recently rate-limited us, don't pile on more calls that will
      // also fail. Serve whatever we last cached (even if stale); otherwise
      // report a transient failure so the client keeps its last-known status.
      const { isGitHubRateLimited } = await import('./rate-limit.js');
      if (isGitHubRateLimited()) {
        if (cached) {
          return res.json(cached.data);
        }
        return res.status(503).json({ error: 'GitHub rate limited' });
      }

      // Intercept res.json to cache successful responses before sending
      // Only caches responses with connected:true — error/edge-case responses are not cached
      const originalJson = res.json.bind(res);
      res.json = (data) => {
        if (data && data.connected === true) {
          setPrStatusCache(cacheKey, data, Date.now());
        }
        return originalJson(data);
      };

      const { getOctokitOrNull, getGitHubAuth } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const { resolveGitHubPrStatus } = await import('./pr-status.js');
      const resolvedStatus = await withTimeout(
        resolveGitHubPrStatus({
          octokit,
          directory,
          branch,
          remoteName: remote,
        }),
        PR_STATUS_RESOLVE_TIMEOUT_MS,
        'resolveGitHubPrStatus',
      );
      const searchRepo = resolvedStatus.repo;
      const first = resolvedStatus.pr;
      if (!searchRepo) {
        return res.json({ connected: true, repo: null, branch, pr: null, checks: null, canMerge: false, defaultBranch: null, resolvedRemoteName: null });
      }
      if (!first) {
        return res.json({ connected: true, repo: searchRepo, branch, pr: null, checks: null, canMerge: false, defaultBranch: resolvedStatus.defaultBranch ?? null, resolvedRemoteName: resolvedStatus.resolvedRemoteName ?? null });
      }

      // Enrich with mergeability fields
      const prFull = await octokit.rest.pulls.get({ owner: searchRepo.owner, repo: searchRepo.repo, pull_number: first.number });
      const prData = prFull?.data;
      if (!prData) {
        return res.json({ connected: true, repo: searchRepo, branch, pr: null, checks: null, canMerge: false });
      }

      // Checks summary: prefer check-runs (Actions), fallback to classic statuses.
      let checks = null;
      const sha = prData.head?.sha;
      if (sha) {
        try {
          const runs = await octokit.rest.checks.listForRef({
            owner: searchRepo.owner,
            repo: searchRepo.repo,
            ref: sha,
            per_page: 100,
          });
          const checkRuns = Array.isArray(runs?.data?.check_runs) ? runs.data.check_runs : [];
          if (checkRuns.length > 0) {
            const counts = { success: 0, failure: 0, pending: 0 };
            for (const run of checkRuns) {
              const status = run?.status;
              const conclusion = run?.conclusion;
              if (status === 'queued' || status === 'in_progress') {
                counts.pending += 1;
                continue;
              }
              if (!conclusion) {
                counts.pending += 1;
                continue;
              }
              if (conclusion === 'success' || conclusion === 'neutral' || conclusion === 'skipped') {
                counts.success += 1;
              } else {
                counts.failure += 1;
              }
            }
            const total = counts.success + counts.failure + counts.pending;
            const state = counts.failure > 0
              ? 'failure'
              : (counts.pending > 0 ? 'pending' : (total > 0 ? 'success' : 'unknown'));
            checks = { state, total, ...counts };
          }
        } catch {
          // ignore and fall back
        }

        if (!checks) {
          try {
            const combined = await octokit.rest.repos.getCombinedStatusForRef({
              owner: searchRepo.owner,
              repo: searchRepo.repo,
              ref: sha,
            });
            const statuses = Array.isArray(combined?.data?.statuses) ? combined.data.statuses : [];
            const counts = { success: 0, failure: 0, pending: 0 };
            statuses.forEach((s) => {
              if (s.state === 'success') counts.success += 1;
              else if (s.state === 'failure' || s.state === 'error') counts.failure += 1;
              else if (s.state === 'pending') counts.pending += 1;
            });
            const total = counts.success + counts.failure + counts.pending;
            const state = counts.failure > 0
              ? 'failure'
              : (counts.pending > 0 ? 'pending' : (total > 0 ? 'success' : 'unknown'));
            checks = { state, total, ...counts };
          } catch {
            checks = null;
          }
        }
      }

      // Permission check (best-effort)
      let canMerge = false;
      try {
        const auth = getGitHubAuth();
        const username = auth?.user?.login;
        if (username) {
          const perm = await octokit.rest.repos.getCollaboratorPermissionLevel({
            owner: searchRepo.owner,
            repo: searchRepo.repo,
            username,
          });
          const level = perm?.data?.permission;
          canMerge = level === 'admin' || level === 'maintain' || level === 'write';
        }
      } catch {
        canMerge = false;
      }

       const isMerged = Boolean(prData.merged || prData.merged_at);
       const mergedState = isMerged ? 'merged' : (prData.state === 'closed' ? 'closed' : 'open');

      return res.json({
        connected: true,
        repo: searchRepo,
        branch,
        pr: {
          number: prData.number,
          title: prData.title,
          body: prData.body || '',
          url: prData.html_url,
          state: mergedState,
          draft: Boolean(prData.draft),
          base: prData.base?.ref,
          head: prData.head?.ref,
          headSha: prData.head?.sha,
          mergeable: prData.mergeable,
          mergeableState: prData.mergeable_state,
        },
        checks,
        canMerge,
        defaultBranch: resolvedStatus.defaultBranch ?? null,
        resolvedRemoteName: resolvedStatus.resolvedRemoteName ?? null,
      });
    } catch (error) {
      if (error?.status === 401) {
        const { clearGitHubAuth } = await getGitHubLibraries();
        clearGitHubAuth();
        return res.json({ connected: false });
      }
      // Transient failures — a rate limit, or the overall resolve timeout
      // firing — are expected under heavy load and should not be logged as hard
      // errors. Record a rate-limit cooldown when applicable, then serve the
      // last cached status (even if stale) or a 503 so the client keeps its
      // last-known value instead of clearing the badge.
      const { noteIfGitHubRateLimit } = await import('./rate-limit.js');
      const wasRateLimited = noteIfGitHubRateLimit(error);
      const wasTimeout = error?.code === 'ETIMEDOUT';
      if (wasRateLimited || wasTimeout) {
        const dir = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
        const br = typeof req.query?.branch === 'string' ? req.query.branch.trim() : '';
        const rem = typeof req.query?.remote === 'string' ? req.query.remote.trim() : 'origin';
        const cached = prStatusCache.get(`${dir}::${br}::${rem}`);
        if (cached) {
          return res.json(cached.data);
        }
        return res.status(503).json({ error: wasRateLimited ? 'GitHub rate limited' : 'GitHub request timed out' });
      }
      if (isGitHubResourceUnavailable(error)) {
        return res.json({
          connected: true,
          repo: null,
          branch: typeof req.query?.branch === 'string' ? req.query.branch.trim() : '',
          pr: null,
          checks: null,
          canMerge: false,
          defaultBranch: null,
          resolvedRemoteName: null,
        });
      }
      console.error('Failed to load GitHub PR status:', error);
      return res.status(500).json({ error: error.message || 'Failed to load GitHub PR status' });
    }
  });

  app.post('/api/github/pr/create', async (req, res) => {
    try {
      const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
      const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
      const head = typeof req.body?.head === 'string' ? req.body.head.trim() : '';
      const requestedBase = typeof req.body?.base === 'string' ? req.body.base.trim() : '';
      const body = typeof req.body?.body === 'string' ? req.body.body : undefined;
      const draft = typeof req.body?.draft === 'boolean' ? req.body.draft : undefined;
      // remote = target repo (where PR is created, e.g., 'upstream' for forks)
      const remote = typeof req.body?.remote === 'string' ? req.body.remote.trim() : 'origin';
      // headRemote = source repo (where head branch lives, e.g., 'origin' for forks)
      const headRemote = typeof req.body?.headRemote === 'string' ? req.body.headRemote.trim() : '';
      // targetRepo = explicit target repo (alternative to remote, for auto-detected upstream)
      const targetRepo = req.body?.targetRepo && typeof req.body.targetRepo.owner === 'string' && typeof req.body.targetRepo.repo === 'string'
        ? { owner: req.body.targetRepo.owner.trim(), repo: req.body.targetRepo.repo.trim() }
        : null;
      if (!directory || !title || !head || !requestedBase) {
        return res.status(400).json({ error: 'directory, title, head, base are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.status(401).json({ error: 'GitHub not connected' });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      let repo;
      if (targetRepo) {
        repo = targetRepo;
      } else {
        const resolved = await resolveGitHubRepoFromDirectory(directory, remote);
        repo = resolved.repo;
      }
      if (!repo) {
        return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
      }

      const normalizeBranchRef = (value, remoteNames = new Set()) => {
        if (!value) {
          return value;
        }
        let normalized = value.trim();
        if (normalized.startsWith('refs/heads/')) {
          normalized = normalized.substring('refs/heads/'.length);
        }
        if (normalized.startsWith('heads/')) {
          normalized = normalized.substring('heads/'.length);
        }
        if (normalized.startsWith('remotes/')) {
          normalized = normalized.substring('remotes/'.length);
        }

        const slashIndex = normalized.indexOf('/');
        if (slashIndex > 0) {
          const maybeRemote = normalized.slice(0, slashIndex);
          if (remoteNames.has(maybeRemote)) {
            const withoutRemotePrefix = normalized.slice(slashIndex + 1).trim();
            if (withoutRemotePrefix) {
              normalized = withoutRemotePrefix;
            }
          }
        }

        return normalized;
      };

      // Determine the source remote for the head branch
      // Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
      let sourceRemote = headRemote;
      const { getStatus, getRemotes } = await import('../git/index.js');
      
      // If no explicit headRemote, check the branch's tracking info
      if (!sourceRemote) {
        const status = await getStatus(directory).catch(() => null);
        if (status?.tracking) {
          // tracking is like "gsxdsm/fix/multi-remote-branch-creation" or "origin/main"
          const trackingRemote = status.tracking.split('/')[0];
          if (trackingRemote) {
            sourceRemote = trackingRemote;
          }
        }
      }
      
      // Fallback: if targeting non-origin and no tracking info, try 'origin'
      if (!sourceRemote && remote !== 'origin') {
        sourceRemote = 'origin';
      }

      const remoteNames = new Set([remote]);
      const remotes = await getRemotes(directory).catch(() => []);
      for (const item of remotes) {
        if (item?.name) {
          remoteNames.add(item.name);
        }
      }
      if (sourceRemote) {
        remoteNames.add(sourceRemote);
      }

      const base = normalizeBranchRef(requestedBase, remoteNames);
      if (!base) {
        return res.status(400).json({ error: 'Invalid base branch name' });
      }

      // For fork workflows: we need to determine the correct head reference
      let headRef = head;
      let headRepo = null;
      
      if (sourceRemote) {
        // The branch is on a different remote than the target - this is a cross-repo PR
        const resolved = await resolveGitHubRepoFromDirectory(directory, sourceRemote);
        headRepo = resolved.repo;
        if (!headRepo) {
          return res.status(400).json({
            error: `Cannot resolve GitHub repo for remote "${sourceRemote}". Check that the remote URL is a valid GitHub repository.`,
          });
        }
        // Always use owner:branch format for cross-repo PRs
        // GitHub API requires this when head is from a different repo/fork
        if (headRepo.owner !== repo.owner || headRepo.repo !== repo.repo) {
          headRef = `${headRepo.owner}:${head}`;
        }
      }

      // For cross-repo PRs, verify the branch exists on the head repo first
      if (headRef.includes(':')) {
        const [headOwner] = headRef.split(':');
        const headRepoName = headRepo?.repo || repo.repo;
        
        if (headRepoName) {
          try {
            await octokit.rest.repos.getBranch({
              owner: headOwner,
              repo: headRepoName,
              branch: head,
            });
          } catch (branchError) {
            if (branchError?.status === 404) {
              return res.status(400).json({
                error: `Branch "${head}" not found on ${headOwner}/${headRepoName}. Please push your branch first: git push ${sourceRemote || 'origin'} ${head}`,
              });
            }
            // For other errors, continue - let the PR create attempt handle it
          }
        }
      }

      const created = await octokit.rest.pulls.create({
        owner: repo.owner,
        repo: repo.repo,
        title,
        head: headRef,
        base,
        ...(typeof body === 'string' ? { body } : {}),
        ...(typeof draft === 'boolean' ? { draft } : {}),
      });

      const pr = created?.data;
      if (!pr) {
        return res.status(500).json({ error: 'Failed to create PR' });
      }

      // Invalidate PR status cache so subsequent prStatus calls fetch fresh data
      const headBranch = head.includes(':') ? head.split(':')[1] || head : head;
      const createCacheKey = `${directory}::${headBranch}::${remote}`;
      prStatusCache.delete(createCacheKey);

      return res.json({
        number: pr.number,
        title: pr.title,
        body: pr.body || '',
        url: pr.html_url,
        state: pr.state === 'closed' ? 'closed' : 'open',
        draft: Boolean(pr.draft),
        base: pr.base?.ref,
        head: pr.head?.ref,
        headSha: pr.head?.sha,
        mergeable: pr.mergeable,
        mergeableState: pr.mergeable_state,
      });
    } catch (error) {
      console.error('Failed to create GitHub PR:', error);
      
      // Check for head validation error (common with fork PRs)
      const errorMessage = error.message || '';
      const isHeadValidationError = 
        errorMessage.includes('Validation Failed') && 
        errorMessage.includes('"field":"head"') &&
        errorMessage.includes('"code":"invalid"');
      
      if (isHeadValidationError) {
        return res.status(400).json({ 
          error: 'Unable to create PR: You must have write access to the source repository. Make sure you have pushed your branch to a repository you own (your fork), and that the branch exists on the remote.' 
        });
      }
      
      return res.status(500).json({ error: error.message || 'Failed to create GitHub PR' });
    }
  });

  app.post('/api/github/pr/update', async (req, res) => {
    try {
      const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
      const number = typeof req.body?.number === 'number' ? req.body.number : null;
      const title = typeof req.body?.title === 'string' ? req.body.title.trim() : '';
      const body = typeof req.body?.body === 'string' ? req.body.body : undefined;
      if (!directory || !number || !title) {
        return res.status(400).json({ error: 'directory, number, title are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.status(401).json({ error: 'GitHub not connected' });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      const { repo } = await resolveGitHubRepoFromDirectory(directory);
      if (!repo) {
        return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
      }

      let updated;
      try {
        updated = await octokit.rest.pulls.update({
          owner: repo.owner,
          repo: repo.repo,
          pull_number: number,
          title,
          ...(typeof body === 'string' ? { body } : {}),
        });
      } catch (error) {
        if (error?.status === 401) {
          return res.status(401).json({ error: 'GitHub not connected' });
        }
        if (error?.status === 403) {
          return res.status(403).json({ error: 'Not authorized to edit this PR' });
        }
        if (error?.status === 404) {
          return res.status(404).json({ error: 'PR not found in this repository' });
        }
        if (error?.status === 422) {
          const apiMessage = error?.response?.data?.message;
          const firstError = Array.isArray(error?.response?.data?.errors) && error.response.data.errors.length > 0
            ? (error.response.data.errors[0]?.message || error.response.data.errors[0]?.code)
            : null;
          const message = [apiMessage, firstError].filter(Boolean).join(' · ') || 'Invalid PR update payload';
          return res.status(422).json({ error: message });
        }
        throw error;
      }

      const pr = updated?.data;
      if (!pr) {
        return res.status(500).json({ error: 'Failed to update PR' });
      }

      return res.json({
        number: pr.number,
        title: pr.title,
        body: pr.body || '',
        url: pr.html_url,
        state: pr.merged_at ? 'merged' : (pr.state === 'closed' ? 'closed' : 'open'),
        draft: Boolean(pr.draft),
        base: pr.base?.ref,
        head: pr.head?.ref,
        headSha: pr.head?.sha,
        mergeable: pr.mergeable,
        mergeableState: pr.mergeable_state,
      });
    } catch (error) {
      console.error('Failed to update GitHub PR:', error);
      return res.status(500).json({ error: error.message || 'Failed to update GitHub PR' });
    }
  });

  app.post('/api/github/pr/merge', async (req, res) => {
    try {
      const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
      const number = typeof req.body?.number === 'number' ? req.body.number : null;
      const method = typeof req.body?.method === 'string' ? req.body.method : 'merge';
      if (!directory || !number) {
        return res.status(400).json({ error: 'directory and number are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.status(401).json({ error: 'GitHub not connected' });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      const { repo } = await resolveGitHubRepoFromDirectory(directory);
      if (!repo) {
        return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
      }

      try {
        const result = await octokit.rest.pulls.merge({
          owner: repo.owner,
          repo: repo.repo,
          pull_number: number,
          merge_method: method,
        });
        return res.json({ merged: Boolean(result?.data?.merged), message: result?.data?.message });
      } catch (error) {
        if (error?.status === 403) {
          return res.status(403).json({ error: 'Not authorized to merge this PR' });
        }
        if (error?.status === 405 || error?.status === 409) {
          return res.json({ merged: false, message: error?.message || 'PR not mergeable' });
        }
        throw error;
      }
    } catch (error) {
      console.error('Failed to merge GitHub PR:', error);
      return res.status(500).json({ error: error.message || 'Failed to merge GitHub PR' });
    }
  });

  app.post('/api/github/pr/ready', async (req, res) => {
    try {
      const directory = typeof req.body?.directory === 'string' ? req.body.directory.trim() : '';
      const number = typeof req.body?.number === 'number' ? req.body.number : null;
      if (!directory || !number) {
        return res.status(400).json({ error: 'directory and number are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.status(401).json({ error: 'GitHub not connected' });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      const { repo } = await resolveGitHubRepoFromDirectory(directory);
      if (!repo) {
        return res.status(400).json({ error: 'Unable to resolve GitHub repo from git remote' });
      }

      const pr = await octokit.rest.pulls.get({ owner: repo.owner, repo: repo.repo, pull_number: number });
      const nodeId = pr?.data?.node_id;
      if (!nodeId) {
        return res.status(500).json({ error: 'Failed to resolve PR node id' });
      }

      if (pr?.data?.draft === false) {
        return res.json({ ready: true });
      }

      try {
        await octokit.graphql(
          `mutation($pullRequestId: ID!) {\n  markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) {\n    pullRequest {\n      id\n      isDraft\n    }\n  }\n}`,
          { pullRequestId: nodeId }
        );
      } catch (error) {
        if (error?.status === 403) {
          return res.status(403).json({ error: 'Not authorized to mark PR ready' });
        }
        throw error;
      }

      return res.json({ ready: true });
    } catch (error) {
      console.error('Failed to mark PR ready:', error);
      return res.status(500).json({ error: error.message || 'Failed to mark PR ready' });
    }
  });

  // ================= GitHub Repo APIs =================

  app.get('/api/github/repo/upstream', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      if (!directory) {
        return res.status(400).json({ error: 'directory is required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false, isFork: false, upstream: null });
      }

      const { resolveRepoNetwork } = await import('./repo/fork-detection.js');
      const network = await resolveRepoNetwork(octokit, directory);

      if (!network || network.length <= 1) {
        return res.json({ connected: true, isFork: false, upstream: null });
      }

      const upstream = network.find((r) => r.source === 'upstream') || null;
      let defaultBranch = 'main';
      let defaultBranchSha = null;
      if (upstream) {
        try {
          const metadata = await octokit.rest.repos.get({ owner: upstream.owner, repo: upstream.repo });
          defaultBranch = metadata?.data?.default_branch || 'main';
          const ref = await octokit.rest.git.getRef({ owner: upstream.owner, repo: upstream.repo, ref: `heads/${defaultBranch}` });
          defaultBranchSha = ref?.data?.object?.sha || null;
        } catch {
          // Fall back if metadata/ref fetch fails
        }
      }

      // Check if a configured git remote points to the upstream repo
      let upstreamRemoteName = null;
      if (upstream) {
        try {
          const { getRemotes } = await import('../git/index.js');
          const { resolveGitHubRepoFromDirectory } = await import('./index.js');
          const remotes = await getRemotes(directory);
          for (const r of remotes) {
            if (r?.name) {
              const resolved = await resolveGitHubRepoFromDirectory(directory, r.name).catch(() => ({ repo: null }));
              if (resolved.repo && resolved.repo.owner === upstream.owner && resolved.repo.repo === upstream.repo) {
                upstreamRemoteName = r.name;
                break;
              }
            }
          }
        } catch {
          // Ignore errors finding remote name
        }
      }

      return res.json({
        connected: true,
        isFork: Boolean(upstream),
        upstream: upstream ? { owner: upstream.owner, repo: upstream.repo, url: upstream.url, defaultBranch, defaultBranchSha, remoteName: upstreamRemoteName } : null,
      });
    } catch (error) {
      console.error('Failed to detect upstream repo:', error);
      return res.status(500).json({ error: error.message || 'Failed to detect upstream repo' });
    }
  });

  app.get('/api/github/repo/branches', async (req, res) => {
    try {
      const owner = typeof req.query?.owner === 'string' ? req.query.owner.trim() : '';
      const repo = typeof req.query?.repo === 'string' ? req.query.repo.trim() : '';
      if (!owner || !repo) {
        return res.status(400).json({ error: 'owner and repo are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ branches: [] });
      }

      const branches = [];
      let page = 1;
      while (true) {
        const response = await octokit.rest.repos.listBranches({ owner, repo, per_page: 100, page });
        if (!response.data || response.data.length === 0) break;
        for (const branch of response.data) {
          branches.push(branch.name);
        }
        if (response.data.length < 100) break;
        page++;
      }

      return res.json({ branches });
    } catch (error) {
      console.error('Failed to fetch repo branches:', error);
      return res.status(500).json({ error: error.message || 'Failed to fetch repo branches' });
    }
  });

  // ================= GitHub Issue APIs =================

  app.get('/api/github/issues/list', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const page = typeof req.query?.page === 'string' ? Number(req.query.page) : 1;
      const searchQuery = typeof req.query?.query === 'string' ? req.query.query.trim() : '';
      if (!directory) {
        return res.status(400).json({ error: 'directory is required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      const { resolveRepoNetwork } = await import('./repo/fork-detection.js');

      const repoNetwork = await resolveRepoNetwork(octokit, directory);
      const { repo } = await resolveGitHubRepoFromDirectory(directory);
      if (!repo) {
        return res.json({ connected: true, repo: null, issues: [] });
      }

      const effectivePage = Number.isFinite(page) && page > 0 ? page : 1;
      const reposToQuery = repoNetwork || [{ ...repo, source: 'origin' }];

      const mapIssueSummary = (item, repoRef) => ({
        number: item.number,
        title: item.title,
        url: item.html_url,
        state: item.state === 'closed' ? 'closed' : 'open',
        author: item.user ? { login: item.user.login, id: item.user.id, avatarUrl: item.user.avatar_url } : null,
        labels: Array.isArray(item.labels)
          ? item.labels
              .map((label) => {
                if (typeof label === 'string') return null;
                const name = typeof label?.name === 'string' ? label.name : '';
                if (!name) return null;
                return { name, color: typeof label?.color === 'string' ? label.color : undefined };
              })
              .filter(Boolean)
          : [],
        sourceRepo: { owner: repoRef.owner, repo: repoRef.repo, source: repoRef.source },
      });

      if (searchQuery) {
        const repoQualifiers = reposToQuery
          .map((r) => `repo:${r.owner}/${r.repo}`)
          .join(' ');
        const q = `${repoQualifiers} ${searchQuery} type:issue state:open`;
        try {
          const searchResult = await octokit.rest.search.issuesAndPullRequests({
            q,
            per_page: 50,
            page: effectivePage,
          });
          const totalCount = searchResult.data.total_count;
          const items = Array.isArray(searchResult.data.items) ? searchResult.data.items : [];
          const issues = items
            .filter((item) => !item?.pull_request)
            .map((item) => {
              const repoFullName = (item.repository_url || '').replace('https://api.github.com/repos/', '');
              const matched = reposToQuery.find((r) => `${r.owner}/${r.repo}` === repoFullName);
              return mapIssueSummary(item, matched || reposToQuery[0]);
            });
          const fetchedCount = (effectivePage - 1) * 50 + items.length;
          const hasMore = fetchedCount < totalCount;
          return res.json({ connected: true, repo, issues, page: effectivePage, hasMore });
        } catch (error) {
          console.error('Failed to search GitHub issues:', error);
          return res.json({ connected: true, repo, issues: [], page: effectivePage, hasMore: false });
        }
      }

      const queryRepo = async (repoRef) => {
        try {
          const list = await octokit.rest.issues.listForRepo({
            owner: repoRef.owner,
            repo: repoRef.repo,
            state: 'open',
            per_page: 50,
            page: effectivePage,
          });
          const link = typeof list?.headers?.link === 'string' ? list.headers.link : '';
          const hasMore = /rel="next"/.test(link);
          const issues = (Array.isArray(list?.data) ? list.data : [])
            .filter((item) => !item?.pull_request)
            .map((item) => mapIssueSummary(item, repoRef));
          return { issues, hasMore };
        } catch (error) {
          console.warn(`Failed to list issues for ${repoRef.owner}/${repoRef.repo}:`, error?.message || error);
          return { issues: [], hasMore: false };
        }
      };

      const results = await Promise.all(reposToQuery.map(queryRepo));
      const allIssues = results.flatMap((r) => r.issues);
      const anyHasMore = results.some((r) => r.hasMore);

      return res.json({ connected: true, repo, issues: allIssues, page: effectivePage, hasMore: anyHasMore });
    } catch (error) {
      console.error('Failed to list GitHub issues:', error);
      return res.status(500).json({ error: error.message || 'Failed to list GitHub issues' });
    }
  });

  app.get('/api/github/issues/get', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const number = typeof req.query?.number === 'string' ? Number(req.query.number) : null;
      if (!directory || !number) {
        return res.status(400).json({ error: 'directory and number are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const requestedRepo = getRequestedRepo(req);
      const repo = await resolveRepoForRequest(octokit, directory, requestedRepo);
      if (!repo) {
        return res.json({ connected: true, repo: null, issue: null });
      }

      const result = await octokit.rest.issues.get({ owner: repo.owner, repo: repo.repo, issue_number: number });
      const issue = result?.data;
      if (!issue || issue.pull_request) {
        return res.status(400).json({ error: 'Not a GitHub issue' });
      }

      return res.json({
        connected: true,
        repo,
        issue: {
          number: issue.number,
          title: issue.title,
          url: issue.html_url,
          state: issue.state === 'closed' ? 'closed' : 'open',
          body: issue.body || '',
          createdAt: issue.created_at,
          updatedAt: issue.updated_at,
          author: issue.user ? { login: issue.user.login, id: issue.user.id, avatarUrl: issue.user.avatar_url } : null,
          assignees: Array.isArray(issue.assignees)
            ? issue.assignees
                .map((u) => (u ? { login: u.login, id: u.id, avatarUrl: u.avatar_url } : null))
                .filter(Boolean)
            : [],
          labels: Array.isArray(issue.labels)
            ? issue.labels
                .map((label) => {
                  if (typeof label === 'string') return null;
                  const name = typeof label?.name === 'string' ? label.name : '';
                  if (!name) return null;
                  return { name, color: typeof label?.color === 'string' ? label.color : undefined };
                })
                .filter(Boolean)
            : [],
        },
      });
    } catch (error) {
      console.error('Failed to fetch GitHub issue:', error);
      return res.status(500).json({ error: error.message || 'Failed to fetch GitHub issue' });
    }
  });

  app.get('/api/github/issues/comments', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const number = typeof req.query?.number === 'string' ? Number(req.query.number) : null;
      if (!directory || !number) {
        return res.status(400).json({ error: 'directory and number are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const requestedRepo = getRequestedRepo(req);
      const repo = await resolveRepoForRequest(octokit, directory, requestedRepo);
      if (!repo) {
        return res.json({ connected: true, repo: null, comments: [] });
      }

      const result = await octokit.rest.issues.listComments({
        owner: repo.owner,
        repo: repo.repo,
        issue_number: number,
        per_page: 100,
      });
      const comments = (Array.isArray(result?.data) ? result.data : [])
        .map((comment) => ({
          id: comment.id,
          url: comment.html_url,
          body: comment.body || '',
          createdAt: comment.created_at,
          updatedAt: comment.updated_at,
          author: comment.user ? { login: comment.user.login, id: comment.user.id, avatarUrl: comment.user.avatar_url } : null,
        }));

      return res.json({ connected: true, repo, comments });
    } catch (error) {
      console.error('Failed to fetch GitHub issue comments:', error);
      return res.status(500).json({ error: error.message || 'Failed to fetch GitHub issue comments' });
    }
  });

  // ================= GitHub Pull Request Context APIs =================

  app.get('/api/github/pulls/list', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const page = typeof req.query?.page === 'string' ? Number(req.query.page) : 1;
      const searchQuery = typeof req.query?.query === 'string' ? req.query.query.trim() : '';
      if (!directory) {
        return res.status(400).json({ error: 'directory is required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const { resolveGitHubRepoFromDirectory } = await import('./index.js');
      const { resolveRepoNetwork } = await import('./repo/fork-detection.js');

      const repoNetwork = await resolveRepoNetwork(octokit, directory);
      const { repo } = await resolveGitHubRepoFromDirectory(directory);
      if (!repo) {
        return res.json({ connected: true, repo: null, prs: [] });
      }

      const effectivePage = Number.isFinite(page) && page > 0 ? page : 1;
      const reposToQuery = repoNetwork || [{ ...repo, source: 'origin' }];

      const mapPrSummary = (pr, repoRef) => {
        const mergedState = pr.merged_at ? 'merged' : (pr.state === 'closed' ? 'closed' : 'open');
        const headRepo = pr.head?.repo
          ? {
              owner: pr.head.repo.owner?.login,
              repo: pr.head.repo.name,
              url: pr.head.repo.html_url,
              cloneUrl: pr.head.repo.clone_url,
              sshUrl: pr.head.repo.ssh_url,
            }
          : null;
        return {
          number: pr.number,
          title: pr.title,
          url: pr.html_url,
          state: mergedState,
          draft: Boolean(pr.draft),
          base: pr.base?.ref,
          head: pr.head?.ref,
          headSha: pr.head?.sha,
          mergeable: pr.mergeable,
          mergeableState: pr.mergeable_state,
          author: pr.user ? { login: pr.user.login, id: pr.user.id, avatarUrl: pr.user.avatar_url } : null,
          headLabel: pr.head?.label,
          headRepo: headRepo && headRepo.owner && headRepo.repo && headRepo.url
            ? headRepo
            : null,
          sourceRepo: { owner: repoRef.owner, repo: repoRef.repo, source: repoRef.source },
        };
      };

      if (searchQuery) {
        const repoQualifiers = reposToQuery
          .map((r) => `repo:${r.owner}/${r.repo}`)
          .join(' ');
        const q = `${repoQualifiers} ${searchQuery} type:pr state:open`;
        try {
          const searchResult = await octokit.rest.search.issuesAndPullRequests({
            q,
            per_page: 50,
            page: effectivePage,
          });
          const totalCount = searchResult.data.total_count;
          const items = Array.isArray(searchResult.data.items) ? searchResult.data.items : [];
          const findRepoForSearchItem = (item) => {
            const repositoryUrl = typeof item?.repository_url === 'string' ? item.repository_url : '';
            const match = repositoryUrl.match(/\/repos\/([^/]+)\/([^/]+)$/);
            if (!match) return reposToQuery[0];
            return reposToQuery.find((repoRef) => repoRef.owner === match[1] && repoRef.repo === match[2]) || reposToQuery[0];
          };
          const prRefs = items
            .map((item) => ({ number: item.number, repoRef: findRepoForSearchItem(item) }))
            .filter((ref) => Number.isFinite(ref.number) && ref.number > 0 && ref.repoRef);
          let prs;
          if (prRefs.length === 0) {
            prs = [];
          } else {
            const results = await Promise.all(prRefs.map(async ({ number, repoRef }) => {
              try {
                const pr = await octokit.rest.pulls.get({
                  owner: repoRef.owner,
                  repo: repoRef.repo,
                  pull_number: number,
                });
                return mapPrSummary(pr.data, repoRef);
              } catch {
                return null;
              }
            }));
            prs = results.filter(Boolean);
          }
          const fetchedCount = (effectivePage - 1) * 50 + items.length;
          const hasMore = fetchedCount < totalCount;
          return res.json({ connected: true, repo, prs, page: effectivePage, hasMore });
        } catch (error) {
          console.error('Failed to search GitHub PRs:', error);
          return res.json({ connected: true, repo, prs: [], page: effectivePage, hasMore: false });
        }
      }

      const queryRepo = async (repoRef) => {
        try {
          const list = await octokit.rest.pulls.list({
            owner: repoRef.owner,
            repo: repoRef.repo,
            state: 'open',
            per_page: 50,
            page: effectivePage,
          });
          const link = typeof list?.headers?.link === 'string' ? list.headers.link : '';
          const hasMore = /rel="next"/.test(link);
          const prs = (Array.isArray(list?.data) ? list.data : []).map((pr) => mapPrSummary(pr, repoRef));
          return { prs, hasMore };
        } catch (error) {
          console.warn(`Failed to list PRs for ${repoRef.owner}/${repoRef.repo}:`, error?.message || error);
          return { prs: [], hasMore: false };
        }
      };

      const results = await Promise.all(reposToQuery.map(queryRepo));
      const allPrs = results.flatMap((r) => r.prs);
      const anyHasMore = results.some((r) => r.hasMore);

      return res.json({ connected: true, repo, prs: allPrs, page: effectivePage, hasMore: anyHasMore });
    } catch (error) {
      if (error?.status === 401) {
        const { clearGitHubAuth } = await getGitHubLibraries();
        clearGitHubAuth();
        return res.json({ connected: false });
      }
      console.error('Failed to list GitHub pull requests:', error);
      return res.status(500).json({ error: error.message || 'Failed to list GitHub pull requests' });
    }
  });

  app.get('/api/github/pulls/context', async (req, res) => {
    try {
      const directory = typeof req.query?.directory === 'string' ? req.query.directory.trim() : '';
      const number = typeof req.query?.number === 'string' ? Number(req.query.number) : null;
      const includeDiff = req.query?.diff === '1' || req.query?.diff === 'true';
      const includeCheckDetails = req.query?.checkDetails === '1' || req.query?.checkDetails === 'true';
      if (!directory || !number) {
        return res.status(400).json({ error: 'directory and number are required' });
      }

      const { getOctokitOrNull } = await getGitHubLibraries();
      const octokit = getOctokitOrNull();
      if (!octokit) {
        return res.json({ connected: false });
      }

      const requestedRepo = getRequestedRepo(req);
      const repo = await resolveRepoForRequest(octokit, directory, requestedRepo);
      if (!repo) {
        return res.json({ connected: true, repo: null, pr: null });
      }

      const prResp = await octokit.rest.pulls.get({ owner: repo.owner, repo: repo.repo, pull_number: number });
      const prData = prResp?.data;
      if (!prData) {
        return res.status(404).json({ error: 'PR not found' });
      }

      const headRepo = prData.head?.repo
        ? {
            owner: prData.head.repo.owner?.login,
            repo: prData.head.repo.name,
            url: prData.head.repo.html_url,
            cloneUrl: prData.head.repo.clone_url,
            sshUrl: prData.head.repo.ssh_url,
          }
        : null;

      const mergedState = prData.merged ? 'merged' : (prData.state === 'closed' ? 'closed' : 'open');
      const pr = {
        number: prData.number,
        title: prData.title,
        url: prData.html_url,
        state: mergedState,
        draft: Boolean(prData.draft),
        base: prData.base?.ref,
        head: prData.head?.ref,
        headSha: prData.head?.sha,
        mergeable: prData.mergeable,
        mergeableState: prData.mergeable_state,
        author: prData.user ? { login: prData.user.login, id: prData.user.id, avatarUrl: prData.user.avatar_url } : null,
        headLabel: prData.head?.label,
        headRepo: headRepo && headRepo.owner && headRepo.repo && headRepo.url ? headRepo : null,
        body: prData.body || '',
        createdAt: prData.created_at,
        updatedAt: prData.updated_at,
      };

      const issueCommentsResp = await octokit.rest.issues.listComments({
        owner: repo.owner,
        repo: repo.repo,
        issue_number: number,
        per_page: 100,
      });
      const issueComments = (Array.isArray(issueCommentsResp?.data) ? issueCommentsResp.data : []).map((comment) => ({
        id: comment.id,
        url: comment.html_url,
        body: comment.body || '',
        createdAt: comment.created_at,
        updatedAt: comment.updated_at,
        author: comment.user ? { login: comment.user.login, id: comment.user.id, avatarUrl: comment.user.avatar_url } : null,
      }));

      const reviewCommentsResp = await octokit.rest.pulls.listReviewComments({
        owner: repo.owner,
        repo: repo.repo,
        pull_number: number,
        per_page: 100,
      });
      const reviewComments = (Array.isArray(reviewCommentsResp?.data) ? reviewCommentsResp.data : []).map((comment) => ({
        id: comment.id,
        url: comment.html_url,
        body: comment.body || '',
        createdAt: comment.created_at,
        updatedAt: comment.updated_at,
        path: comment.path,
        line: typeof comment.line === 'number' ? comment.line : null,
        position: typeof comment.position === 'number' ? comment.position : null,
        author: comment.user ? { login: comment.user.login, id: comment.user.id, avatarUrl: comment.user.avatar_url } : null,
      }));

      const filesResp = await octokit.rest.pulls.listFiles({
        owner: repo.owner,
        repo: repo.repo,
        pull_number: number,
        per_page: 100,
      });
      const files = (Array.isArray(filesResp?.data) ? filesResp.data : []).map((f) => ({
        filename: f.filename,
        status: f.status,
        additions: f.additions,
        deletions: f.deletions,
        changes: f.changes,
        patch: f.patch,
      }));

      // checks summary (same logic as status endpoint)
      let checks = null;
      let checkRunsOut = undefined;
      const sha = prData.head?.sha;
      if (sha) {
        try {
          const runs = await octokit.rest.checks.listForRef({ owner: repo.owner, repo: repo.repo, ref: sha, per_page: 100 });
          const checkRuns = Array.isArray(runs?.data?.check_runs) ? runs.data.check_runs : [];
          if (checkRuns.length > 0) {
            const parsedJobs = new Map();
            const parsedAnnotations = new Map();
            if (includeCheckDetails) {
              // Prefetch actions jobs per runId.
              const runIds = new Set();
              const jobIds = new Map();
              for (const run of checkRuns) {
                const details = typeof run.details_url === 'string' ? run.details_url : '';
                const match = details.match(/\/actions\/runs\/(\d+)(?:\/job\/(\d+))?/);
                if (match) {
                  const runId = Number(match[1]);
                  const jobId = match[2] ? Number(match[2]) : null;
                  if (Number.isFinite(runId) && runId > 0) {
                    runIds.add(runId);
                    if (jobId && Number.isFinite(jobId) && jobId > 0) {
                      jobIds.set(details, { runId, jobId });
                    } else {
                      jobIds.set(details, { runId, jobId: null });
                    }
                  }
                }
              }

              for (const runId of runIds) {
                try {
                  const jobsResp = await octokit.rest.actions.listJobsForWorkflowRun({
                    owner: repo.owner,
                    repo: repo.repo,
                    run_id: runId,
                    per_page: 100,
                  });
                  const jobs = Array.isArray(jobsResp?.data?.jobs) ? jobsResp.data.jobs : [];
                  parsedJobs.set(runId, jobs);
                } catch {
                  parsedJobs.set(runId, []);
                }
              }

              for (const run of checkRuns) {
                const runConclusion = typeof run?.conclusion === 'string' ? run.conclusion.toLowerCase() : '';
                const shouldLoadAnnotations = Boolean(
                  run?.id
                  && runConclusion
                  && !['success', 'neutral', 'skipped'].includes(runConclusion)
                );
                if (!shouldLoadAnnotations) {
                  continue;
                }

                const checkRunId = Number(run.id);
                if (!Number.isFinite(checkRunId) || checkRunId <= 0) {
                  continue;
                }

                const annotations = [];
                for (let page = 1; page <= 3; page += 1) {
                  try {
                    const annotationsResp = await octokit.rest.checks.listAnnotations({
                      owner: repo.owner,
                      repo: repo.repo,
                      check_run_id: checkRunId,
                      per_page: 50,
                      page,
                    });
                    const chunk = Array.isArray(annotationsResp?.data) ? annotationsResp.data : [];
                    annotations.push(...chunk);
                    if (chunk.length < 50) {
                      break;
                    }
                  } catch {
                    break;
                  }
                }

                if (annotations.length > 0) {
                  parsedAnnotations.set(checkRunId, annotations);
                }
              }
            }

            checkRunsOut = checkRuns.map((run) => {
              const detailsUrl = typeof run.details_url === 'string' ? run.details_url : undefined;
              let job = undefined;
              if (includeCheckDetails && detailsUrl) {
                const match = detailsUrl.match(/\/actions\/runs\/(\d+)(?:\/job\/(\d+))?/);
                const runId = match ? Number(match[1]) : null;
                const jobId = match && match[2] ? Number(match[2]) : null;
                if (runId && Number.isFinite(runId)) {
                  const jobs = parsedJobs.get(runId) || [];
                  const matched = jobId
                    ? jobs.find((j) => j.id === jobId)
                    : null;
                  const picked = matched || jobs.find((j) => j.name === run.name) || null;
                  if (picked) {
                    job = {
                      runId,
                      jobId: picked.id,
                      url: picked.html_url,
                      name: picked.name,
                      conclusion: picked.conclusion,
                          steps: Array.isArray(picked.steps)
                            ? picked.steps.map((s) => ({
                                name: s.name,
                                status: s.status,
                                conclusion: s.conclusion,
                                number: s.number,
                                startedAt: s.started_at || undefined,
                                completedAt: s.completed_at || undefined,
                              }))
                            : undefined,
                    };
                  } else {
                    job = { runId, ...(jobId ? { jobId } : {}), url: detailsUrl };
                  }
                }
              }

              return {
                id: run.id,
                name: run.name,
                app: run.app
                  ? {
                      name: run.app.name || undefined,
                      slug: run.app.slug || undefined,
                    }
                  : undefined,
                status: run.status,
                conclusion: run.conclusion,
                detailsUrl,
                output: run.output
                  ? {
                      title: run.output.title || undefined,
                      summary: run.output.summary || undefined,
                      text: run.output.text || undefined,
                    }
                  : undefined,
                ...(job ? { job } : {}),
                ...(run.id && parsedAnnotations.has(run.id)
                  ? {
                      annotations: parsedAnnotations.get(run.id).map((a) => ({
                        path: a.path || undefined,
                        startLine: typeof a.start_line === 'number' ? a.start_line : undefined,
                        endLine: typeof a.end_line === 'number' ? a.end_line : undefined,
                        level: a.annotation_level || undefined,
                        message: a.message || '',
                        title: a.title || undefined,
                        rawDetails: a.raw_details || undefined,
                      })).filter((a) => a.message),
                    }
                  : {}),
              };
            });
            const counts = { success: 0, failure: 0, pending: 0 };
            for (const run of checkRuns) {
              const status = run?.status;
              const conclusion = run?.conclusion;
              if (status === 'queued' || status === 'in_progress') {
                counts.pending += 1;
                continue;
              }
              if (!conclusion) {
                counts.pending += 1;
                continue;
              }
              if (conclusion === 'success' || conclusion === 'neutral' || conclusion === 'skipped') {
                counts.success += 1;
              } else {
                counts.failure += 1;
              }
            }
            const total = counts.success + counts.failure + counts.pending;
            const state = counts.failure > 0 ? 'failure' : (counts.pending > 0 ? 'pending' : (total > 0 ? 'success' : 'unknown'));
            checks = { state, total, ...counts };
          }
        } catch {
          // ignore and fall back
        }
        if (!checks) {
          try {
            const combined = await octokit.rest.repos.getCombinedStatusForRef({ owner: repo.owner, repo: repo.repo, ref: sha });
            const statuses = Array.isArray(combined?.data?.statuses) ? combined.data.statuses : [];
            const counts = { success: 0, failure: 0, pending: 0 };
            statuses.forEach((s) => {
              if (s.state === 'success') counts.success += 1;
              else if (s.state === 'failure' || s.state === 'error') counts.failure += 1;
              else if (s.state === 'pending') counts.pending += 1;
            });
            const total = counts.success + counts.failure + counts.pending;
            const state = counts.failure > 0 ? 'failure' : (counts.pending > 0 ? 'pending' : (total > 0 ? 'success' : 'unknown'));
            checks = { state, total, ...counts };
          } catch {
            checks = null;
          }
        }
      }

      let diff = undefined;
      if (includeDiff) {
        const diffResp = await octokit.request('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
          owner: repo.owner,
          repo: repo.repo,
          pull_number: number,
          headers: { accept: 'application/vnd.github.v3.diff' },
        });
        diff = typeof diffResp?.data === 'string' ? diffResp.data : undefined;
      }

      return res.json({
        connected: true,
        repo,
        pr,
        issueComments,
        reviewComments,
        files,
        ...(diff ? { diff } : {}),
        checks,
        ...(Array.isArray(checkRunsOut) ? { checkRuns: checkRunsOut } : {}),
      });
    } catch (error) {
      if (error?.status === 401) {
        const { clearGitHubAuth } = await getGitHubLibraries();
        clearGitHubAuth();
        return res.json({ connected: false });
      }
      console.error('Failed to load GitHub PR context:', error);
      return res.status(500).json({ error: error.message || 'Failed to load GitHub PR context' });
    }
  });
}
