import {
  CLAUDE_CODE_CLIENT_ID,
  DEFAULT_APP_NAME,
  EXTERNAL_AGENT_ID_HEADER,
  VIRTUAL_KEY_HEADER,
} from "@archestra/shared";
import type { ConnectionSetupClientId } from "@/types";
import {
  claudeCodeOAuthNextStep,
  type SetupScriptContext,
  type SetupScriptProxySection,
} from "./connection-setup-script";

/**
 * PowerShell renderer for the Windows variant of the /connection one-command
 * setup flow (`irm <url> | iex`). Mirrors the bash renderer in
 * connection-setup-script.ts: deterministic string building, no DB/IO, same
 * idempotency + secret-handling contract, just expressed in PowerShell.
 *
 * Contract parity with the bash renderer:
 * - idempotent re-runs (remove-then-add for CLI registrations, key-scoped
 *   JSON merges / marker-delimited TOML blocks, backups taken once);
 * - secrets travel via PowerShell variables / request bodies / stdin, never as
 *   argv of an external command;
 * - colorized output (Cyan headers, Green success, Yellow/Red advisories),
 *   suppressed when NO_COLOR is set;
 * - ends with next steps + revocation guidance.
 *
 * Targets Windows PowerShell 5.1 (the OS default) as well as PowerShell 7+: no
 * `-AsHashtable`, ternary, or null-coalescing. A native command's non-zero exit
 * is not promoted to a terminating error ($PSNativeCommandUseErrorActionPreference),
 * but that setting does not cover stderr: under $ErrorActionPreference='Stop'
 * Windows PowerShell 5.1 turns any native-command stderr line into a terminating
 * error that 2>$null does not suppress, so the idempotent MCP remove-then-add —
 * whose remove writes "No MCP server named …" to stderr when the server is not
 * yet registered — wraps the remove in try/catch to stay idempotent.
 */
export function renderWindowsSetupScript(ctx: SetupScriptContext): string {
  const sections: string[] = [header(ctx)];

  switch (ctx.clientId) {
    case "claude-code":
      sections.push(...claudeCodeSections(ctx));
      break;
    case "codex":
      sections.push(...codexSections(ctx));
      break;
    case "copilot-cli":
      sections.push(...copilotSections(ctx));
      break;
    case "cursor":
      sections.push(...cursorSections(ctx));
      break;
  }

  sections.push(footer(ctx));
  return `${sections.join("\n\n")}\n`;
}

// ===================================================================
// Internal helpers — shared scaffolding
// ===================================================================

const CLIENT_LABELS: Record<ConnectionSetupClientId, string> = {
  "claude-code": "Claude Code",
  codex: "Codex",
  "copilot-cli": "Copilot CLI",
  cursor: "Cursor",
};

const CLIENT_BINARIES: Partial<Record<ConnectionSetupClientId, string>> = {
  "claude-code": "claude",
  codex: "codex",
  "copilot-cli": "copilot",
};

/** Single-quote a value for PowerShell; safe for arbitrary content. */
function psq(value: string): string {
  return `'${value.replace(/'/g, "''")}'`;
}

/**
 * Color setup + logging helpers. Colors are emitted only when NO_COLOR is
 * unset. `Say` marks section headers, `Ok` a success, `Warn`/`Err` advisory
 * and failure lines. A native command's non-zero exit is demoted from a
 * terminating error ($PSNativeCommandUseErrorActionPreference = $false); its
 * stderr — which $ErrorActionPreference='Stop' still promotes to a terminating
 * error on Windows PowerShell 5.1, past 2>$null — is instead swallowed per-call
 * (try/catch around the idempotent MCP remove) so a re-run never aborts.
 */
const SCRIPT_HELPERS = `$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
$ArchUseColor = [string]::IsNullOrEmpty($env:NO_COLOR)
function Say($m)  { Write-Host ''; if ($ArchUseColor) { Write-Host ('==> ' + $m) -ForegroundColor Cyan } else { Write-Host ('==> ' + $m) } }
function Ok($m)   { if ($ArchUseColor) { Write-Host ('==> ' + $m) -ForegroundColor Green } else { Write-Host ('==> ' + $m) } }
function Warn($m) { if ($ArchUseColor) { Write-Host ('warning: ' + $m) -ForegroundColor Yellow } else { Write-Host ('warning: ' + $m) } }
function Err($m)  { if ($ArchUseColor) { Write-Host ('error: ' + $m) -ForegroundColor Red } else { Write-Host ('error: ' + $m) } }`;

