/**
 * engine/issues.js — GitHub issue creation helpers.
 */

const fs = require('fs');
const path = require('path');
const { execFileSync: _execFileSync } = require('child_process');
const shared = require('./shared');
const ghToken = require('./gh-token');

const DEFAULT_REPO = 'opg-microsoft/minions';
const DEFAULT_LABELS = ['bug'];
const WRITABLE_REPO_PERMISSIONS = new Set(['WRITE', 'MAINTAIN', 'ADMIN']);

class GitHubIssueError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.name = 'GitHubIssueError';
    this.statusCode = statusCode;
  }
}

function normalizeLabels(labels, defaultLabels = DEFAULT_LABELS) {
  let raw;
  if (labels == null) raw = defaultLabels;
  else if (Array.isArray(labels)) raw = labels;
  else if (typeof labels === 'string') raw = labels.split(',');
  else raw = [];

  const seen = new Set();
  const out = [];
  for (const label of raw) {
    const value = String(label || '').trim();
    if (!value) continue;
    const key = value.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(value);
  }
  return out;
}

function ghMessage(err) {
  if (!err) return '';
  return [err.message, err.stdout, err.stderr]
    .filter(Boolean)
    .map(String)
    .join('\n');
}

function conciseGhMessage(errOrText) {
  const text = typeof errOrText === 'string' ? errOrText : ghMessage(errOrText);
  return text.replace(/\s+/g, ' ').trim().slice(0, 240);
}

function isAuthError(errOrText) {
  return /(authentication|auth login|not authenticated|bad credentials|http 401|requires authentication)/i.test(
    typeof errOrText === 'string' ? errOrText : ghMessage(errOrText)
  );
}

function isLabelUnavailableError(errOrText) {
  const msg = typeof errOrText === 'string' ? errOrText : ghMessage(errOrText);
  return /label/i.test(msg) && /(not found|not exist|unavailable|invalid|could not add|could not resolve|does not exist)/i.test(msg);
}

function extractIssueUrl(output) {
  const match = String(output || '').match(/https:\/\/github\.com\/\S+\/issues\/\d+/);
  return match ? match[0] : null;
}

function runGh(execFileSync, args, timeout, { env } = {}) {
  const opts = {
    encoding: 'utf8',
    timeout,
    windowsHide: true,
  };
  if (env) opts.env = { ...process.env, ...env };
  return execFileSync('gh', args, opts);
}

function isWritableRepoPermission(permission) {
  return WRITABLE_REPO_PERMISSIONS.has(String(permission || '').trim().toUpperCase());
}

function repoViewerPermission({ repo, execFileSync, ghEnv }) {
  const output = runGh(execFileSync, ['repo', 'view', repo, '--json', 'viewerPermission'], 10000, { env: ghEnv });
  const parsed = JSON.parse(output || '{}');
  return String(parsed.viewerPermission || '').trim().toUpperCase();
}

function parseGhAuthUsers(output) {
  const users = [];
  const seen = new Set();
  for (const line of String(output || '').split(/\r?\n/)) {
    const match = line.match(/\baccount\s+([^\s()]+)/i) || line.match(/\bas\s+([^\s()]+)/i);
    if (!match) continue;
    const user = match[1].trim();
    const key = user.toLowerCase();
    if (!user || seen.has(key)) continue;
    seen.add(key);
    users.push(user);
  }
  return users;
}

function listAuthenticatedGhUsers({ execFileSync }) {
  const output = runGh(execFileSync, ['auth', 'status', '--hostname', 'github.com'], 10000);
  return parseGhAuthUsers(output);
}

function ghTokenForUser({ user, execFileSync }) {
  return String(runGh(execFileSync, ['auth', 'token', '--hostname', 'github.com', '--user', user], 10000) || '').trim();
}

