/**
 * In-memory cache for GitHub PR data with TTL and concurrent-fetch coalescing.
 *
 * @module pr-cache
 */

const GH_API_BASE = 'https://api.github.com';
const TTL_MS = 30_000;

/**
 * Parse owner, repo, and PR number from a GitHub PR URL.
 *
 * @param {string} prUrl e.g. https://github.com/owner/repo/pull/123
 * @returns {{ owner: string, repo: string, number: number } | null}
 */
function parsePrUrl(prUrl) {
  const m = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(prUrl);
  if (!m) return null;
  return { owner: m[1], repo: m[2], number: parseInt(m[3], 10) };
}

/**
 * Reduce an array of check runs into a single status string.
 *
 * @param {Array<{conclusion: string|null, status: string}>} checkRuns
 * @returns {'pending'|'passing'|'failing'|null}
 */
function reduceCheckRuns(checkRuns) {
  if (!checkRuns || checkRuns.length === 0) return null;

  let hasFailure = false;
  let hasPending = false;

  for (const run of checkRuns) {
    const conclusion = run.conclusion;
    const status = run.status;

    if (
      conclusion === 'failure' ||
      conclusion === 'timed_out' ||
      conclusion === 'action_required'
    ) {
      hasFailure = true;
    } else if (
      status === 'queued' ||
      status === 'in_progress' ||
      conclusion === null
    ) {
      hasPending = true;
    }
  }

  if (hasFailure) return 'failing';
  if (hasPending) return 'pending';
  return 'passing';
}

/**
 * @typedef {object} PrCacheEntry
 * @property {number} fetchedAt
 * @property {'open'|'merged'|'closed'|null} prState
 * @property {number|null} prNumber
 * @property {'pending'|'passing'|'failing'|null} prChecks
 * @property {Promise<PrData>|null} promise
 */

/**
 * @typedef {object} PrData
 * @property {'open'|'merged'|'closed'|null} state
 * @property {number|null} number
 * @property {'pending'|'passing'|'failing'|null} checks
 */

/**
 * Fetch PR data from GitHub API.
 *
 * @param {string} prUrl
 * @param {() => Promise<string|null>} getToken
 * @returns {Promise<PrData>}
 */
async function fetchPrData(prUrl, getToken) {
  const parsed = parsePrUrl(prUrl);
  if (!parsed) return { state: null, number: null, checks: null };

  const token = await getToken();
  /** @type {HeadersInit} */
  const headers = { Accept: 'application/vnd.github+json' };
  if (token) headers['Authorization'] = `token ${token}`;

  // Fetch PR metadata
  const prResp = await fetch(
    `${GH_API_BASE}/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`,
    { headers, signal: AbortSignal.timeout(10_000) },
  );
  if (!prResp.ok) {
    return { state: null, number: parsed.number, checks: null };
  }
  const prData = await prResp.json();

  let state = prData.state ?? null;
  if (state === 'closed' && prData.merged_at) state = 'merged';

  const sha = prData.head?.sha ?? null;
  if (!sha) {
    return { state, number: parsed.number, checks: null };
  }

  // Fetch check runs for the head SHA
  let checks = null;
  try {
    const checksResp = await fetch(
      `${GH_API_BASE}/repos/${parsed.owner}/${parsed.repo}/commits/${sha}/check-runs`,
      { headers, signal: AbortSignal.timeout(10_000) },
    );
    if (checksResp.ok) {
      const checksData = await checksResp.json();
      const runs = Array.isArray(checksData.check_runs) ? checksData.check_runs : [];
      checks = reduceCheckRuns(runs);
    }
  } catch {
    // Non-fatal — return null checks
  }

  return { state, number: parsed.number, checks };
}

/**
 * Create a PR data cache with TTL and concurrent-fetch coalescing.
 *
 * @returns {{ getPr: (prUrl: string, getToken: () => Promise<string|null>) => Promise<PrData|null>, deletePr: (prUrl: string) => void }}
 */
export function createPrCache() {
  /** @type {Map<string, PrCacheEntry>} */
  const cache = new Map();

  /**
   * Get PR data for a URL, using cache if fresh or coalescing concurrent fetches.
   *
   * @param {string} prUrl
   * @param {() => Promise<string|null>} getToken
   * @returns {Promise<PrData|null>}
   */
  async function getPr(prUrl, getToken) {
    if (!prUrl) return null;

    const now = Date.now();
    const entry = cache.get(prUrl);

    // Fresh cache hit
    if (entry && !entry.promise && now - entry.fetchedAt < TTL_MS) {
      return { state: entry.prState, number: entry.prNumber, checks: entry.prChecks };
    }

    // In-flight fetch — coalesce
    if (entry && entry.promise) {
      try {
        return await entry.promise;
      } catch {
        return null;
      }
    }

    // Stale or missing — start new fetch
    const promise = fetchPrData(prUrl, getToken).then(
      (data) => {
        cache.set(prUrl, {
          fetchedAt: Date.now(),
          prState: data.state,
          prNumber: data.number,
          prChecks: data.checks,
          promise: null,
        });
        return data;
      },
      (err) => {
        // Clear promise on error so next call retries
        const current = cache.get(prUrl);
        if (current && current.promise) {
          cache.set(prUrl, { ...current, promise: null });
        }
        throw err;
      },
    );

    cache.set(prUrl, {
      fetchedAt: entry ? entry.fetchedAt : 0,
      prState: entry ? entry.prState : null,
      prNumber: entry ? entry.prNumber : null,
      prChecks: entry ? entry.prChecks : null,
      promise,
    });

    try {
      return await promise;
    } catch {
      return null;
    }
  }

  /**
   * Evict a PR entry from the cache (call on world destroy).
   *
   * @param {string} prUrl
   */
  function deletePr(prUrl) {
    cache.delete(prUrl);
  }

  return { getPr, deletePr };
}