function header(ctx: SetupScriptContext): string {
  const label = CLIENT_LABELS[ctx.clientId];
  const binary = CLIENT_BINARIES[ctx.clientId];
  const requireBinary = binary
    ? `
if (-not (Get-Command ${binary} -ErrorAction SilentlyContinue)) {
  Err "the '${binary}' CLI was not found on PATH. Install ${label} first, then re-run this command."
  exit 1
}`
    : "";

  return `# ${ctx.appName} setup for ${label}.
# Generated by the ${ctx.appName} /connection page. This script contains
# credentials — do not share or commit it.
${SCRIPT_HELPERS}

${banner(ctx)}

Say ${psq(`${ctx.appName} setup: ${label}`)}${requireBinary}`;
}

/**
 * Splash printed at the top of every script: the Archestra ASCII mark (only
 * when not white-labeled) plus a plain-ASCII details block, emitted through a
 * single-quoted here-string so nothing in it is ever expanded by PowerShell.
 */
function banner(ctx: SetupScriptContext): string {
  const label = CLIENT_LABELS[ctx.clientId];

  const configures: string[] = [];
  if (ctx.mcp) configures.push("MCP gateway (OAuth)");
  if (ctx.proxy) {
    configures.push(
      `${ctx.proxy.providerLabel} via the LLM proxy${
        ctx.proxy.virtualKey ? " (virtual key)" : ""
      }`,
    );
  }
  if (ctx.skills) configures.push("Skills marketplace");

  // Pure-ASCII rendition of the Archestra mark — a filled tilted bar and dot
  // echoing logo-icon.svg. The built-in Windows PowerShell 5.1 console can
  // render block/quadrant Unicode glyphs as mojibake (legacy codepage), so this
  // mirrors the macOS/Linux block-mark's composition with portable ASCII only.
  const logo =
    ctx.appName === DEFAULT_APP_NAME
      ? `   .------------------.
   |                  |
   |        ,##.      |
   |        ####      |     ${ctx.appName}
   |       ####       |     Secure access to your AI tools
   |       #### ,.    |
   |       \`##' \`'    |
   |                  |
   '------------------'`
      : `   ${ctx.appName}
   Secure access to your AI tools`;

  const details = [
    `   Client:     ${label}`,
    configures.length > 0 ? `   Configures: ${configures.join(", ")}` : null,
    `   Note:       one-time setup — this link expires after first use.`,
  ]
    .filter(Boolean)
    .join("\n");

  return `Write-Host @'

${logo}

${details}
'@`;
}

function footer(ctx: SetupScriptContext): string {
  const lines = [`Ok "Done."`];

  const nextSteps = nextStepsFor(ctx);
  if (nextSteps.length > 0) {
    lines.push(`Write-Host @'

Next steps:
${nextSteps.map((step, i) => `  ${i + 1}. ${step}`).join("\n")}
'@`);
  }

  const revocation: string[] = [];
  if (ctx.proxy?.virtualKeyName) {
    revocation.push(
      `delete the "${ctx.proxy.virtualKeyName}" key on the Virtual API Keys page`,
    );
  }
  if (ctx.skills) {
    revocation.push(
      `revoke the "${ctx.skills.marketplaceName}" share link on the Skills page`,
    );
  }
  if (revocation.length > 0) {
    lines.push(`Write-Host @'

To revoke this machine's access later in ${ctx.appName}: ${revocation.join("; ")}.
'@`);
  }

  return lines.join("\n");
}