function resolveWritableGitHubEnv({ repo, execFileSync }) {
  // W-mp76pw7a001da7c1 — Try the per-slug resolver FIRST. If `engine.ghAccounts`
  // maps this owner to a specific gh account, use that account's token instead
  // of probing whatever account is globally active. Falls through to the
  // legacy active-account / enumerate-all-accounts path when the resolver
  // returns null (preserves backward compat for unmapped slugs).
  try {
    const mappedToken = ghToken.resolveTokenForSlug(repo);
    if (mappedToken) {
      const ghEnv = { GH_TOKEN: mappedToken };
      try {
        const permission = repoViewerPermission({ repo, execFileSync, ghEnv });
        if (isWritableRepoPermission(permission)) return ghEnv;
      } catch { /* fall through to legacy enumeration */ }
    }
  } catch { /* fall through to legacy enumeration */ }

  // Final fallback: explicit GH_TOKEN env var. Only consulted when no mapping
  // produced a usable token. Honors the contract spelled out in the task —
  // resolver is primary, GH_TOKEN env is the env-side fallback.
  const envToken = String(process.env.GH_TOKEN || '').trim();
  if (envToken) {
    const ghEnv = { GH_TOKEN: envToken };
    try {
      const permission = repoViewerPermission({ repo, execFileSync, ghEnv });
      if (isWritableRepoPermission(permission)) return ghEnv;
    } catch { /* fall through */ }
  }

  let activePermission = '';
  let activeError = null;
  try {
    activePermission = repoViewerPermission({ repo, execFileSync });
    if (isWritableRepoPermission(activePermission)) return undefined;
  } catch (e) {
    activeError = e;
  }

  let users = [];
  let accountListError = null;
  try {
    users = listAuthenticatedGhUsers({ execFileSync });
  } catch (e) {
    if (activeError && isAuthError(activeError) && isAuthError(e)) {
      throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
    }
    accountListError = e;
  }

  const candidateErrors = [];
  for (const user of users) {
    try {
      const token = ghTokenForUser({ user, execFileSync });
      if (!token) continue;
      const ghEnv = { GH_TOKEN: token };
      const permission = repoViewerPermission({ repo, execFileSync, ghEnv });
      if (isWritableRepoPermission(permission)) return ghEnv;
    } catch (e) {
      candidateErrors.push(`${user}: ${conciseGhMessage(e)}`);
    }
  }

  const activeDetail = activePermission
    ? ` Active account permission is ${activePermission}.`
    : activeError
      ? ` Active account permission could not be checked: ${conciseGhMessage(activeError)}.`
      : '';
  const authStatusDetail = accountListError
    ? ` Authenticated accounts could not be listed: ${conciseGhMessage(accountListError)}.`
    : '';
  const candidateDetail = candidateErrors.length
    ? ` Checked ${candidateErrors.length} authenticated account(s) without finding write access.`
    : '';
  throw new GitHubIssueError(
    `No authenticated GitHub account with write access to ${repo}.${activeDetail} ` +
      `${authStatusDetail}${candidateDetail} ` +
      `Run: gh auth login --hostname github.com with a profile that can write to ${repo}, or set GH_TOKEN for that account.`,
    403
  );
}

