/**
 * End-to-end attach smoke against PROD infrastructure.
 *
 * What this exercises (against real Cloud DNS + sandbox OpenSRS + real Coolify):
 *   1. registerDomain        → OpenSRS Horizon sandbox creates a fake but
 *                              protocol-correct registration (no $$).
 *   2. cloudDnsProvider.createZone   → real public managed zone in
 *                              master-ai-484822 (anycast DNS, costs ~$0.20/mo
 *                              while it lives — we delete on cleanup).
 *   3. cloudDnsProvider.setRecords   → A records for apex + www → 34.19.250.135.
 *   4. updateDomainNameservers → tells the OpenSRS sandbox to point the test
 *                              domain at the Cloud DNS NS set. Triggers the
 *                              unlock/relock fallback if the registry returns
 *                              405 — proves the recovery path works.
 *   5. Coolify PATCH         → adds the new fqdn to a real Coolify app's
 *                              domain list (target: missinglettr-test, which
 *                              is already on a sslip.io URL so no SSL/Traefik
 *                              fallout from a vanity hostname).
 *
 * What it deliberately SKIPS:
 *   - DB writes (vibn_domains row + events) — those require prod DB which is
 *     on the internal Docker network. The lib code that does those writes is
 *     covered by unit-style call sites; what we're proving here is the
 *     external-side-effect surface that's hard to test from CI.
 *
 * Cleanup (best-effort) at the end:
 *   - Removes the test fqdn from the Coolify app's domain list.
 *   - Deletes the Cloud DNS managed zone.
 *
 * The sandbox OpenSRS domain expires on its own — no cleanup needed.
 *
 * Required env (load from .opensrs.env + .coolify.env + .gcp.env):
 *   OPENSRS_RESELLER_USERNAME, OPENSRS_API_KEY_TEST, OPENSRS_MODE=test
 *   COOLIFY_URL, COOLIFY_API_TOKEN
 *   GOOGLE_SERVICE_ACCOUNT_KEY_B64 (or GOOGLE_APPLICATION_CREDENTIALS pointing at the JSON)
 *   GCP_PROJECT_ID (defaults to master-ai-484822)
 *
 * Usage:
 *   set -a && source ../.opensrs.env && source ../.coolify.env && source ../.gcp.env && set +a
 *   npx tsx scripts/smoke-attach-e2e.ts
 */

import { registerDomain, updateDomainNameservers, setDomainLock, OpenSrsError, type RegistrationContact } from '../lib/opensrs';
import { cloudDnsProvider } from '../lib/dns/cloud-dns';
import { getGcpAccessToken, GCP_PROJECT_ID } from '../lib/gcp-auth';

const COOLIFY_URL = process.env.COOLIFY_URL ?? '';
const COOLIFY_API_TOKEN = process.env.COOLIFY_API_TOKEN ?? '';

// missinglettr-test — pre-existing sslip.io test app on the Vibn server.
// Safe target: no public DNS, no Let's Encrypt issuance for vanity hostnames.
const TARGET_APP_UUID = 'nksoo0cscw48gwo4ggc4g4kk';
const TARGET_APP_PUBLIC_IP = '34.19.250.135';

const CONTACT: RegistrationContact = {
  first_name: 'Mark',
  last_name: 'Henderson',
  org_name: 'Get Acquired Inc',
  address1: '123 King St W',
  city: 'Toronto',
  state: 'ON',
  country: 'CA',
  postal_code: 'M5H 1A1',
  phone: '+1.4165551234',
  email: 'mark@getacquired.com',
};

interface CoolifyApp {
  uuid: string;
  name: string;
  fqdn?: string;
  domains?: string;
}

async function coolifyGetApp(uuid: string): Promise<CoolifyApp> {
  const res = await fetch(`${COOLIFY_URL}/api/v1/applications/${uuid}`, {
    headers: { Authorization: `Bearer ${COOLIFY_API_TOKEN}` },
  });
  if (!res.ok) throw new Error(`coolify GET app ${uuid} → ${res.status}: ${await res.text()}`);
  return res.json() as Promise<CoolifyApp>;
}