function nextStepsFor(ctx: SetupScriptContext): string[] {
  const steps: string[] = [];
  switch (ctx.clientId) {
    case "claude-code":
      if (ctx.mcp) {
        steps.push(claudeCodeOAuthNextStep(ctx.mcp.serverName));
      }
      if (ctx.proxy?.provider === "bedrock" && ctx.proxy.virtualKey) {
        steps.push(
          "Set the AWS_BEARER_TOKEN_BEDROCK environment variable shown above (setx or System settings to persist).",
        );
      }
      if (ctx.skills) {
        steps.push(
          "The shared skills are installed for Claude Code — start `claude` and they load automatically.",
        );
      }
      break;
    case "codex":
      if (ctx.mcp) {
        steps.push(
          `Run \`codex\` — it opens your browser to finish the OAuth handshake for "${ctx.mcp.serverName}".`,
        );
      }
      if (ctx.proxy) {
        if (!ctx.proxy.virtualKey) {
          steps.push(
            "Make sure Codex is signed in with your own OpenAI API key ($env:OPENAI_API_KEY | codex login --with-api-key).",
          );
        }
        steps.push(
          `Start Codex through the proxy: codex -c model_provider=${ctx.proxy.proxyName}`,
        );
      }
      if (ctx.skills) {
        steps.push(
          'Run /plugins inside Codex and pick "Install Plugin" to install the bundled skills.',
        );
      }
      break;
    case "copilot-cli":
      if (ctx.mcp) {
        steps.push(
          "Copilot opens your browser to complete OAuth when the gateway asks for it.",
        );
      }
      if (ctx.proxy) {
        steps.push(
          'Set the COPILOT_* environment variables shown above, set COPILOT_MODEL, then verify with: copilot -p "Reply with exactly: archestra-copilot-cli-ok"',
        );
      }
      if (ctx.skills) {
        steps.push(
          `Browse and install the shared skills: copilot plugin marketplace browse ${ctx.skills.marketplaceName}`,
        );
      }
      break;
    case "cursor":
      if (ctx.mcp) {
        steps.push(
          `Open Cursor settings → MCP and toggle on "${ctx.mcp.serverName}"; Cursor handles the OAuth flow.`,
        );
      }
      if (ctx.proxy) {
        steps.push(
          "Apply the Cursor model settings printed above (Settings → Models → OpenAI API Key).",
        );
      }
      if (ctx.skills) {
        steps.push(
          "Run /add-plugin in Cursor's command palette and paste the clone URL printed above.",
        );
      }
      break;
  }
  return steps;
}

/**
 * Key-scoped JSON merge using PowerShell's built-in ConvertFrom-Json /
 * ConvertTo-Json (no python dependency). Ensures the file exists, backs it up
 * once, ensures a nested object, then sets each leaf property. Works on
 * Windows PowerShell 5.1 (PSCustomObject + Add-Member, not -AsHashtable).
 */
function mergeJsonFileSnippet(params: {
  /** PowerShell expression resolving to the file path (e.g. a Join-Path). */
  pathExpr: string;
  /** Dotted accessor of the nested object to merge into, e.g. "env". */
  nestedKey: string;
  /** Leaf key/value pairs to set under the nested object. */
  values: Record<string, string>;
}): string {
  const setLines = Object.entries(params.values)
    .map(
      ([key, value]) =>
        `if ($arch_nested.PSObject.Properties[${psq(key)}]) { $arch_nested.${psBareOrIndex(key)} = ${psq(value)} } else { $arch_nested | Add-Member -NotePropertyName ${psq(key)} -NotePropertyValue ${psq(value)} }`,
    )
    .join("\n");

  return `$arch_path = ${params.pathExpr}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $arch_path) | Out-Null
if ((Test-Path $arch_path) -and -not (Test-Path ($arch_path + '.archestra-backup'))) {
  Copy-Item -Path $arch_path -Destination ($arch_path + '.archestra-backup')
}
$arch_config = [pscustomobject]@{}
if (Test-Path $arch_path) {
  $arch_raw = Get-Content -Raw -Path $arch_path
  if ($arch_raw -and $arch_raw.Trim()) { $arch_config = $arch_raw | ConvertFrom-Json }
}
if (-not $arch_config.PSObject.Properties[${psq(params.nestedKey)}]) { $arch_config | Add-Member -NotePropertyName ${psq(params.nestedKey)} -NotePropertyValue ([pscustomobject]@{}) }
$arch_nested = $arch_config.${psBareOrIndex(params.nestedKey)}
${setLines}
$arch_config | ConvertTo-Json -Depth 32 | Set-Content -Path $arch_path -Encoding utf8
Write-Host ('Updated ' + $arch_path)`;
}

/**
 * Property access for a key known to be a safe slug; falls back to a quoted
 * index for anything with non-identifier characters. Server/proxy names are
 * already slugs, so this is defensive.
 */
function psBareOrIndex(key: string): string {
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? key : `${psq(key)}`;
}

// ===================================================================
// Internal helpers — Claude Code
// ===================================================================