function listRepoLabels({ repo, execFileSync, ghEnv }) {
  const output = runGh(execFileSync, ['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], 15000, { env: ghEnv });
  const parsed = JSON.parse(output || '[]');
  if (!Array.isArray(parsed)) {
    throw new GitHubIssueError('GitHub label list returned an unexpected response shape');
  }
  const labelsByLower = new Map();
  for (const item of parsed) {
    if (!item || typeof item.name !== 'string') continue;
    labelsByLower.set(item.name.toLowerCase(), item.name);
  }
  return labelsByLower;
}

function resolveLabels({ labels, repo, execFileSync, ghEnv }) {
  const requested = normalizeLabels(labels);
  if (requested.length === 0) {
    return { requested, labelsToApply: [], labelsSkipped: [], validationUnavailable: false };
  }

  try {
    const available = listRepoLabels({ repo, execFileSync, ghEnv });
    const labelsToApply = [];
    const labelsSkipped = [];
    for (const label of requested) {
      const matched = available.get(label.toLowerCase());
      if (matched) labelsToApply.push(matched);
      else labelsSkipped.push(label);
    }
    return { requested, labelsToApply, labelsSkipped, validationUnavailable: false };
  } catch (e) {
    if (e instanceof GitHubIssueError) throw e;
    if (isAuthError(e)) throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
    return { requested, labelsToApply: requested, labelsSkipped: [], validationUnavailable: true };
  }
}

function buildWarning(labelsSkipped, filedWithoutLabels) {
  if (!labelsSkipped.length) return undefined;
  const base = `Skipped unavailable GitHub label(s): ${labelsSkipped.join(', ')}.`;
  return filedWithoutLabels ? `${base} Filed without labels.` : base;
}

function _addRedactionIdentifier(entries, seen, value, replacement) {
  const clean = String(value || '').trim().replace(/\/+$/, '');
  if (clean.length < 4) return;
  const key = `${replacement}:${clean.toLowerCase()}`;
  if (seen.has(key)) return;
  seen.add(key);
  entries.push({ value: clean, replacement });
}

function _stripPrUrlBase(url) {
  return String(url || '')
    .trim()
    .replace(/\/(?:pull|pullrequest)\/?$/i, '')
    .replace(/\/+$/, '');
}

function _buildIssueRedactionIdentifiers({ repo, projects = [] } = {}) {
  const entries = [];
  const seen = new Set();
  _addRedactionIdentifier(entries, seen, repo, '[REDACTED_REPO]');

  for (const project of projects || []) {
    if (!project || typeof project !== 'object') continue;
    const owner = project.adoOrg || project.org || project.owner || '';
    const repoName = project.repoName || project.repositoryName || '';
    const adoProject = project.adoProject || project.project || '';

    if (owner && repoName) {
      _addRedactionIdentifier(entries, seen, `${owner}/${repoName}`, '[REDACTED_REPO]');
    }
    if (owner && adoProject && repoName) {
      _addRedactionIdentifier(entries, seen, `${owner}/${adoProject}/_git/${repoName}`, '[REDACTED_REPO]');
      _addRedactionIdentifier(entries, seen, `${owner}/${adoProject}/${repoName}`, '[REDACTED_REPO]');
    }
    const repositoryId = String(project.repositoryId || '').trim();
    if (repositoryId.length >= 8) {
      _addRedactionIdentifier(entries, seen, repositoryId, '[REDACTED_REPOSITORY_ID]');
    }

    for (const field of ['remoteUrl', 'repositoryUrl', 'cloneUrl', 'sshUrl', 'webUrl']) {
      _addRedactionIdentifier(entries, seen, project[field], '[REDACTED_REPO_URL]');
    }
    const prBase = _stripPrUrlBase(project.prUrlBase);
    _addRedactionIdentifier(entries, seen, prBase, '[REDACTED_REPO_URL]');
  }

  entries.sort((a, b) => b.value.length - a.value.length);
  return entries;
}

function _redactIssueContent(value, { repo, projects } = {}) {
  return shared.redactSecrets(String(value || ''), {
    redactRepositoryUrls: true,
    repositoryIdentifiers: _buildIssueRedactionIdentifiers({ repo, projects }),
  });
}

function createIssueWithLabels({ title, bodyFile, repo, labels, execFileSync, ghEnv }) {
  const args = ['issue', 'create', '--repo', repo, '--title', title, '--body-file', bodyFile];
  if (labels.length > 0) args.push('--label', labels.join(','));
  const output = runGh(execFileSync, args, 30000, { env: ghEnv });
  const url = extractIssueUrl(output);
  if (!url) {
    throw new GitHubIssueError(`Issue may not have been created: ${conciseGhMessage(output)}`);
  }
  return { url, output: String(output || '').trim() };
}

function createGitHubIssue({
  title,
  description = '',
  labels,
  repo = DEFAULT_REPO,
  projects,
  tmpDir,
  execFileSync = _execFileSync,
} = {}) {
  if (!title) throw new GitHubIssueError('title required', 400);

  try {
    runGh(execFileSync, ['--version'], 5000);
  } catch (e) {
    throw new GitHubIssueError('gh CLI not found. Install from https://cli.github.com/');
  }
  const ghEnv = resolveWritableGitHubEnv({ repo, execFileSync });

  const redactionProjects = projects || shared.getProjects();
  const safeTitle = _redactIssueContent(title, { repo, projects: redactionProjects });
  const safeDescription = _redactIssueContent(description || '', { repo, projects: redactionProjects });
  const issueBody = `${safeDescription}\n\n---\n_Filed via Minions dashboard_`;
  // P-f6-tmp-toctou: per-call mkdtempSync dir (under engine/tmp/ for
  // collocation with other engine tmp state). Random suffix closes the
  // predictable-prefix window on engine/tmp/bug-body-* that an attacker could
  // otherwise plant as a symlink.
  let scopedDir = null;
  let bodyFile;
  if (tmpDir) {
    fs.mkdirSync(tmpDir, { recursive: true });
    bodyFile = path.join(tmpDir, `bug-body-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.md`);
  } else {
    scopedDir = shared.createDispatchTmpDir(`issues-${process.pid}-${Date.now()}`);
    bodyFile = path.join(scopedDir, 'bug-body.md');
  }
  fs.writeFileSync(bodyFile, issueBody);

  let resolved;
  try {
    resolved = resolveLabels({ labels, repo, execFileSync, ghEnv });
    const created = createIssueWithLabels({
      title: safeTitle,
      bodyFile,
      repo,
      labels: resolved.labelsToApply,
      execFileSync,
      ghEnv,
    });
    const filedWithoutLabels = resolved.requested.length > 0 && resolved.labelsToApply.length === 0;
    return {
      ok: true,
      url: created.url,
      output: created.output,
      labelsRequested: resolved.requested,
      labelsApplied: resolved.labelsToApply,
      labelsSkipped: resolved.labelsSkipped,
      warning: buildWarning(resolved.labelsSkipped, filedWithoutLabels),
    };
  } catch (e) {
    if (e instanceof GitHubIssueError) throw e;
    if (isAuthError(e)) throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
    if (resolved && resolved.labelsToApply.length > 0 && isLabelUnavailableError(e)) {
      try {
        const created = createIssueWithLabels({ title: safeTitle, bodyFile, repo, labels: [], execFileSync, ghEnv });
        const skipped = normalizeLabels([...resolved.labelsSkipped, ...resolved.labelsToApply], []);
        return {
          ok: true,
          url: created.url,
          output: created.output,
          labelsRequested: resolved.requested,
          labelsApplied: [],
          labelsSkipped: skipped,
          warning: buildWarning(skipped, true),
        };
      } catch (retryErr) {
        if (isAuthError(retryErr)) throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
        throw new GitHubIssueError(`GitHub issue creation failed after retrying without labels: ${conciseGhMessage(retryErr)}`);
      }
    }
    throw new GitHubIssueError(`GitHub issue creation failed: ${conciseGhMessage(e)}`);
  } finally {
    try { fs.unlinkSync(bodyFile); } catch {}
    if (scopedDir) shared.removeDispatchTmpDir(scopedDir);
  }
}

module.exports = {
  DEFAULT_LABELS,
  GitHubIssueError,
  normalizeLabels,
  isLabelUnavailableError,
  createGitHubIssue,
  _buildIssueRedactionIdentifiers,
};
