#!/usr/bin/env tsx
/**
 * tools/federation-harness/seed.ts
 *
 * Provisions test data for the two-gateway federation harness.
 * Run via: tsx tools/federation-harness/seed.ts
 *
 * What this script does:
 *  1. (Optional) Boots the compose stack if --boot flag is passed.
 *  2. Waits for both gateways to be healthy.
 *  3. Bootstraps an admin user + token on each gateway via POST /api/bootstrap/setup.
 *  4. Creates three grants on Server B matching the M3 acceptance test scenarios:
 *     - Scope variant A: tasks + notes, include_personal: true
 *     - Scope variant B: tasks only, include_teams: ['T1'], exclude T2
 *     - Scope variant C: tasks + credentials in resources, credentials excluded (sanity)
 *  5. For each grant, walks the full enrollment flow:
 *       a. Server B creates a peer keypair (represents the requesting side).
 *       b. Server B creates the grant referencing that peer.
 *       c. Server B issues an enrollment token.
 *       d. Server A creates its own peer keypair (represents its view of B).
 *       e. Server A redeems the enrollment token at Server B's enrollment endpoint,
 *          submitting A's CSR → receives signed cert back.
 *       f. Server A stores the cert on its peer record → peer becomes active.
 *  6. Inserts representative test tasks/notes/credentials on Server B.
 *
 * IMPORTANT: This script uses the real admin REST API — no direct DB writes.
 * It exercises the full enrollment flow as M3 acceptance tests will.
 *
 * ESM / NodeNext: all imports use .js extensions.
 */

import { execSync } from 'node:child_process';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

// ─── Constants ───────────────────────────────────────────────────────────────

const __dirname = dirname(fileURLToPath(import.meta.url));
const COMPOSE_FILE = resolve(__dirname, 'docker-compose.two-gateways.yml');

/** Base URLs as seen from the host machine (mapped host ports). */
const SERVER_A_URL = process.env['GATEWAY_A_URL'] ?? 'http://localhost:14001';
const SERVER_B_URL = process.env['GATEWAY_B_URL'] ?? 'http://localhost:14002';

/**
 * Bootstrap passwords used when calling POST /api/bootstrap/setup on each
 * gateway. Each gateway starts with zero users and requires a one-time setup
 * call before any admin-guarded endpoints can be used.
 */
const ADMIN_BOOTSTRAP_PASSWORD_A =
  process.env['ADMIN_BOOTSTRAP_PASSWORD_A'] ?? 'harness-admin-password-a';
const ADMIN_BOOTSTRAP_PASSWORD_B =
  process.env['ADMIN_BOOTSTRAP_PASSWORD_B'] ?? 'harness-admin-password-b';

const READINESS_TIMEOUT_MS = 120_000;
const READINESS_POLL_MS = 3_000;

// ─── Scope variant definitions (for M3 acceptance tests) ─────────────────────

/** Scope variant A — tasks + notes, personal data included. */
export const SCOPE_VARIANT_A = {
  resources: ['tasks', 'notes'],
  filters: {
    tasks: { include_personal: true },
    notes: { include_personal: true },
  },
  excluded_resources: [] as string[],
  max_rows_per_query: 500,
};

/** Scope variant B — tasks only, team T1 only, no personal. */
export const SCOPE_VARIANT_B = {
  resources: ['tasks'],
  filters: {
    tasks: { include_teams: ['T1'], include_personal: false },
  },
  excluded_resources: [] as string[],
  max_rows_per_query: 500,
};

/**
 * Scope variant C — tasks + credentials in resources list, but credentials
 * explicitly in excluded_resources. Sanity test: credentials must still be
 * inaccessible even though they appear in resources.
 */
export const SCOPE_VARIANT_C = {
  resources: ['tasks', 'credentials'],
  filters: {
    tasks: { include_personal: true },
  },
  excluded_resources: ['credentials'],
  max_rows_per_query: 500,
};

// ─── Inline types (no import from packages/types — M3-01 branch not yet merged) ─

interface AdminFetchOptions {
  method?: string;
  body?: unknown;
  adminToken: string;
}

interface PeerRecord {
  peerId: string;
  csrPem: string;
}

interface GrantRecord {
  id: string;
  status: string;
  scope: unknown;
}

interface EnrollmentTokenResult {
  token: string;
  expiresAt: string;
  enrollmentUrl: string;
}

interface EnrollmentRedeemResult {
  certPem: string;
  certChainPem: string;
}

interface BootstrapResult {
  adminUserId: string;
  adminToken: string;
}