function claudeCodeSections(ctx: SetupScriptContext): string[] {
  const sections: string[] = [];

  if (ctx.mcp) {
    sections.push(`Say ${psq(`Registering MCP gateway "${ctx.mcp.serverName}" (OAuth)`)}
try { claude mcp remove ${psq(ctx.mcp.serverName)} 2>$null | Out-Null } catch { }
claude mcp add --transport http ${psq(ctx.mcp.serverName)} ${psq(ctx.mcp.url)}`);
  }

  if (ctx.proxy) {
    sections.push(
      ctx.proxy.provider === "bedrock"
        ? claudeBedrockProxySection(ctx.proxy)
        : claudeAnthropicProxySection(ctx.proxy),
    );
  }

  if (ctx.skills) {
    const pluginRef = `${ctx.skills.marketplaceName}@${ctx.skills.marketplaceName}`;
    sections.push(`Say ${psq(`Installing the "${ctx.skills.marketplaceName}" skills bundle`)}
claude plugin marketplace add ${psq(ctx.skills.cloneUrl)}
if ($LASTEXITCODE -ne 0) { Warn 'Marketplace may already be registered — continuing.' }
claude plugin install ${psq(pluginRef)}
if ($LASTEXITCODE -ne 0) { Warn ${psq(`Could not install the skills automatically — run 'claude plugin install ${pluginRef}' or open /plugin inside Claude Code.`)} }`);
  }

  return sections;
}

const CLAUDE_SETTINGS_PATH =
  "(Join-Path $env:USERPROFILE '.claude\\settings.json')";

/**
 * Custom headers Claude Code sends on every proxied request (Anthropic and
 * Bedrock alike — ANTHROPIC_CUSTOM_HEADERS applies to both), one "Name: Value"
 * per line: X-Archestra-Agent-Id (Claude Code attribution, always) and the
 * X-Archestra-Virtual-Key passthrough header (user attribution, when present).
 */
function claudeCustomHeaderLines(proxy: SetupScriptProxySection): string[] {
  const headerLines = [`${EXTERNAL_AGENT_ID_HEADER}: ${CLAUDE_CODE_CLIENT_ID}`];
  if (proxy.passthroughVirtualKey) {
    headerLines.push(`${VIRTUAL_KEY_HEADER}: ${proxy.passthroughVirtualKey}`);
  }
  return headerLines;
}

function claudeAnthropicProxySection(proxy: SetupScriptProxySection): string {
  const values: Record<string, string> = {
    ANTHROPIC_BASE_URL: proxy.url,
  };
  if (proxy.virtualKey) {
    values.ANTHROPIC_AUTH_TOKEN = proxy.virtualKey;
  }
  // Append our custom headers after the base-URL merge, preserving any the user
  // already set.
  const headerAppend = `\n${claudeCustomHeaderAppendSnippet(claudeCustomHeaderLines(proxy))}`;
  const passthroughNote = proxy.virtualKey
    ? ""
    : `
Write-Host ${psq(`Your existing ${proxy.providerLabel} credentials keep working — only the base URL changed.`)}`;

  return `Say ${psq(`Routing Claude Code through the ${proxy.providerLabel} proxy`)}
${mergeJsonFileSnippet({
  pathExpr: CLAUDE_SETTINGS_PATH,
  nestedKey: "env",
  values,
})}${headerAppend}${passthroughNote}`;
}

/**
 * Append-merge our headers into env.ANTHROPIC_CUSTOM_HEADERS in settings.json:
 * keep the user's other headers, replace only our lines (matched
 * case-insensitively by header name) so re-runs / key rotation never duplicate
 * or leave a stale token. Runs right after the base-URL merge created the file,
 * so it neither re-creates the dir nor re-takes the backup.
 */
