/**
 * Interactive wizard that provisions an OpenBao backend end-to-end:
 *
 *   1. Asks the user for the OpenBao URL + admin/root token.
 *   2. Verifies connectivity (`/sys/health`).
 *   3. Ensures KV v2 is mounted at `<mount>/`.
 *   4. Writes policy `app-mcpd` scoped to `<mount>/{data,metadata}/<prefix>/*`
 *      plus the self-rotation paths.
 *   5. Ensures a token role `app-mcpd-role` with `period=720h, renewable=true`.
 *   6. Mints the first periodic token via that role.
 *   7. Stores the token as a plaintext `Secret` on mcpd.
 *   8. Creates the `SecretBackend` row with rotation config pointing at the role.
 *   9. Kicks an initial rotate via `POST /api/v1/secretbackends/:id/rotate`
 *      to seed `tokenMeta` + prove the self-rotation policy works.
 *  10. (Optional) promotes the new backend to default.
 *  11. Prints the migration command for the user to run.
 *
 * Setup token (only needs policy-write + auth/token admin on OpenBao; root is
 * a convenient default but a scoped service account works too) is used only
 * for steps 2–6 and is never persisted.
 *
 * All prompts go through `ConfigSetupPrompt` (from `config-setup.ts`) so the
 * wizard is testable without real stdin.
 */
import type { ApiClient } from '../api-client.js';
import {
  verifyHealth,
  ensureKvV2,
  writePolicy,
  ensureTokenRole,
  mintRoleToken,
  testWriteReadDelete,
  buildAppMcpdPolicyHcl,
  type VaultDeps,
} from '@mcpctl/shared';
import { type ConfigSetupPrompt, defaultPrompt } from './config-setup.js';

export interface WizardDeps {
  client: ApiClient;
  log: (...args: unknown[]) => void;
  prompt?: ConfigSetupPrompt;
  /** Overridable for tests. Forwarded to all vault HTTP calls. */
  fetch?: typeof globalThis.fetch;
}

export interface WizardInput {
  /** Backend name. Required — supplied via `mcpctl create secretbackend <name> --wizard`. */
  name: string;
  /** Pre-filled via flags for CI; falls back to prompt. */
  url?: string | undefined;
  setupToken?: string | undefined;
  mount?: string | undefined;
  pathPrefix?: string | undefined;
  policyName?: string | undefined;
  tokenRole?: string | undefined;
  promoteToDefault?: boolean | undefined;
  /** If set, skip the test write/read/delete (for dev/debugging only). */
  skipSmoke?: boolean | undefined;
}