async function coolifySetAppDomains(uuid: string, domainsCsv: string): Promise<void> {
  // Mirror the production lib/coolify.ts setApplicationDomains() body.
  // Send `domains` (the controller maps it to the DB's `fqdn` column when the
  // destination server has Server::isProxyShouldRun() === true — i.e.
  // proxy.type=TRAEFIK|CADDY AND is_build_server=false). If the PATCH
  // returns 200 but the fqdn doesn't change, the destination server's
  // proxy/build-server config is the most likely cause.
  const res = await fetch(`${COOLIFY_URL}/api/v1/applications/${uuid}`, {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${COOLIFY_API_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      domains: domainsCsv,
      force_domain_override: true,
      is_force_https_enabled: true,
    }),
  });
  if (!res.ok) throw new Error(`coolify PATCH app ${uuid} → ${res.status}: ${await res.text()}`);
}

function parseDomains(s: string | undefined): string[] {
  if (!s) return [];
  return s
    .split(/[,\s]+/)
    .map(x => x.trim())
    .filter(Boolean);
}

async function main() {
  // Reject anything that would let us hit live OpenSRS by accident.
  const mode = process.env.OPENSRS_MODE ?? 'test';
  if (mode !== 'test') {
    throw new Error(`Refusing to run with OPENSRS_MODE=${mode}. Set OPENSRS_MODE=test for sandbox-proof.`);
  }
  if (!COOLIFY_URL || !COOLIFY_API_TOKEN) throw new Error('COOLIFY_URL + COOLIFY_API_TOKEN required');
  if (!process.env.GOOGLE_SERVICE_ACCOUNT_KEY_B64 && !process.env.GOOGLE_APPLICATION_CREDENTIALS) {
    throw new Error('GOOGLE_SERVICE_ACCOUNT_KEY_B64 (or GOOGLE_APPLICATION_CREDENTIALS) required');
  }

  const apex = `vibnai-e2e-${Date.now()}.com`;
  const subs = ['@', 'www'];
  const fqdns = subs.map(s => (s === '@' ? `https://${apex}` : `https://${s}.${apex}`));

  console.log('━'.repeat(70));
  console.log(' VIBN P5.1 attach end-to-end (PROD GCP + sandbox OpenSRS + PROD Coolify)');
  console.log('━'.repeat(70));
  console.log(' apex      :', apex);
  console.log(' target    : Coolify app', TARGET_APP_UUID, '(missinglettr-test)');
  console.log(' target IP :', TARGET_APP_PUBLIC_IP);
  console.log('');

  // ── Step 0: capture the Coolify app's current domains so we can restore later
  console.log('[0/6] Snapshot Coolify app current state…');
  const appBefore = await coolifyGetApp(TARGET_APP_UUID);
  const domainsBefore = parseDomains(appBefore.domains ?? appBefore.fqdn ?? '');
  console.log('      app name        :', appBefore.name);
  console.log('      domains before  :', domainsBefore.length, '→', JSON.stringify(domainsBefore.slice(0, 3)));

  // ── Step 1: register sandbox domain
  console.log('\n[1/6] Register sandbox domain via OpenSRS Horizon…');
  const reg = await registerDomain({ domain: apex, period: 1, contact: CONTACT, whoisPrivacy: true });
  console.log('      ✓ orderId        :', reg.orderId);
  console.log('      ✓ responseCode   :', reg.responseCode, '/', reg.responseText);

  // ── Step 2: create Cloud DNS managed zone
  console.log('\n[2/6] Create Cloud DNS managed zone (PROD master-ai-484822)…');
  const zone = await cloudDnsProvider.createZone(apex);
  console.log('      ✓ zoneId         :', zone.zoneId);
  console.log('      ✓ nameservers    :', zone.nameservers.join(', '));

  // ── Step 3: write A records (apex + www → 34.19.250.135)
  console.log('\n[3/6] Write A records to Cloud DNS…');
  await cloudDnsProvider.setRecords(apex, [
    { name: '@',   type: 'A', rrdatas: [TARGET_APP_PUBLIC_IP], ttl: 300 },
    { name: 'www', type: 'A', rrdatas: [TARGET_APP_PUBLIC_IP], ttl: 300 },
  ]);
  console.log('      ✓ A @            →', TARGET_APP_PUBLIC_IP);
  console.log('      ✓ A www          →', TARGET_APP_PUBLIC_IP);

  // ── Step 4: update registrar nameservers (with unlock/relock fallback)
  //
  // 4a. Try with real Cloud DNS nameservers. In sandbox mode this is expected
  //     to fail with 480 ("nameserver doesn't exist at the registry") because
  //     OpenSRS Horizon mocks a registry that only knows pre-seeded NS hosts.
  //     The point of trying anyway is to prove the trailing-dot normalization
  //     works (otherwise we'd see a 460 "Invalid nameserver host" before the
  //     registry even gets consulted). 480 == validation passed, registry
  //     check failed → in live mode, real registries accept any resolvable NS.
  //
  // 4b. Re-run against `ns{1,2}.systemdns.com` (which Horizon recognizes) so
  //     we also exercise the success path including the unlock/relock fallback.
  console.log('\n[4/6] Update registrar nameservers via OpenSRS…');
  let cloudNsResult: { responseCode: string; responseText: string } | string;
  try {
    cloudNsResult = await updateDomainNameservers(apex, zone.nameservers);
    console.log('      ✓ 4a real Cloud DNS NS accepted:', cloudNsResult.responseCode);
  } catch (err) {
    if (err instanceof OpenSrsError && err.code === '480') {
      cloudNsResult = '480-sandbox-mock-registry-rejected (expected — live mode uses real registry)';
      console.log('      ✓ 4a Cloud DNS NS validation passed; sandbox mock-registry rejection (480) is expected');
    } else {
      throw err;
    }
  }

  console.log('      → 4b retry with sandbox-recognized NS (ns{1,2}.systemdns.com) to exercise success path…');
  const sandboxNs = ['ns1.systemdns.com', 'ns2.systemdns.com'];
  let nsResult: { responseCode: string; responseText: string } | null = null;
  let nsPath: 'direct' | 'unlock-retry-relock' = 'direct';
  try {
    nsResult = await updateDomainNameservers(apex, sandboxNs);
    console.log('      ✓ 4b direct NS update succeeded:', nsResult.responseCode);
  } catch (err) {
    if (
      err instanceof OpenSrsError &&
      err.code === '405' &&
      /status prohibits operation/i.test(err.message)
    ) {
      console.log('      ↻ 4b 405 lock conflict → unlocking, retrying, then relocking');
      nsPath = 'unlock-retry-relock';
      await setDomainLock(apex, false);
      nsResult = await updateDomainNameservers(apex, sandboxNs);
      console.log('      ✓ 4b retry NS update succeeded:', nsResult.responseCode);
      try {
        const relock = await setDomainLock(apex, true);
        console.log('      ✓ 4b relocked        :', relock.responseCode);
      } catch (relockErr) {
        console.warn('      ⚠ relock failed (non-fatal):', relockErr instanceof Error ? relockErr.message : relockErr);
      }
    } else {
      throw err;
    }
  }

  // ── Step 5: PATCH Coolify app domain list
  console.log('\n[5/6] Add fqdns to Coolify app domain list…');
  const merged = Array.from(new Set([...domainsBefore, ...fqdns]));
  await coolifySetAppDomains(TARGET_APP_UUID, merged.join(','));
  const appAfter = await coolifyGetApp(TARGET_APP_UUID);
  const domainsAfter = parseDomains(appAfter.domains ?? appAfter.fqdn ?? '');
  console.log('      ✓ PATCH accepted (HTTP 200, no error)');
  console.log('      ✓ app domains now:', domainsAfter.length);
  let coolifyApplied = true;
  for (const fq of fqdns) {
    const present = domainsAfter.includes(fq);
    console.log('         ', present ? '✓' : '✗', fq);
    if (!present) coolifyApplied = false;
  }
  if (!coolifyApplied) {
    console.log('      ✗ Coolify still did not persist the fqdn — unexpected. Previously this was');
    console.log('         caused by sending `domains` instead of `fqdn` (API alias vs model column).');
    console.log('         We now send `fqdn`; if this branch fires, dig into the PATCH response body.');
  }

  // ── Step 6: cleanup
  console.log('\n[6/6] Cleanup (remove fqdns from Coolify, delete Cloud DNS zone)…');
  await coolifySetAppDomains(TARGET_APP_UUID, domainsBefore.join(','));
  console.log('      ✓ Coolify domains restored');

  // Cloud DNS rejects deleteZone if any non-SOA/NS rrsets remain. List them
  // and submit a deletion change for everything except SOA + the apex NS.
  const zoneApiName = `vibn-${apex.toLowerCase().replace(/[^a-z0-9-]/g, '-')}`;
  const token = await getGcpAccessToken();
  const listRes = await fetch(
    `https://dns.googleapis.com/dns/v1/projects/${GCP_PROJECT_ID}/managedZones/${zoneApiName}/rrsets`,
    { headers: { Authorization: `Bearer ${token}` } },
  );
  const listJson = (await listRes.json()) as { rrsets?: Array<{ name: string; type: string; ttl?: number; rrdatas: string[] }> };
  const dnsName = `${apex}.`;
  const toDelete = (listJson.rrsets ?? []).filter(rs => !(rs.name === dnsName && (rs.type === 'SOA' || rs.type === 'NS')));
  if (toDelete.length > 0) {
    const dropRes = await fetch(
      `https://dns.googleapis.com/dns/v1/projects/${GCP_PROJECT_ID}/managedZones/${zoneApiName}/changes`,
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ kind: 'dns#change', deletions: toDelete }),
      },
    );
    if (!dropRes.ok) console.warn('      ⚠ rrset cleanup failed:', dropRes.status, (await dropRes.text()).slice(0, 200));
    else console.log('      ✓ dropped', toDelete.length, 'rrset(s)');
  }
  await cloudDnsProvider.deleteZone(apex);
  console.log('      ✓ Cloud DNS zone deleted');

  console.log('\n━'.repeat(35));
  console.log(' SUMMARY');
  console.log('━'.repeat(70));
  console.log(' OpenSRS register     : ✓ orderId', reg.orderId);
  console.log(' Cloud DNS zone       : ✓', zone.zoneId);
  console.log(' Cloud DNS rrsets     : ✓ A @ + A www');
  console.log(' Registrar NS (real)  : ' + (typeof cloudNsResult === 'string' ? cloudNsResult : '✓ ' + cloudNsResult.responseCode));
  console.log(' Registrar NS (mock)  : ✓ via', nsPath, '(', nsResult?.responseCode, ')');
  console.log(' Coolify domain PATCH : ' + (coolifyApplied
    ? '✓ added ' + fqdns.length + ' fqdns'
    : '✗ fqdn did not persist — PATCH returned 200 but column unchanged. '
      + 'Check destination server: proxy.type must be TRAEFIK|CADDY and '
      + 'is_build_server must be false (Server::isProxyShouldRun()).'));
  console.log(' Cleanup              : ✓ Coolify restored, zone deleted');
  console.log('');
  if (coolifyApplied) {
    console.log(' ALL 5 PROD SUB-SYSTEMS PROVEN END-TO-END.');
  } else {
    console.log(' 4 of 5 PROD SUB-SYSTEMS PROVEN. Coolify fqdn update still failing —');
    console.log(' inspect the PATCH response body + Application.$fillable in the running image.');
  }
}

main().catch(err => {
  console.error('\n[smoke-attach-e2e] FAILED:', err);
  process.exit(1);
});
