/**
 * 1Password CLI (op) integration for importing secrets from vaults.
 */
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
function runOp(args, input) {
    const result = spawnSync('op', args, {
        stdio: [input !== undefined ? 'pipe' : 'ignore', 'pipe', 'pipe'],
        encoding: 'utf-8',
        maxBuffer: 10 * 1024 * 1024,
        input,
    });
    if (result.error) {
        if (result.error.code === 'ENOENT') {
            return { ok: false, error: '1Password CLI not found. Install: brew install 1password-cli' };
        }
        return { ok: false, error: result.error.message };
    }
    if (result.status !== 0) {
        const stderr = result.stderr?.trim() || '';
        if (stderr.includes('not signed in') || stderr.includes('sign in') || stderr.includes('no active session')) {
            return { ok: false, error: 'Not signed in to 1Password. Run: op signin' };
        }
        return { ok: false, error: stderr || `op exited with code ${result.status}` };
    }
    return { ok: true, stdout: result.stdout };
}
export function assertOpAvailable() {
    // `op account list` works with both CLI session tokens and the 1Password
    // desktop biometric integration; `op whoami` fails on the latter.
    const result = runOp(['account', 'list', '--format=json']);
    if (!result.ok) {
        throw new Error(result.error);
    }
}
export function listVaults() {
    const result = runOp(['vault', 'list', '--format=json']);
    if (!result.ok)
        throw new Error(result.error);
    return JSON.parse(result.stdout);
}
export function listItems(vaultName) {
    const result = runOp(['item', 'list', '--vault', vaultName, '--format=json']);
    if (!result.ok) {
        if (result.error.includes('vault') && result.error.includes('not found')) {
            const vaults = listVaults();
            const available = vaults.map((v) => v.name).join(', ');
            throw new Error(`Vault '${vaultName}' not found. Available: ${available || '(none)'}`);
        }
        throw new Error(result.error);
    }
    const items = JSON.parse(result.stdout);
    return items || [];
}
export function getItem(itemId, vaultName) {
    const result = runOp(['item', 'get', itemId, '--vault', vaultName, '--format=json', '--reveal']);
    if (!result.ok)
        throw new Error(result.error);
    return JSON.parse(result.stdout);
}
export function toEnvKey(title) {
    return title
        .toUpperCase()
        .replace(/[^A-Z0-9]+/g, '_')
        .replace(/^_+|_+$/g, '')
        .replace(/^(\d)/, '_$1');
}
export function slugify(name) {
    return name
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '');
}
const IMPORTABLE_FIELD_TYPES = new Set([
    'CONCEALED', 'concealed',
    'STRING', 'string', 'text', 'TEXT',
    'URL', 'url',
]);
const SKIP_FIELD_LABELS = new Set(['username', 'notesPlain', 'notes']);
const NOTES_FIELD_LABELS = new Set(['notesplain', 'notes']);
function pickBestField(fields) {
    const dominated = fields.filter((f) => IMPORTABLE_FIELD_TYPES.has(f.type) &&
        f.value &&
        !SKIP_FIELD_LABELS.has(f.label?.toLowerCase() || ''));
    if (dominated.length === 0)
        return null;
    // Prefer concealed fields (credentials/passwords)
    const concealed = dominated.find((f) => f.type.toLowerCase() === 'concealed');
    if (concealed)
        return concealed;
    // Then prefer fields labeled credential/password/secret/key/token
    const secretLabels = ['credential', 'password', 'secret', 'key', 'token', 'api_key', 'apikey'];
    const labeled = dominated.find((f) => secretLabels.includes(f.label?.toLowerCase() || ''));
    if (labeled)
        return labeled;
    // Fall back to first importable field
    return dominated[0];
}
/**
 * The item's notes, if any — a NOTES-purpose field (1Password's `notesPlain`)
 * or a field labelled notes/notesPlain, whichever carries a non-empty value.
 * Returns undefined when the item has no notes. This is descriptive metadata
 * only; it is deliberately independent of value selection and is never picked
 * as the secret value (see SKIP_FIELD_LABELS / pickBestField).
 */
function pickNotes(fields) {
    const notes = fields.find((f) => f.value != null &&
        f.value.trim() !== '' &&
        (f.purpose?.toUpperCase() === 'NOTES' ||
            NOTES_FIELD_LABELS.has(f.label?.toLowerCase() || '')));
    const value = notes?.value?.trim();
    return value ? value : undefined;
}
/**
 * Pure transform from a fetched 1Password item to an importable secret (or the
 * reason it was skipped). Split out of extractSecrets so the field-selection
 * and notes-extraction logic is exercised directly by tests without shelling
 * out to `op`.
 */
export function itemToSecret(item) {
    const field = pickBestField(item.fields || []);
    if (!field) {
        return { skipped: { itemTitle: item.title, fieldLabel: '*', reason: 'no importable fields' } };
    }
    if (field.value.includes('\n')) {
        return {
            skipped: {
                itemTitle: item.title,
                fieldLabel: field.label,
                reason: 'contains newlines (keychain limitation)',
            },
        };
    }
    const secret = {
        envKey: toEnvKey(item.title),
        itemTitle: item.title,
        fieldLabel: field.label,
        value: field.value,
    };
    const description = pickNotes(item.fields || []);
    if (description)
        secret.description = description;
    return { secret };
}
export function extractSecrets(items, vaultName) {
    const secrets = [];
    const skipped = [];
    for (const summary of items) {
        let item;
        try {
            item = getItem(summary.id, vaultName);
        }
        catch (err) {
            skipped.push({
                itemTitle: summary.title,
                fieldLabel: '*',
                reason: err.message,
            });
            continue;
        }
        const result = itemToSecret(item);
        if ('skipped' in result) {
            skipped.push(result.skipped);
        }
        else {
            secrets.push(result.secret);
        }
    }
    return { secrets, skipped };
}
export function buildPasswordItemTemplate(title, value) {
    return {
        title,
        category: 'PASSWORD',
        tags: ['agents-cli'],
        fields: [
            { id: 'password', type: 'CONCEALED', purpose: 'PASSWORD', label: 'password', value },
        ],
    };
}
export function itemExistsByTitle(title, vaultName) {
    const result = runOp(['item', 'get', title, '--vault', vaultName, '--format=json']);
    if (result.ok)
        return true;
    if (/isn't an item|not found|no item found/i.test(result.error))
        return false;
    throw new Error(result.error);
}
export function deleteItemByTitle(title, vaultName) {
    const result = runOp(['item', 'delete', title, '--vault', vaultName]);
    if (!result.ok)
        throw new Error(result.error);
}
export function createPasswordItem(title, value, vaultName) {
    // op item create reads stdin templates only from a real pipe; spawnSync's
    // input plumbing is detected as empty and op silently ignores the template.
    // The supported alternative is --template <file>, which works reliably.
    const template = JSON.stringify(buildPasswordItemTemplate(title, value));
    const tmpFile = path.join(os.tmpdir(), `agents-op-tpl-${process.pid}-${Date.now()}.json`);
    fs.writeFileSync(tmpFile, template, { mode: 0o600 });
    try {
        const result = runOp(['item', 'create', '--template', tmpFile, '--vault', vaultName]);
        if (!result.ok)
            throw new Error(result.error);
    }
    finally {
        try {
            fs.unlinkSync(tmpFile);
        }
        catch {
            // best-effort cleanup
        }
    }
}
