/**
 * engine/gh-token.js — Per-slug GitHub PAT resolution.
 *
 * Resolves the PAT for a given owner/repo slug by consulting
 * `config.engine.ghAccounts` and shelling out to `gh auth token --user <account>
 * --hostname github.com`. Threading the resolved token via `GH_TOKEN` lets
 * `gh` calls hit the right account WITHOUT touching the global active account.
 *
 * Lookup chain for a slug `<owner>/<repo>`:
 *   1. `engine.ghAccounts[<owner>]` — exact owner match.
 *   2. `engine.ghAccounts[<owner>/* ]` — owner glob (optional).
 *   3. `engine.ghAccounts['*']` — fleet default.
 *   4. null → caller falls back to the ambient `gh` identity, preserving
 *      legacy behavior for unmapped slugs.
 *
 * Mirrors the shape of `engine/ado-token.js` (separate sync token acquisition
 * helper, in-memory cache, narrow exec timeout). Tests can prime the cache
 * via `_setTokenForTest(slug, token)` and clear it via `_clearTokenCache()`.
 */

const { execFileSync } = require('child_process');
const path = require('path');
const shared = require('./shared');
const { safeJson, MINIONS_DIR, log } = shared;

const TOKEN_TTL_MS = 30 * 60 * 1000; // 30 minutes
const FETCH_TIMEOUT_MS = 10000;       // 10s — same ceiling as `gh api user`
const MAX_BUFFER = 1024 * 1024;       // 1 MB — token output is ~80 chars
const CONFIG_CACHE_MS = 30 * 1000;    // 30s — re-read config periodically without hammering fs

const _accountTokens = new Map(); // accountName → { token, expiresAt }
const _slugTokenOverrides = new Map(); // slug → token (test seam)

let _cachedConfig = null;
let _cachedConfigAt = 0;

function _readConfig(opts = {}) {
  if (opts.config) return opts.config;
  if (_cachedConfig && (Date.now() - _cachedConfigAt) < CONFIG_CACHE_MS) {
    return _cachedConfig;
  }
  const configPath = path.join(MINIONS_DIR, 'config.json');
  _cachedConfig = safeJson(configPath) || {};
  _cachedConfigAt = Date.now();
  return _cachedConfig;
}

/**
 * Resolve the configured gh account name for a slug, or null when no mapping
 * applies. Mapping precedence: exact owner > owner-glob > fleet default ('*').
 */
function resolveAccountForSlug(slug, opts = {}) {
  if (!slug || typeof slug !== 'string') return null;
  const owner = slug.split('/')[0];
  if (!owner) return null;

  const config = _readConfig(opts);
  const accounts = (config && config.engine && config.engine.ghAccounts) || {};
  if (!accounts || typeof accounts !== 'object') return null;

  if (accounts[owner]) return String(accounts[owner]);
  const glob = `${owner}/*`;
  if (accounts[glob]) return String(accounts[glob]);
  if (accounts['*']) return String(accounts['*']);
  return null;
}

function _fetchTokenForAccount(account, opts = {}) {
  if (!account) return null;
  const cached = _accountTokens.get(account);
  if (cached && cached.expiresAt > Date.now()) return cached.token;

  const run = opts.execFileSync || execFileSync;
  try {
    // Argv-array form: `account` is passed as a literal argument and never
    // interpreted by a shell, so shell metacharacters in the configured
    // account name (e.g. `;`, backticks, `$()`) cannot be executed.
    const out = run('gh', ['auth', 'token', '--user', account, '--hostname', 'github.com'], {
      timeout: FETCH_TIMEOUT_MS,
      encoding: 'utf8',
      windowsHide: true,
      maxBuffer: MAX_BUFFER,
    });
    const token = String(out || '').trim();
    if (!token) {
      log('warn', `gh-token: empty token returned for account "${account}"`);
      return null;
    }
    _accountTokens.set(account, { token, expiresAt: Date.now() + TOKEN_TTL_MS });
    return token;
  } catch (e) {
    log('warn', `gh-token: failed to fetch token for "${account}": ${e?.message || e}`);
    return null;
  }
}

/**
 * Resolve a GitHub PAT for `slug` (e.g. `opg-microsoft/minions`).
 * Returns the token string on success, or null when no mapping applies and the
 * caller should fall back to the ambient `gh` identity.
 *
 * Test seam: `_setTokenForTest(slug, token)` short-circuits the entire chain
 * so unit tests do not have to mock execFileSync nor stand up a config file.
 */
function resolveTokenForSlug(slug, opts = {}) {
  if (slug && _slugTokenOverrides.has(slug)) return _slugTokenOverrides.get(slug);
  const account = resolveAccountForSlug(slug, opts);
  if (!account) return null;
  return _fetchTokenForAccount(account, opts);
}

/** Test-only: prime a slug→token override that bypasses config + shell-out. */
function _setTokenForTest(slug, token) {
  if (!slug) return;
  if (token == null) _slugTokenOverrides.delete(slug);
  else _slugTokenOverrides.set(slug, String(token));
}

/** Test-only: drop every cache so the next resolve goes through the real path. */
function _clearTokenCache() {
  _accountTokens.clear();
  _slugTokenOverrides.clear();
  _cachedConfig = null;
  _cachedConfigAt = 0;
}

module.exports = {
  resolveTokenForSlug,
  resolveAccountForSlug,
  _setTokenForTest,
  _clearTokenCache,
  // Constants exposed for tests + diagnostics.
  TOKEN_TTL_MS,
  FETCH_TIMEOUT_MS,
};