function claudeCustomHeaderAppendSnippet(headerLines: string[]): string {
  const psArray = headerLines.map(psq).join(", ");
  return `$arch_hpath = ${CLAUDE_SETTINGS_PATH}
$arch_hconfig = [pscustomobject]@{}
if (Test-Path $arch_hpath) {
  $arch_hraw = Get-Content -Raw -Path $arch_hpath
  if ($arch_hraw -and $arch_hraw.Trim()) { $arch_hconfig = $arch_hraw | ConvertFrom-Json }
}
if (-not $arch_hconfig.PSObject.Properties['env']) { $arch_hconfig | Add-Member -NotePropertyName 'env' -NotePropertyValue ([pscustomobject]@{}) }
$arch_henv = $arch_hconfig.env
$arch_hnew = @(${psArray})
$arch_hnames = @($arch_hnew | ForEach-Object { ($_ -split ':',2)[0].Trim().ToLower() })
$arch_hexisting = ''
if ($arch_henv.PSObject.Properties['ANTHROPIC_CUSTOM_HEADERS']) { $arch_hexisting = [string]$arch_henv.ANTHROPIC_CUSTOM_HEADERS }
$arch_hkept = @()
foreach ($arch_hl in ($arch_hexisting -split "\\r?\\n")) {
  if ($arch_hl.Trim() -and ($arch_hnames -notcontains ($arch_hl -split ':',2)[0].Trim().ToLower())) { $arch_hkept += $arch_hl }
}
$arch_hkept += $arch_hnew
$arch_hjoined = ($arch_hkept -join "\`n")
if ($arch_henv.PSObject.Properties['ANTHROPIC_CUSTOM_HEADERS']) { $arch_henv.ANTHROPIC_CUSTOM_HEADERS = $arch_hjoined } else { $arch_henv | Add-Member -NotePropertyName 'ANTHROPIC_CUSTOM_HEADERS' -NotePropertyValue $arch_hjoined }
$arch_hconfig | ConvertTo-Json -Depth 32 | Set-Content -Path $arch_hpath -Encoding utf8
Write-Host ('Updated ' + $arch_hpath)`;
}

function claudeBedrockProxySection(proxy: SetupScriptProxySection): string {
  return `Say ${psq("Routing Claude Code through the Bedrock proxy")}
${mergeJsonFileSnippet({
  pathExpr: CLAUDE_SETTINGS_PATH,
  nestedKey: "env",
  values: {
    CLAUDE_CODE_USE_BEDROCK: "1",
    AWS_REGION: "us-east-1",
    ANTHROPIC_BEDROCK_BASE_URL: proxy.url,
  },
})}
${claudeCustomHeaderAppendSnippet(claudeCustomHeaderLines(proxy))}
Write-Host 'Update AWS_REGION in the settings.json env block if you use a different region.'
${
  proxy.virtualKey
    ? // Printed inside a single-quoted here-string (literal — $env: is not
      // expanded). virtual-key values are url-safe base64, so they can never
      // contain a newline that would break out of the here-string.
      `Write-Host @'

Set this environment variable (kept out of files claude reads; use setx or System settings to persist):
  $env:AWS_BEARER_TOKEN_BEDROCK = "${proxy.virtualKey}"
'@`
    : `Write-Host 'Your existing AWS credentials keep working — only the base URL changed.'`
}`;
}

// ===================================================================
// Internal helpers — Codex
// ===================================================================

function codexSections(ctx: SetupScriptContext): string[] {
  const sections: string[] = [];

  if (ctx.mcp) {
    sections.push(`Say ${psq(`Registering MCP gateway "${ctx.mcp.serverName}" (OAuth)`)}
try { codex mcp remove ${psq(ctx.mcp.serverName)} 2>$null | Out-Null } catch { }
codex mcp add ${psq(ctx.mcp.serverName)} --url ${psq(ctx.mcp.url)}`);
  }

  if (ctx.proxy) {
    const marker = `archestra:${ctx.proxy.proxyName}`;
    const block = `# >>> ${marker} >>>
[model_providers.${ctx.proxy.proxyName}]
name = "${ctx.proxy.proxyName}"
base_url = "${ctx.proxy.url}"
wire_api = "responses"
requires_openai_auth = true
# <<< ${marker} <<<`;

    sections.push(`Say ${psq(`Adding the "${ctx.proxy.proxyName}" provider to ~/.codex/config.toml`)}
$arch_config = Join-Path $env:USERPROFILE '.codex\\config.toml'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $arch_config) | Out-Null
if (Test-Path $arch_config) {
  # Back up once so re-runs preserve the pristine pre-Archestra config.
  if (-not (Test-Path ($arch_config + '.archestra-backup'))) { Copy-Item -Path $arch_config -Destination ($arch_config + '.archestra-backup') }
  # Drop any previous archestra-managed block for this provider (idempotent).
  $arch_start = ${psq(`# >>> ${marker} >>>`)}
  $arch_end = ${psq(`# <<< ${marker} <<<`)}
  $arch_keep = New-Object System.Collections.Generic.List[string]
  $arch_skip = $false
  foreach ($arch_line in (Get-Content -Path $arch_config)) {
    if ($arch_line -eq $arch_start) { $arch_skip = $true; continue }
    if ($arch_line -eq $arch_end) { $arch_skip = $false; continue }
    if (-not $arch_skip) { $arch_keep.Add($arch_line) }
  }
  Set-Content -Path $arch_config -Value $arch_keep -Encoding utf8
}
Add-Content -Path $arch_config -Encoding utf8 -Value @'
${block}
'@
Write-Host ('Updated ' + $arch_config)${
      ctx.proxy.virtualKey
        ? `

Say ${psq("Signing Codex in with your virtual key")}
$ArchVirtualKey = ${psq(ctx.proxy.virtualKey)}
$ArchVirtualKey | codex login --with-api-key`
        : `
Write-Host 'Codex keeps using your own OpenAI API key login.'`
    }`);
  }

  if (ctx.skills) {
    sections.push(`Say ${psq(`Registering the "${ctx.skills.marketplaceName}" skills marketplace`)}