export interface SeedResult {
  serverAUrl: string;
  serverBUrl: string;
  adminTokenA: string;
  adminTokenB: string;
  adminUserIdA: string;
  adminUserIdB: string;
  grants: {
    variantA: GrantRecord;
    variantB: GrantRecord;
    variantC: GrantRecord;
  };
  peers: {
    variantA: PeerRecord & { grantId: string };
    variantB: PeerRecord & { grantId: string };
    variantC: PeerRecord & { grantId: string };
  };
}

// ─── HTTP helpers ─────────────────────────────────────────────────────────────

/**
 * Authenticated admin fetch. Sends `Authorization: Bearer <adminToken>` which
 * is the only path supported by AdminGuard (DB-backed sha256 token lookup).
 * No `x-admin-key` header path exists in the gateway.
 */
async function adminFetch<T>(baseUrl: string, path: string, opts: AdminFetchOptions): Promise<T> {
  const url = `${baseUrl}${path}`;
  const res = await fetch(url, {
    method: opts.method ?? 'GET',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${opts.adminToken}`,
    },
    body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
  });

  if (!res.ok) {
    const text = await res.text().catch(() => '(no body)');
    throw new Error(`${opts.method ?? 'GET'} ${url} → ${res.status}: ${text}`);
  }

  return res.json() as Promise<T>;
}

// ─── Admin bootstrap ──────────────────────────────────────────────────────────

/**
 * Bootstrap an admin user on a pristine gateway.
 *
 * Steps:
 *  1. GET /api/bootstrap/status — confirms needsSetup === true.
 *  2. POST /api/bootstrap/setup with { name, email, password } — returns
 *     { user, token: { plaintext } }.
 *
 * The harness assumes a fresh DB. If needsSetup is false the harness fails
 * fast with a clear error rather than proceeding with an unknown token.
 */
async function bootstrapAdmin(
  baseUrl: string,
  label: string,
  password: string,
): Promise<BootstrapResult> {
  console.log(`[seed] Bootstrapping admin on ${label} (${baseUrl})...`);

  // 1. Check status
  const statusRes = await fetch(`${baseUrl}/api/bootstrap/status`);
  if (!statusRes.ok) {
    throw new Error(`[seed] GET ${baseUrl}/api/bootstrap/status → ${statusRes.status.toString()}`);
  }
  const status = (await statusRes.json()) as { needsSetup: boolean };

  if (!status.needsSetup) {
    throw new Error(
      `[seed] ${label} at ${baseUrl} already has users (needsSetup=false). ` +
        `The harness requires a pristine database. Run 'docker compose down -v' to reset.`,
    );
  }

  // 2. Bootstrap
  const setupRes = await fetch(`${baseUrl}/api/bootstrap/setup`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: `Harness Admin (${label})`,
      email: `harness-admin-${label.toLowerCase().replace(/\s+/g, '-')}@example.invalid`,
      password,
    }),
  });

  if (!setupRes.ok) {
    const body = await setupRes.text().catch(() => '(no body)');
    throw new Error(
      `[seed] POST ${baseUrl}/api/bootstrap/setup → ${setupRes.status.toString()}: ${body}`,
    );
  }

  const result = (await setupRes.json()) as {
    user: { id: string };
    token: { plaintext: string };
  };

  console.log(`[seed]   ${label} admin user: ${result.user.id}`);
  console.log(`[seed]   ${label} admin token: ${result.token.plaintext.slice(0, 8)}...`);

  return {
    adminUserId: result.user.id,
    adminToken: result.token.plaintext,
  };
}

// ─── Readiness probe ──────────────────────────────────────────────────────────

async function waitForGateway(baseUrl: string, label: string): Promise<void> {
  const deadline = Date.now() + READINESS_TIMEOUT_MS;
  let lastError: string = '';

  while (Date.now() < deadline) {
    try {
      const res = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(5_000) });
      if (res.ok) {
        console.log(`[seed] ${label} is ready (${baseUrl})`);
        return;
      }
      lastError = `HTTP ${res.status.toString()}`;
    } catch (err) {
      lastError = err instanceof Error ? err.message : String(err);
    }
    await new Promise((r) => setTimeout(r, READINESS_POLL_MS));
  }

  throw new Error(
    `[seed] ${label} did not become ready within ${READINESS_TIMEOUT_MS.toString()}ms — last error: ${lastError}`,
  );
}

// ─── Enrollment flow ──────────────────────────────────────────────────────────

/**
 * Walk the full enrollment flow for one grant.
 *
 * The correct two-sided flow (matching the data model's FK semantics):
 *
 *  1. On Server B: POST /api/admin/federation/peers/keypair
 *       → peerId_B (Server B's peer record representing the requesting side)
 *  2. On Server B: POST /api/admin/federation/grants with peerId: peerId_B
 *       → grant (FK to Server B's own federation_peers table — no violation)
 *  3. On Server B: POST /api/admin/federation/grants/:id/tokens
 *       → enrollmentUrl pointing back to Server B
 *  4. On Server A: POST /api/admin/federation/peers/keypair
 *       → peerId_A + csrPem_A (Server A's local record of Server B)
 *  5. Server A → Server B: POST enrollmentUrl with { csrPem: csrPem_A }
 *       → certPem signed by Server B's CA
 *  6. On Server A: PATCH /api/admin/federation/peers/:peerId_A/cert with certPem
 *       → Server A's peer record transitions to active
 *
 * Returns the activated grant (from Server B) and Server A's peer record.
 */
async function enrollGrant(opts: {
  label: string;
  subjectUserId: string;
  scope: unknown;
  adminTokenA: string;
  adminTokenB: string;
  serverAUrl: string;
  serverBUrl: string;
}): Promise<{ grant: GrantRecord; peer: PeerRecord & { grantId: string } }> {
  const { label, subjectUserId, scope, adminTokenA, adminTokenB, serverAUrl, serverBUrl } = opts;
  console.log(`\n[seed] Enrolling grant for scope variant ${label}...`);

  // 1. Create peer keypair on Server B (represents the requesting peer from B's perspective)
  const peerB = await adminFetch<PeerRecord>(serverBUrl, '/api/admin/federation/peers/keypair', {
    method: 'POST',
    adminToken: adminTokenB,
    body: {
      commonName: `harness-peer-${label.toLowerCase()}-from-b`,
      displayName: `Harness Peer ${label} (Server A as seen from B)`,
      endpointUrl: serverAUrl,
    },
  });
  console.log(`[seed]   Created peer on B: ${peerB.peerId}`);

  // 2. Create grant on Server B referencing B's own peer record
  const grant = await adminFetch<GrantRecord>(serverBUrl, '/api/admin/federation/grants', {
    method: 'POST',
    adminToken: adminTokenB,
    body: {
      peerId: peerB.peerId,
      subjectUserId,
      scope,
    },
  });
  console.log(`[seed]   Created grant on B: ${grant.id} (status: ${grant.status})`);

  // 3. Generate enrollment token on Server B
  const tokenResult = await adminFetch<EnrollmentTokenResult>(
    serverBUrl,
    `/api/admin/federation/grants/${grant.id}/tokens`,
    { method: 'POST', adminToken: adminTokenB, body: { ttlSeconds: 900 } },
  );
  console.log(`[seed]   Enrollment token: ${tokenResult.token.slice(0, 8)}...`);
  console.log(`[seed]   Enrollment URL: ${tokenResult.enrollmentUrl}`);

  // 4. Create peer keypair on Server A (Server A's local record of Server B)
  const peerA = await adminFetch<PeerRecord>(serverAUrl, '/api/admin/federation/peers/keypair', {
    method: 'POST',
    adminToken: adminTokenA,
    body: {
      commonName: `harness-peer-${label.toLowerCase()}-from-a`,
      displayName: `Harness Peer ${label} (Server B as seen from A)`,
      endpointUrl: serverBUrl,
    },
  });
  console.log(`[seed]   Created peer on A: ${peerA.peerId}`);

  // 5. Redeem token at Server B's enrollment endpoint with A's CSR.
  //    The enrollment endpoint is not admin-guarded — the one-time token IS the credential.
  //
  //    The enrollmentUrl returned by the gateway is built using BETTER_AUTH_URL which
  //    resolves to the in-cluster Docker hostname (gateway-b:3000). That URL is only
  //    reachable from other containers, not from the host machine running this script.
  //    We rewrite the host portion to use the host-accessible serverBUrl so the
  //    seed script can reach the endpoint from the host.
  const parsedEnrollment = new URL(tokenResult.enrollmentUrl);
  const tokenSegment = parsedEnrollment.pathname.split('/').pop()!;
  const redeemUrl = `${serverBUrl}/api/federation/enrollment/${tokenSegment}`;
  console.log(`[seed]   Rewritten redeem URL (host-accessible): ${redeemUrl}`);
  const redeemRes = await fetch(redeemUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ csrPem: peerA.csrPem }),
  });

  if (!redeemRes.ok) {
    const body = await redeemRes.text().catch(() => '(no body)');
    throw new Error(`Enrollment redemption failed: ${redeemRes.status.toString()} — ${body}`);
  }

  const redeemResult = (await redeemRes.json()) as EnrollmentRedeemResult;
  console.log(`[seed]   Cert issued (${redeemResult.certPem.length.toString()} bytes)`);

  // 6. Store cert on Server A's peer record → transitions to active
  await adminFetch<unknown>(serverAUrl, `/api/admin/federation/peers/${peerA.peerId}/cert`, {
    method: 'PATCH',
    adminToken: adminTokenA,
    body: { certPem: redeemResult.certPem },
  });
  console.log(`[seed]   Cert stored on A — peer ${peerA.peerId} is now active`);

  // Verify grant flipped to active on B
  const activeGrant = await adminFetch<GrantRecord>(
    serverBUrl,
    `/api/admin/federation/grants/${grant.id}`,
    { adminToken: adminTokenB },
  );
  console.log(`[seed]   Grant status on B: ${activeGrant.status}`);

  return { grant: activeGrant, peer: { ...peerA, grantId: grant.id } };
}

// ─── Test data insertion ──────────────────────────────────────────────────────

/**
 * Insert representative test data on Server B via its admin APIs.
 *
 * NOTE: The gateway's task/note/credential APIs require an authenticated user
 * session. For the harness, we seed via admin-level endpoints if available,
 * or document the gap here for M3-11 to fill in with proper user session seeding.
 *
 * ASSUMPTION: Server B exposes POST /api/admin/tasks (or similar) for test data.
 * If that endpoint does not yet exist, this function logs a warning and skips
 * without failing — M3-11 will add the session-based seeding path.
 */
async function seedTestData(
  subjectUserId: string,
  scopeLabel: string,
  serverBUrl: string,
  adminTokenB: string,
): Promise<void> {
  console.log(`\n[seed] Seeding test data on Server B for ${scopeLabel}...`);

  const testTasks = [
    {
      title: `${scopeLabel} Task 1`,
      description: 'Federation harness test task',
      userId: subjectUserId,
    },
    {
      title: `${scopeLabel} Task 2`,
      description: 'Team-scoped test task',
      userId: subjectUserId,
      teamId: 'T1',
    },
  ];

  const testNotes = [
    {
      title: `${scopeLabel} Note 1`,
      content: 'Personal note for federation test',
      userId: subjectUserId,
    },
  ];

  // Attempt to insert — tolerate 404 (endpoint not yet implemented)
  for (const task of testTasks) {
    try {
      await adminFetch<unknown>(serverBUrl, '/api/admin/tasks', {
        method: 'POST',
        adminToken: adminTokenB,
        body: task,
      });
      console.log(`[seed]   Inserted task: "${task.title}"`);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes('404') || msg.includes('Cannot POST')) {
        console.warn(
          `[seed]   WARN: /api/admin/tasks not found — skipping task insertion (expected until M3-11)`,
        );
        break;
      }
      throw err;
    }
  }

  for (const note of testNotes) {
    try {
      await adminFetch<unknown>(serverBUrl, '/api/admin/notes', {
        method: 'POST',
        adminToken: adminTokenB,
        body: note,
      });
      console.log(`[seed]   Inserted note: "${note.title}"`);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes('404') || msg.includes('Cannot POST')) {
        console.warn(
          `[seed]   WARN: /api/admin/notes not found — skipping note insertion (expected until M3-11)`,
        );
        break;
      }
      throw err;
    }
  }

  console.log(`[seed] Test data seeding for ${scopeLabel} complete.`);
}

// ─── Main entrypoint ──────────────────────────────────────────────────────────

export async function runSeed(opts?: {
  serverAUrl?: string;
  serverBUrl?: string;
  adminBootstrapPasswordA?: string;
  adminBootstrapPasswordB?: string;
  subjectUserIds?: { variantA: string; variantB: string; variantC: string };
}): Promise<SeedResult> {
  const aUrl = opts?.serverAUrl ?? SERVER_A_URL;
  const bUrl = opts?.serverBUrl ?? SERVER_B_URL;
  const passwordA = opts?.adminBootstrapPasswordA ?? ADMIN_BOOTSTRAP_PASSWORD_A;
  const passwordB = opts?.adminBootstrapPasswordB ?? ADMIN_BOOTSTRAP_PASSWORD_B;

  // Use provided or default subject user IDs.
  // In a real run these would be real user UUIDs from Server B's DB.
  // For the harness, the admin bootstrap user on Server B is used as the subject.
  // These are overridden after bootstrap if opts.subjectUserIds is not provided.
  const subjectIds = opts?.subjectUserIds;

  console.log('[seed] Waiting for gateways to be ready...');
  await Promise.all([waitForGateway(aUrl, 'Server A'), waitForGateway(bUrl, 'Server B')]);

  // Bootstrap admin users on both gateways (requires pristine DBs).
  console.log('\n[seed] Bootstrapping admin accounts...');
  const [bootstrapA, bootstrapB] = await Promise.all([
    bootstrapAdmin(aUrl, 'Server A', passwordA),
    bootstrapAdmin(bUrl, 'Server B', passwordB),
  ]);

  // Default subject user IDs to the admin user on Server B (guaranteed to exist).
  const resolvedSubjectIds = subjectIds ?? {
    variantA: bootstrapB.adminUserId,
    variantB: bootstrapB.adminUserId,
    variantC: bootstrapB.adminUserId,
  };

  // Enroll all three scope variants sequentially to avoid race conditions on
  // the step-ca signing queue. Parallel enrollment would work too but
  // sequential is easier to debug when something goes wrong.
  console.log('\n[seed] Enrolling scope variants...');
  const resultA = await enrollGrant({
    label: 'A',
    subjectUserId: resolvedSubjectIds.variantA,
    scope: SCOPE_VARIANT_A,
    adminTokenA: bootstrapA.adminToken,
    adminTokenB: bootstrapB.adminToken,
    serverAUrl: aUrl,
    serverBUrl: bUrl,
  });
  const resultB = await enrollGrant({
    label: 'B',
    subjectUserId: resolvedSubjectIds.variantB,
    scope: SCOPE_VARIANT_B,
    adminTokenA: bootstrapA.adminToken,
    adminTokenB: bootstrapB.adminToken,
    serverAUrl: aUrl,
    serverBUrl: bUrl,
  });
  const resultC = await enrollGrant({
    label: 'C',
    subjectUserId: resolvedSubjectIds.variantC,
    scope: SCOPE_VARIANT_C,
    adminTokenA: bootstrapA.adminToken,
    adminTokenB: bootstrapB.adminToken,
    serverAUrl: aUrl,
    serverBUrl: bUrl,
  });

  // Seed test data on Server B for each scope variant
  await Promise.all([
    seedTestData(resolvedSubjectIds.variantA, 'A', bUrl, bootstrapB.adminToken),
    seedTestData(resolvedSubjectIds.variantB, 'B', bUrl, bootstrapB.adminToken),
    seedTestData(resolvedSubjectIds.variantC, 'C', bUrl, bootstrapB.adminToken),
  ]);

  const result: SeedResult = {
    serverAUrl: aUrl,
    serverBUrl: bUrl,
    adminTokenA: bootstrapA.adminToken,
    adminTokenB: bootstrapB.adminToken,
    adminUserIdA: bootstrapA.adminUserId,
    adminUserIdB: bootstrapB.adminUserId,
    grants: {
      variantA: resultA.grant,
      variantB: resultB.grant,
      variantC: resultC.grant,
    },
    peers: {
      variantA: resultA.peer,
      variantB: resultB.peer,
      variantC: resultC.peer,
    },
  };

  console.log('\n[seed] Seed complete.');
  console.log('[seed] Summary:');
  console.log(`  Variant A grant: ${result.grants.variantA.id} (${result.grants.variantA.status})`);
  console.log(`  Variant B grant: ${result.grants.variantB.id} (${result.grants.variantB.status})`);
  console.log(`  Variant C grant: ${result.grants.variantC.id} (${result.grants.variantC.status})`);

  return result;
}

// ─── CLI entry ────────────────────────────────────────────────────────────────

const isCli =
  process.argv[1] != null &&
  fileURLToPath(import.meta.url).endsWith(process.argv[1]!.split('/').pop()!);

if (isCli) {
  const shouldBoot = process.argv.includes('--boot');

  if (shouldBoot) {
    console.log('[seed] --boot flag detected — starting compose stack...');
    execSync(`docker compose -f "${COMPOSE_FILE}" up -d`, { stdio: 'inherit' });
  }

  runSeed()
    .then(() => {
      process.exit(0);
    })
    .catch((err) => {
      console.error('[seed] Fatal:', err);
      process.exit(1);
    });
}