export async function runSecretBackendOpenbaoWizard(
  input: WizardInput,
  deps: WizardDeps,
): Promise<void> {
  const prompt = deps.prompt ?? defaultPrompt;
  const log = deps.log;

  const url = input.url ?? await prompt.input('OpenBao URL', 'https://bao.ad.itaz.eu');
  const setupToken = input.setupToken ?? await prompt.password('OpenBao setup token (needs policy write + auth/token admin perms; root is fine)');
  if (setupToken === '') throw new Error('setup token is required');

  const vaultDeps: VaultDeps = {};
  if (deps.fetch !== undefined) vaultDeps.fetch = deps.fetch;

  // 1. Health check.
  log('  → checking OpenBao health …');
  const health = await verifyHealth(url, setupToken, vaultDeps);
  if (!health.initialized || health.sealed) {
    throw new Error(`OpenBao is not ready (initialized=${String(health.initialized)}, sealed=${String(health.sealed)})`);
  }
  log(`    ok (version ${health.version})`);

  const mount = input.mount ?? await prompt.input('KV v2 mount', 'secret');
  const pathPrefix = input.pathPrefix ?? await prompt.input('Path prefix under mount', 'mcpd');
  const policyName = input.policyName ?? await prompt.input('Policy name', 'app-mcpd');
  const tokenRole = input.tokenRole ?? await prompt.input('Token role name', 'app-mcpd-role');

  // 2. Enable KV v2 if needed.
  log(`  → ensuring KV v2 at ${mount}/ …`);
  const created = await ensureKvV2(url, setupToken, mount, vaultDeps);
  log(`    ${created ? 'mounted' : 'already mounted'}`);

  // 3. Write policy.
  log(`  → writing policy '${policyName}' …`);
  const hcl = buildAppMcpdPolicyHcl({ mount, pathPrefix, tokenRole });
  await writePolicy(url, setupToken, policyName, hcl, vaultDeps);
  log(`    written (scope: ${mount}/{data,metadata}/${pathPrefix}/* + self-rotation paths)`);

  // 4. Ensure token role.
  log(`  → ensuring token role '${tokenRole}' (period=720h, renewable) …`);
  await ensureTokenRole(url, setupToken, tokenRole, {
    allowedPolicies: [policyName],
    period: 720 * 3600,
    renewable: true,
    orphan: false,
  }, vaultDeps);
  log('    ok');

  // 5. Mint the first periodic token using the setup token.
  log('  → minting first periodic token …');
  const minted = await mintRoleToken(url, setupToken, tokenRole, vaultDeps);
  if (!minted.renewable) {
    throw new Error(`minted token is not renewable — the role '${tokenRole}' config is wrong`);
  }
  log(`    minted (accessor ${minted.accessor.slice(0, 12)}…)`);

  // 6. Smoke test with the minted token before committing to mcpd.
  if (input.skipSmoke !== true) {
    log('  → smoke-testing write/read/delete with the minted token …');
    await testWriteReadDelete(url, minted.clientToken, mount, `${pathPrefix}/.__mcpctl_wizard_smoke__`, vaultDeps);
    log('    ok');
  }

  // 7. Store token on mcpd as a plaintext Secret.
  const credsSecretName = `${input.name}-creds`;
  log(`  → creating Secret '${credsSecretName}' on mcpd (plaintext) …`);
  await createSecret(deps.client, credsSecretName, { token: minted.clientToken });

  // 8. Create SecretBackend row (non-default by default; promote later).
  log(`  → creating SecretBackend '${input.name}' …`);
  const backendBody = {
    name: input.name,
    type: 'openbao',
    config: {
      url,
      auth: 'token',
      mount,
      pathPrefix,
      tokenSecretRef: { name: credsSecretName, key: 'token' },
      rotation: {
        enabled: true,
        tokenRole,
        intervalHours: 24,
      },
    },
  };
  const backend = await deps.client.post<{ id: string; name: string }>('/api/v1/secretbackends', backendBody);
  log(`    created (id: ${backend.id})`);

  // 9. Kick initial rotation so tokenMeta is populated + self-rotation is proven.
  //    This uses the FIRST token (just-minted) to mint its successor. The old
  //    first token is then revoked by accessor.
  log('  → running initial rotation (seeds tokenMeta) …');
  try {
    await deps.client.post(`/api/v1/secretbackends/${backend.id}/rotate`, {});
    log('    rotated — tokenMeta populated');
  } catch (err) {
    log(`    warn: initial rotation failed: ${err instanceof Error ? err.message : String(err)}`);
    log('         backend is still usable; rotation will retry on the 24h loop');
  }

  // 10. Optional promote.
  const promote = input.promoteToDefault
    ?? await prompt.confirm(`Promote '${input.name}' to default backend?`, true);
  if (promote) {
    await deps.client.post(`/api/v1/secretbackends/${backend.id}/default`, {});
    log(`    promoted '${input.name}' to default`);
  }

  // 11. Migration hint.
  log('');
  await printMigrationHint(deps.client, input.name, log);

  log('');
  log(`Describe the new backend:   mcpctl --direct describe secretbackend ${input.name}`);
  log(`Force a rotation manually:  mcpctl --direct rotate secretbackend ${input.name}`);
}

async function createSecret(
  client: ApiClient,
  name: string,
  data: Record<string, string>,
): Promise<void> {
  try {
    await client.post('/api/v1/secrets', { name, data });
  } catch (err) {
    // 409 → secret already exists with this name. Update its data instead so
    // re-running the wizard with the same --name is idempotent.
    const status = (err as { status?: number }).status;
    if (status !== 409) throw err;
    const existing = (await client.get<Array<{ id: string; name: string }>>('/api/v1/secrets'))
      .find((s) => s.name === name);
    if (existing === undefined) throw err;
    await client.put(`/api/v1/secrets/${existing.id}`, { data });
  }
}

async function printMigrationHint(
  client: ApiClient,
  newBackendName: string,
  log: (...args: unknown[]) => void,
): Promise<void> {
  // Find the current default backend name (likely 'default') so the hint
  // points at a real source.
  let defaultName = 'default';
  try {
    const rows = await client.get<Array<{ name: string; isDefault: boolean }>>('/api/v1/secretbackends');
    const d = rows.find((r) => r.isDefault);
    if (d !== undefined && d.name !== newBackendName) defaultName = d.name;
  } catch {
    /* fall through with 'default' guess */
  }

  // Count candidate secrets.
  try {
    const body = await client.post<{ candidates: Array<{ name: string }> }>(
      '/api/v1/secrets/migrate',
      { from: defaultName, to: newBackendName, dryRun: true },
    );
    const n = body.candidates.length;
    if (n === 0) {
      log(`No secrets to migrate — '${defaultName}' is empty.`);
      return;
    }
    log(`You have ${String(n)} secret(s) on '${defaultName}'. To migrate them to '${newBackendName}':`);
    log('');
    log(`    mcpctl --direct migrate secrets --from ${defaultName} --to ${newBackendName} --dry-run`);
    log(`    mcpctl --direct migrate secrets --from ${defaultName} --to ${newBackendName}`);
  } catch (err) {
    log(`(could not dry-run migration: ${err instanceof Error ? err.message : String(err)})`);
    log(`Manual command:  mcpctl --direct migrate secrets --from ${defaultName} --to ${newBackendName}`);
  }
}