codex plugin marketplace add ${psq(ctx.skills.cloneUrl)}
if ($LASTEXITCODE -ne 0) { Warn 'Marketplace may already be registered — run /plugins inside Codex to inspect.' }`);
  }

  return sections;
}

// ===================================================================
// Internal helpers — Copilot CLI
// ===================================================================

function copilotSections(ctx: SetupScriptContext): string[] {
  const sections: string[] = [];

  if (ctx.mcp) {
    sections.push(`Say ${psq(`Registering MCP gateway "${ctx.mcp.serverName}" (OAuth)`)}
try { copilot mcp remove ${psq(ctx.mcp.serverName)} 2>$null | Out-Null } catch { }
copilot mcp add --transport http ${psq(ctx.mcp.serverName)} ${psq(ctx.mcp.url)}
copilot mcp get ${psq(ctx.mcp.serverName)}`);
  }

  if (ctx.proxy) {
    if (ctx.proxy.provider === "github-copilot" && !ctx.proxy.virtualKey) {
      sections.push(copilotGithubLinkSection(ctx.proxy));
    } else {
      sections.push(`Say ${psq(`Copilot provider settings (${ctx.proxy.providerLabel} via OpenAI-compatible protocol)`)}
Write-Host @'

Set these environment variables (current session shown; use setx or System settings to persist), set COPILOT_MODEL to the model you use:
  $env:COPILOT_PROVIDER_TYPE = "openai"
  $env:COPILOT_PROVIDER_BASE_URL = "${ctx.proxy.url}"
  $env:COPILOT_PROVIDER_API_KEY = "${
    ctx.proxy.virtualKey
      ? ctx.proxy.virtualKey
      : `<your-${ctx.proxy.provider}-api-key>`
  }"
  $env:COPILOT_MODEL = "<model-name>"
'@`);
    }
  }

  if (ctx.skills) {
    sections.push(`Say ${psq(`Registering the "${ctx.skills.marketplaceName}" skills marketplace`)}
copilot plugin marketplace add ${psq(ctx.skills.cloneUrl)}
if ($LASTEXITCODE -ne 0) { Warn "Marketplace may already be registered — run 'copilot plugin marketplace browse' to inspect." }`);
  }

  return sections;
}

/**
 * GitHub Copilot in passthrough mode: there is no static API key — the proxy
 * expects the user's long-lived GitHub OAuth token as the bearer. Mirrors the
 * bash device flow: reuse a token the Copilot CLI / VS Code already stored (only
 * if Copilot's token exchange accepts it), otherwise run the GitHub device flow
 * (RFC 8628). The token stays in a PowerShell variable and is passed via
 * Invoke-RestMethod headers / request bodies, never as argv to an external
 * command; it is printed only in the final export-equivalent lines.
 */
