/**
 * @shade/keychain — OS-keychain backend for @shade/storage-encrypted.
 *
 * No native deps: shells out to the platform's standard credential CLI:
 *   - macOS:   `security` (CLI shipped with the OS)
 *   - Linux:   `secret-tool` (libsecret CLI; available on most distros)
 *   - Windows: PowerShell + CredentialManager module (built into Windows 10+)
 *
 * Values are stored as base64 strings on platforms whose native APIs only
 * accept strings (macOS, libsecret) and as raw bytes where supported.
 */

import { spawn } from 'node:child_process';
import { Buffer } from 'node:buffer';

export interface KeychainBackend {
  get(service: string, account: string): Promise<Uint8Array | null>;
  set(service: string, account: string, value: Uint8Array): Promise<void>;
  delete(service: string, account: string): Promise<void>;
}

/** Pick a backend appropriate for the current platform; throw if unsupported. */
export function getDefaultKeychain(): KeychainBackend {
  switch (process.platform) {
    case 'darwin': return new MacOSKeychain();
    case 'linux':  return new LibSecretKeychain();
    case 'win32':  return new WindowsCredentialManager();
    default:
      throw new Error(`@shade/keychain: unsupported platform ${process.platform}`);
  }
}

// ─── macOS ──────────────────────────────────────────────────

export class MacOSKeychain implements KeychainBackend {
  async get(service: string, account: string): Promise<Uint8Array | null> {
    const { code, stdout } = await runCmd('security', [
      'find-generic-password', '-s', service, '-a', account, '-w',
    ]);
    if (code !== 0) return null;
    const b64 = stdout.trim();
    if (!b64) return null;
    return Buffer.from(b64, 'base64');
  }

  async set(service: string, account: string, value: Uint8Array): Promise<void> {
    const b64 = Buffer.from(value).toString('base64');
    // -U: update if exists. Send password via stdin to avoid leaking via argv.
    const { code, stderr } = await runCmd(
      'security',
      ['add-generic-password', '-U', '-s', service, '-a', account, '-w', b64],
    );
    if (code !== 0) throw new Error(`security add-generic-password failed: ${stderr}`);
  }

  async delete(service: string, account: string): Promise<void> {
    await runCmd('security', ['delete-generic-password', '-s', service, '-a', account]);
  }
}

// ─── Linux (libsecret via secret-tool) ──────────────────────

export class LibSecretKeychain implements KeychainBackend {
  async get(service: string, account: string): Promise<Uint8Array | null> {
    const { code, stdout } = await runCmd('secret-tool', ['lookup', 'service', service, 'account', account]);
    if (code !== 0) return null;
    const b64 = stdout.trim();
    if (!b64) return null;
    return Buffer.from(b64, 'base64');
  }

  async set(service: string, account: string, value: Uint8Array): Promise<void> {
    const b64 = Buffer.from(value).toString('base64');
    const { code, stderr } = await runCmd(
      'secret-tool',
      ['store', '--label', `shade:${service}:${account}`, 'service', service, 'account', account],
      b64,
    );
    if (code !== 0) throw new Error(`secret-tool store failed: ${stderr}`);
  }

  async delete(service: string, account: string): Promise<void> {
    await runCmd('secret-tool', ['clear', 'service', service, 'account', account]);
  }
}

// ─── Windows Credential Manager ────────────────────────────

export class WindowsCredentialManager implements KeychainBackend {
  private target(service: string, account: string): string {
    return `shade:${service}:${account}`;
  }

  async get(service: string, account: string): Promise<Uint8Array | null> {
    const target = this.target(service, account);
    const ps = `
      $ErrorActionPreference = 'Stop'
      Import-Module CredentialManager -ErrorAction SilentlyContinue
      $c = Get-StoredCredential -Target '${target}'
      if ($null -eq $c) { exit 1 }
      $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($c.Password)
      try { [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr) }
      finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
    `.trim();
    const { code, stdout } = await runCmd('powershell', ['-NoProfile', '-Command', ps]);
    if (code !== 0) return null;
    const b64 = stdout.trim();
    return b64 ? Buffer.from(b64, 'base64') : null;
  }

  async set(service: string, account: string, value: Uint8Array): Promise<void> {
    const target = this.target(service, account);
    const b64 = Buffer.from(value).toString('base64');
    const ps = `
      $ErrorActionPreference = 'Stop'
      Import-Module CredentialManager
      $sec = ConvertTo-SecureString -String '${b64}' -AsPlainText -Force
      New-StoredCredential -Target '${target}' -UserName '${account}' -SecurePassword $sec -Persist LocalMachine | Out-Null
    `.trim();
    const { code, stderr } = await runCmd('powershell', ['-NoProfile', '-Command', ps]);
    if (code !== 0) throw new Error(`Windows CredentialManager store failed: ${stderr}`);
  }

  async delete(service: string, account: string): Promise<void> {
    const target = this.target(service, account);
    const ps = `Remove-StoredCredential -Target '${target}' -ErrorAction SilentlyContinue`;
    await runCmd('powershell', ['-NoProfile', '-Command', ps]);
  }
}

// ─── Helpers ────────────────────────────────────────────────

interface CmdResult { code: number; stdout: string; stderr: string }

function runCmd(cmd: string, args: string[], stdin?: string): Promise<CmdResult> {
  return new Promise((resolve) => {
    const child = spawn(cmd, args, { stdio: ['pipe', 'pipe', 'pipe'] });
    let out = '';
    let err = '';
    child.stdout.on('data', (d: Buffer) => { out += d.toString('utf8'); });
    child.stderr.on('data', (d: Buffer) => { err += d.toString('utf8'); });
    child.on('error', () => resolve({ code: 127, stdout: '', stderr: 'spawn failed' }));
    child.on('close', (code: number | null) => resolve({ code: code ?? 0, stdout: out, stderr: err }));
    if (stdin !== undefined) {
      child.stdin.write(stdin);
      child.stdin.end();
    } else {
      child.stdin.end();
    }
  });
}