function copilotGithubLinkSection(proxy: SetupScriptProxySection): string {
  const gh = proxy.githubCopilot;
  if (!gh) {
    throw new Error(
      "github-copilot passthrough proxy section requires githubCopilot device-flow configuration",
    );
  }

  const deviceCodeUrl = `${gh.deviceAuthBaseUrl.replace(/\/+$/, "")}/login/device/code`;
  const accessTokenUrl = `${gh.deviceAuthBaseUrl.replace(/\/+$/, "")}/login/oauth/access_token`;
  const deviceRequestBody = JSON.stringify({
    client_id: gh.clientId,
    scope: "read:user",
  });

  return `Say 'Linking your GitHub Copilot subscription'
$ArchGhcpToken = ''

# Probe the Copilot token exchange: succeeds only for a valid GitHub token on
# an account with an active Copilot seat. Token is sent in a request header
# (in-process), never as argv.
function Test-ArchGhcp($arch_tok) {
  if ([string]::IsNullOrEmpty($arch_tok)) { return $false }
  try {
    Invoke-RestMethod -Method Get -Uri ${psq(gh.tokenExchangeUrl)} -TimeoutSec 30 -Headers @{
      'authorization' = ('token ' + $arch_tok)
      'accept' = 'application/json'
      'editor-version' = 'vscode/1.99.0'
      'copilot-integration-id' = 'vscode-chat'
    } -ErrorAction Stop | Out-Null
    return $true
  } catch {
    return $false
  }
}

# 1. Reuse a GitHub token already stored by the Copilot CLI / VS Code.
$arch_ghcp_paths = @(
  (Join-Path $env:USERPROFILE '.config\\github-copilot\\apps.json'),
  (Join-Path $env:USERPROFILE '.config\\github-copilot\\hosts.json')
)
if ($env:LOCALAPPDATA) {
  $arch_ghcp_paths += (Join-Path $env:LOCALAPPDATA 'github-copilot\\apps.json')
  $arch_ghcp_paths += (Join-Path $env:LOCALAPPDATA 'github-copilot\\hosts.json')
}
$arch_ghcp_candidates = @()
foreach ($arch_p in $arch_ghcp_paths) {
  if (Test-Path $arch_p) {
    try {
      $arch_data = Get-Content -Raw -Path $arch_p | ConvertFrom-Json
      foreach ($arch_prop in $arch_data.PSObject.Properties) {
        $arch_v = $arch_prop.Value
        if ($arch_v -and $arch_v.PSObject.Properties['oauth_token']) {
          $arch_t = $arch_v.oauth_token
          if ($arch_t -and ($arch_ghcp_candidates -notcontains $arch_t)) { $arch_ghcp_candidates += $arch_t }
        }
      }
    } catch { }
  }
}
foreach ($arch_cand in $arch_ghcp_candidates) {
  if (Test-ArchGhcp $arch_cand) {
    $ArchGhcpToken = $arch_cand
    Write-Host 'Re-using the GitHub token stored by the Copilot CLI on this machine.'
    break
  }
}

# 2. No usable stored token: run the GitHub device flow (RFC 8628).
if ([string]::IsNullOrEmpty($ArchGhcpToken)) {
  try {
    $arch_device = Invoke-RestMethod -Method Post -Uri ${psq(deviceCodeUrl)} -TimeoutSec 30 -Headers @{ 'accept' = 'application/json' } -ContentType 'application/json' -Body ${psq(deviceRequestBody)}
  } catch {
    Err 'could not reach GitHub to start the device flow.'
    exit 1
  }
  $arch_device_code = $arch_device.device_code
  $arch_user_code = $arch_device.user_code
  $arch_verification_uri = $arch_device.verification_uri
  $arch_interval = 5
  if ($arch_device.interval) { $arch_interval = [int]$arch_device.interval }
  $arch_expires_in = 900
  if ($arch_device.expires_in) { $arch_expires_in = [int]$arch_device.expires_in }
  if ([string]::IsNullOrEmpty($arch_device_code)) {
    Err 'GitHub did not return a device code.'
    exit 1
  }
  $arch_deadline = (Get-Date).AddSeconds($arch_expires_in)
  Write-Host ''
  Write-Host ('  Open:        ' + $arch_verification_uri)
  Write-Host ('  Enter code:  ' + $arch_user_code)
  Write-Host ''
  Write-Host 'Waiting for you to authorize in the browser...'
  while ([string]::IsNullOrEmpty($ArchGhcpToken)) {
    if ((Get-Date) -ge $arch_deadline) {
      Err 'timed out waiting for GitHub authorization — re-run this command to try again.'
      exit 1
    }
    Start-Sleep -Seconds $arch_interval
    $arch_poll_body = (@{ client_id = ${psq(gh.clientId)}; device_code = $arch_device_code; grant_type = 'urn:ietf:params:oauth:grant-type:device_code' } | ConvertTo-Json -Compress)
    try {
      $arch_poll = Invoke-RestMethod -Method Post -Uri ${psq(accessTokenUrl)} -TimeoutSec 30 -Headers @{ 'accept' = 'application/json' } -ContentType 'application/json' -Body $arch_poll_body
    } catch {
      continue
    }
    if ($arch_poll.access_token) {
      $ArchGhcpToken = $arch_poll.access_token
      break
    }
    $arch_err = ''
    if ($arch_poll.PSObject.Properties['error']) { $arch_err = [string]$arch_poll.error }
    switch ($arch_err) {
      'authorization_pending' { }
      '' { }
      'slow_down' { $arch_interval = $arch_interval + 5 }
      default {
        Err ('GitHub sign-in failed: ' + $arch_err)
        exit 1
      }
    }
  }
  Ok 'GitHub account linked.'
  if (-not (Test-ArchGhcp $ArchGhcpToken)) {
    Err 'this GitHub account does not appear to have an active Copilot subscription.'
    exit 1
  }
}

Say 'Copilot provider settings (GitHub Copilot via OpenAI-compatible protocol)'
Write-Host ''
Write-Host 'Set these environment variables (use setx or System settings to persist):'
Write-Host '  $env:COPILOT_PROVIDER_TYPE = "openai"'
Write-Host ('  $env:COPILOT_PROVIDER_BASE_URL = "' + ${psq(proxy.url)} + '"')
if (-not [string]::IsNullOrEmpty($ArchGhcpToken)) {
  Write-Host ('  $env:COPILOT_PROVIDER_API_KEY = "' + $ArchGhcpToken + '"')
} else {
  Write-Host '  $env:COPILOT_PROVIDER_API_KEY = "<your-github-oauth-token>"'
}
Write-Host '  $env:COPILOT_MODEL = "<model-name>"'`;
}

// ===================================================================
// Internal helpers — Cursor
// ===================================================================

function cursorSections(ctx: SetupScriptContext): string[] {
  const sections: string[] = [];

  if (ctx.mcp) {
    sections.push(`Say ${psq(`Adding MCP gateway "${ctx.mcp.serverName}" to ~/.cursor/mcp.json (OAuth)`)}
$arch_path = Join-Path $env:USERPROFILE '.cursor\\mcp.json'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $arch_path) | Out-Null
if ((Test-Path $arch_path) -and -not (Test-Path ($arch_path + '.archestra-backup'))) {
  Copy-Item -Path $arch_path -Destination ($arch_path + '.archestra-backup')
}
$arch_config = [pscustomobject]@{}
if (Test-Path $arch_path) {
  $arch_raw = Get-Content -Raw -Path $arch_path
  if ($arch_raw -and $arch_raw.Trim()) { $arch_config = $arch_raw | ConvertFrom-Json }
}
if (-not $arch_config.PSObject.Properties['mcpServers']) { $arch_config | Add-Member -NotePropertyName 'mcpServers' -NotePropertyValue ([pscustomobject]@{}) }
$arch_servers = $arch_config.mcpServers
$arch_server_name = ${psq(ctx.mcp.serverName)}
$arch_entry = [pscustomobject]@{ url = ${psq(ctx.mcp.url)} }
if ($arch_servers.PSObject.Properties[$arch_server_name]) { $arch_servers.$arch_server_name = $arch_entry } else { $arch_servers | Add-Member -NotePropertyName $arch_server_name -NotePropertyValue $arch_entry }
$arch_config | ConvertTo-Json -Depth 32 | Set-Content -Path $arch_path -Encoding utf8
Write-Host ('Updated ' + $arch_path)`);
  }

  if (ctx.proxy) {
    sections.push(`Say ${psq("Cursor model settings (manual step)")}
Write-Host @'

In Cursor: Settings -> Models -> API Keys -> OpenAI API Key
  1. Turn on "Override OpenAI Base URL" and paste: ${ctx.proxy.url}
  2. ${
    ctx.proxy.virtualKey
      ? `Paste this key into the API Key field and click Verify:
     ${ctx.proxy.virtualKey}`
      : `Paste your own ${ctx.proxy.providerLabel} API key into the API Key field and click Verify.`
  }
'@`);
  }

  if (ctx.skills) {
    sections.push(`Say ${psq(`Skills marketplace (manual step)`)}
Write-Host @'

In Cursor's command palette run /add-plugin and paste:
  ${ctx.skills.cloneUrl}
'@`);
  }

  return sections;
}
