/**
 * Gateway configuration stage — writes .env + mosaic.config.json, starts the
 * daemon, and waits for it to become healthy.
 *
 * Runs as the penultimate stage of the unified first-run wizard, and is also
 * invoked directly by the `mosaic gateway install` standalone entry point
 * (see `commands/gateway/install.ts`).
 *
 * Idempotency contract:
 *   - If both .env and mosaic.config.json already exist AND the daemon is
 *     running AND meta has an adminToken, we short-circuit with a confirmation
 *     prompt asking whether to re-run the config wizard.
 *   - Partial state (one file present, the other missing) is refused and the
 *     user is told to run `mosaic gateway uninstall` first.
 */

import { randomBytes } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import type { WizardPrompter } from '../prompter/interface.js';
import type { GatewayState, GatewayStorageTier, WizardState } from '../types.js';

// ── Headless detection ────────────────────────────────────────────────────────

function isHeadless(): boolean {
  return process.env['MOSAIC_ASSUME_YES'] === '1' || !process.stdin.isTTY;
}

// ── CORS derivation ───────────────────────────────────────────────────────────

/**
 * Derive a full CORS origin URL from a user-provided hostname + web UI port.
 *
 * Rules:
 *   - "localhost" and "127.0.0.1" always use http (never https)
 *   - Everything else uses https by default; pass useHttps=false to override
 *   - Standard ports (80 for http, 443 for https) are omitted from the origin
 */
export function deriveCorsOrigin(hostname: string, webUiPort: number, useHttps?: boolean): string {
  const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1';
  const proto =
    useHttps !== undefined ? (useHttps ? 'https' : 'http') : isLocalhost ? 'http' : 'https';
  const defaultPort = proto === 'https' ? 443 : 80;
  const portSuffix = webUiPort === defaultPort ? '' : `:${webUiPort.toString()}`;
  return `${proto}://${hostname}${portSuffix}`;
}

// ── .env helpers ──────────────────────────────────────────────────────────────

function readEnvVarFromFile(envFile: string, key: string): string | null {
  if (!existsSync(envFile)) return null;
  try {
    for (const line of readFileSync(envFile, 'utf-8').split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const eqIdx = trimmed.indexOf('=');
      if (eqIdx <= 0) continue;
      if (trimmed.slice(0, eqIdx) !== key) continue;
      return trimmed.slice(eqIdx + 1);
    }
  } catch {
    return null;
  }
  return null;
}

function readPortFromEnv(envFile: string): number | null {
  const raw = readEnvVarFromFile(envFile, 'GATEWAY_PORT');
  if (raw === null) return null;
  const parsed = parseInt(raw, 10);
  return Number.isNaN(parsed) ? null : parsed;
}

// ── Prompt helpers (unified prompter) ────────────────────────────────────────

async function promptTier(p: WizardPrompter): Promise<GatewayStorageTier> {
  const tier = await p.select<GatewayStorageTier>({
    message: 'Storage tier',
    initialValue: 'local',
    options: [
      {
        value: 'local',
        label: 'Local',
        hint: 'embedded database, no dependencies',
      },
      {
        value: 'standalone',
        label: 'Standalone',
        hint: 'PostgreSQL + Valkey required',
      },
      {
        value: 'federated',
        label: 'Federated',
        hint: 'PostgreSQL + Valkey + pgvector, federation server+client',
      },
    ],
  });
  return tier;
}

async function promptPort(p: WizardPrompter, defaultPort: number): Promise<number> {
  const raw = await p.text({
    message: 'Gateway port',
    // initialValue prefills the input buffer so the user sees 14242 and can
    // press Enter to accept it. defaultValue is only used when the user submits
    // an empty string, which never shows in the field.
    initialValue: defaultPort.toString(),
    defaultValue: defaultPort.toString(),
    validate: (v) => {
      const n = parseInt(v, 10);
      if (Number.isNaN(n) || n < 1 || n > 65535) return 'Port must be a number between 1 and 65535';
      return undefined;
    },
  });
  return parseInt(raw, 10);
}

// ── Options ───────────────────────────────────────────────────────────────────

export interface GatewayConfigStageOptions {
  /** Gateway host (from CLI flag or meta fallback). Defaults to localhost. */
  host: string;
  /** Default port when nothing else is set. */
  defaultPort?: number;
  /**
   * Explicit port override from the caller (e.g. `mosaic gateway install
   * --port 9999`). When set, this value wins over the port stored in an
   * existing `.env` / meta.json so users can recover from a conflicting
   * saved port without deleting config files first.
   */
  portOverride?: number;
  /** Skip the `npm install -g @mosaicstack/gateway` step (local build / tests). */
  skipInstall?: boolean;
  /**
   * Pre-collected provider API key (from the provider-setup stage or Quick
   * Start path). When set, the gateway-config stage will skip the interactive
   * API key prompt and use this value directly.
   */
  providerKey?: string;
  /** Provider type detected from the key prefix. */
  providerType?: 'anthropic' | 'openai' | 'none';
}

export interface GatewayConfigStageResult {
  /** `true` when the daemon is running, healthy, and `meta.json` is current. */
  ready: boolean;
  /** Populated when ready — caller uses this for the bootstrap stage. */
  host?: string;
  port?: number;
}

// ── Stage ─────────────────────────────────────────────────────────────────────

export async function gatewayConfigStage(
  p: WizardPrompter,
  state: WizardState,
  opts: GatewayConfigStageOptions,
): Promise<GatewayConfigStageResult> {
  // Ensure gateway modules resolve against the correct MOSAIC_GATEWAY_HOME
  // before any dynamic import — the daemon module captures paths at import
  // time from process.env.
  const defaultMosaicHome = join(process.env['HOME'] ?? '', '.config', 'mosaic');
  if (state.mosaicHome !== defaultMosaicHome && !process.env['MOSAIC_GATEWAY_HOME']) {
    process.env['MOSAIC_GATEWAY_HOME'] = join(state.mosaicHome, 'gateway');
  }

  const {
    ENV_FILE,
    GATEWAY_HOME,
    LOG_FILE,
    ensureDirs,
    getDaemonPid,
    installGatewayPackage,
    readMeta,
    resolveGatewayEntry,
    startDaemon,
    stopDaemon,
    waitForHealth,
    writeMeta,
    getInstalledGatewayVersion,
  } = await import('../commands/gateway/daemon.js');

  const MOSAIC_CONFIG_FILE = join(GATEWAY_HOME, 'mosaic.config.json');

  p.separator();

  const existing = readMeta();
  const envExists = existsSync(ENV_FILE);
  const mosaicConfigExists = existsSync(MOSAIC_CONFIG_FILE);
  let hasConfig = envExists && mosaicConfigExists;
  let daemonRunning = getDaemonPid() !== null;
  const hasAdminToken = Boolean(existing?.adminToken);

  const defaultPort = opts.defaultPort ?? 14242;
  const host = opts.host;

  // If the caller explicitly asked for a port that differs from the saved
  // .env port, force config regeneration. Otherwise meta.json and .env would
  // drift: the daemon still binds to the saved GATEWAY_PORT while meta +
  // health checks believe the daemon is on the override port.
  //
  // We track this as a separate `forcePortRegen` flag so the corrupt-
  // partial-state guard below does not mistake an intentional override
  // regeneration for half-written config from a crashed install.
  let forcePortRegen = false;
  if (hasConfig && opts.portOverride !== undefined) {
    const savedPort = readPortFromEnv(ENV_FILE);
    if (savedPort !== null && savedPort !== opts.portOverride) {
      p.log(
        `Port override (${opts.portOverride.toString()}) differs from saved GATEWAY_PORT=${savedPort.toString()} — regenerating config.`,
      );
      hasConfig = false;
      forcePortRegen = true;
    }
  }

  // Corrupt partial state — refuse. (Skip when we intentionally forced
  // regeneration due to a port-override mismatch; in that case both files
  // are present and `hasConfig` was deliberately cleared.)
  if ((envExists || mosaicConfigExists) && !hasConfig && !forcePortRegen) {
    p.warn('Gateway install is in a corrupt partial state:');
    p.log(`  .env file:            ${envExists ? 'present' : 'MISSING'} (${ENV_FILE})`);
    p.log(
      `  mosaic.config.json:   ${mosaicConfigExists ? 'present' : 'MISSING'} (${MOSAIC_CONFIG_FILE})`,
    );
    p.log('\nRun `mosaic gateway uninstall` to clean up, then re-run install.');
    return { ready: false };
  }

  // Already fully installed path — ask whether to re-run config.
  let explicitReinstall = false;
  if (existing && hasConfig && daemonRunning && hasAdminToken) {
    p.note(
      [
        `Gateway is already installed and running (v${existing.version}).`,
        `  Endpoint:  http://${existing.host}:${existing.port.toString()}`,
        `  Status:    mosaic gateway status`,
        '',
        'Re-running the config wizard will:',
        '  - regenerate .env and mosaic.config.json',
        '  - restart the daemon',
        '  - preserve BETTER_AUTH_SECRET (sessions stay valid)',
        '  - clear the stored admin token (you will re-bootstrap an admin user)',
        '  - allow changing storage tier / DB URLs (may point at a different data store)',
        'To wipe persisted data, run `mosaic gateway uninstall` first.',
      ].join('\n'),
      'Gateway already installed',
    );

    const rerun = await p.confirm({
      message: 'Re-run config wizard?',
      initialValue: false,
    });
    if (!rerun) {
      // Not rewriting config — the daemon is still listening on
      // `existing.port`, so downstream callers must use that even if the
      // user passed a --port override. An override only applies when the
      // user agrees to a rerun (handled in the regeneration branch below).
      state.gateway = {
        host: existing.host,
        port: existing.port,
        tier: 'local',
        corsOrigin:
          readEnvVarFromFile(ENV_FILE, 'GATEWAY_CORS_ORIGIN') ??
          deriveCorsOrigin('localhost', 3000),
        regeneratedConfig: false,
      };
      return { ready: true, host: existing.host, port: existing.port };
    }
    hasConfig = false;
    explicitReinstall = true;
  } else if (existing && (hasConfig || daemonRunning)) {
    p.log('Detected a partial gateway installation — resuming setup.\n');
  }

  // Stop daemon before rewriting config.
  if (!hasConfig && daemonRunning) {
    p.log('Stopping gateway daemon before writing new config...');
    try {
      await stopDaemon();
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (!/not running/i.test(msg)) {
        p.warn(`Failed to stop running daemon: ${msg}`);
        p.warn('Refusing to rewrite config while an unknown-state daemon is running.');
        return { ready: false };
      }
    }
    if (getDaemonPid() !== null) {
      p.warn('Gateway daemon is still running after stop attempt. Aborting.');
      return { ready: false };
    }
    daemonRunning = false;
  }

  // Install the gateway npm package on first install or after failure.
  if (!opts.skipInstall && !daemonRunning) {
    installGatewayPackage();
  }

  ensureDirs();

  // Collect configuration.
  const regeneratedConfig = !hasConfig;
  let port: number;
  let gatewayState: GatewayState;

  if (hasConfig) {
    const envPort = readPortFromEnv(ENV_FILE);
    // Explicit --port override wins even on resume so users can recover from
    // a conflicting saved port without wiping config first.
    port = opts.portOverride ?? envPort ?? existing?.port ?? defaultPort;
    p.log(`Using existing config at ${ENV_FILE} (port ${port.toString()})`);
    gatewayState = {
      host,
      port,
      tier: 'local',
      corsOrigin:
        readEnvVarFromFile(ENV_FILE, 'GATEWAY_CORS_ORIGIN') ?? deriveCorsOrigin('localhost', 3000),
      regeneratedConfig: false,
    };
  } else {
    try {
      gatewayState = await collectAndWriteConfig(p, {
        host,
        defaultPort: opts.portOverride ?? defaultPort,
        envFile: ENV_FILE,
        mosaicConfigFile: MOSAIC_CONFIG_FILE,
        gatewayHome: GATEWAY_HOME,
        providerKey: opts.providerKey,
        providerType: opts.providerType,
      });
    } catch (err) {
      if (err instanceof GatewayConfigValidationError) {
        p.warn(err.message);
        return { ready: false };
      }
      throw err;
    }
    port = gatewayState.port;
  }

  state.gateway = gatewayState;

  // Write meta.json.
  let entryPoint: string;
  try {
    entryPoint = resolveGatewayEntry();
  } catch {
    p.warn(
      'Gateway package not found after install. Check that @mosaicstack/gateway installed correctly.',
    );
    return { ready: false };
  }

  const version = getInstalledGatewayVersion() ?? 'unknown';
  const preserveToken = !regeneratedConfig && Boolean(existing?.adminToken);
  const meta = {
    version,
    installedAt: explicitReinstall
      ? new Date().toISOString()
      : (existing?.installedAt ?? new Date().toISOString()),
    entryPoint,
    host,
    port,
    ...(preserveToken && existing?.adminToken ? { adminToken: existing.adminToken } : {}),
  };
  writeMeta(meta);

  // Start the daemon.
  if (!daemonRunning) {
    p.log('Starting gateway daemon...');
    try {
      const pid = startDaemon();
      p.log(`Gateway started (PID ${pid.toString()})`);
    } catch (err) {
      p.warn(`Failed to start: ${err instanceof Error ? err.message : String(err)}`);
      printLogTailViaPrompter(p, LOG_FILE);
      return { ready: false };
    }
  } else {
    p.log('Gateway daemon is already running.');
  }

  // Wait for health.
  p.log('Waiting for gateway to become healthy...');
  const healthy = await waitForHealth(host, port, 30_000);
  if (!healthy) {
    p.warn('Gateway did not become healthy within 30 seconds.');
    printLogTailViaPrompter(p, LOG_FILE);
    p.warn('Fix the underlying error above, then re-run `mosaic gateway install`.');
    return { ready: false };
  }
  p.log('Gateway is healthy.');

  return { ready: true, host, port };
}

// ── Config collection ─────────────────────────────────────────────────────────

interface CollectOptions {
  host: string;
  defaultPort: number;
  envFile: string;
  mosaicConfigFile: string;
  gatewayHome: string;
  /** Pre-collected API key — skips the interactive prompt when set. */
  providerKey?: string;
  /** Provider type — determines the env var name for the key. */
  providerType?: 'anthropic' | 'openai' | 'none';
}

/** Raised by the config stage when headless env validation fails. */
export class GatewayConfigValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'GatewayConfigValidationError';
  }
}

async function collectAndWriteConfig(
  p: WizardPrompter,
  opts: CollectOptions,
): Promise<GatewayState> {
  p.note('Collecting gateway configuration', 'Gateway Configuration');

  // Preserve existing BETTER_AUTH_SECRET if an .env survives on disk.
  const preservedAuthSecret = readEnvVarFromFile(opts.envFile, 'BETTER_AUTH_SECRET');
  if (preservedAuthSecret) {
    p.log('(Preserving existing BETTER_AUTH_SECRET — auth sessions will remain valid.)');
  }

  let tier: GatewayStorageTier;
  let port: number;
  let databaseUrl: string | undefined;
  let valkeyUrl: string | undefined;
  let anthropicKey: string;
  let corsOrigin: string;
  let hostname: string;

  if (isHeadless()) {
    p.log('Headless mode detected — reading configuration from environment variables.');

    const storageTierEnv = process.env['MOSAIC_STORAGE_TIER'] ?? 'local';
    if (storageTierEnv === 'team') {
      // Deprecated alias — warn and treat as standalone
      process.stderr.write(
        '[mosaic] DEPRECATED: MOSAIC_STORAGE_TIER=team is deprecated — use "standalone" instead.\n',
      );
      tier = 'standalone';
    } else if (storageTierEnv === 'standalone' || storageTierEnv === 'federated') {
      tier = storageTierEnv;
    } else if (storageTierEnv !== '' && storageTierEnv !== 'local') {
      throw new GatewayConfigValidationError(
        `Invalid MOSAIC_STORAGE_TIER="${storageTierEnv}" — expected "local", "standalone", or "federated" (deprecated alias "team" also accepted)`,
      );
    } else {
      tier = 'local';
    }

    const portEnv = process.env['MOSAIC_GATEWAY_PORT'];
    port = portEnv ? parseInt(portEnv, 10) : opts.defaultPort;

    databaseUrl = process.env['MOSAIC_DATABASE_URL'];
    valkeyUrl = process.env['MOSAIC_VALKEY_URL'];
    anthropicKey = process.env['MOSAIC_ANTHROPIC_API_KEY'] ?? '';

    // MOSAIC_CORS_ORIGIN is the full override (e.g. from CI).
    // MOSAIC_HOSTNAME is the user-friendly alternative — derive from it.
    const corsOverride = process.env['MOSAIC_CORS_ORIGIN'];
    const hostnameEnv = process.env['MOSAIC_HOSTNAME'] ?? 'localhost';
    hostname = hostnameEnv;
    corsOrigin = corsOverride ?? deriveCorsOrigin(hostnameEnv, 3000);

    if (tier === 'standalone' || tier === 'federated') {
      const missing: string[] = [];
      if (!databaseUrl) missing.push('MOSAIC_DATABASE_URL');
      if (!valkeyUrl) missing.push('MOSAIC_VALKEY_URL');
      if (missing.length > 0) {
        throw new GatewayConfigValidationError(
          `Headless install with tier=${tier} requires env vars: ` + missing.join(', '),
        );
      }
    }
  } else {
    tier = await promptTier(p);
    port = await promptPort(p, opts.defaultPort);

    if (tier === 'standalone' || tier === 'federated') {
      const defaultDbUrl =
        tier === 'federated'
          ? 'postgresql://mosaic:mosaic@localhost:5433/mosaic'
          : 'postgresql://mosaic:mosaic@localhost:5432/mosaic';
      databaseUrl = await p.text({
        message: 'DATABASE_URL',
        initialValue: defaultDbUrl,
        defaultValue: defaultDbUrl,
      });
      valkeyUrl = await p.text({
        message: 'VALKEY_URL',
        initialValue: 'redis://localhost:6380',
        defaultValue: 'redis://localhost:6380',
      });
    }

    if (opts.providerKey) {
      anthropicKey = opts.providerKey;
      p.log(`Using API key from provider setup (${opts.providerType ?? 'unknown'}).`);
    } else {
      anthropicKey = await p.text({
        message: 'ANTHROPIC_API_KEY (optional, press Enter to skip)',
        defaultValue: '',
      });
    }

    hostname = await p.text({
      message: 'Web UI hostname (for browser access)',
      initialValue: 'localhost',
      defaultValue: 'localhost',
      placeholder: 'e.g. localhost or myserver.example.com',
    });

    // For non-localhost, ask if HTTPS is in use (defaults to yes for remote hosts)
    let useHttps: boolean | undefined;
    if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
      useHttps = await p.confirm({
        message: 'Is HTTPS enabled for the web UI?',
        initialValue: true,
      });
    }

    corsOrigin = deriveCorsOrigin(hostname, 3000, useHttps);
    p.log(`CORS origin set to: ${corsOrigin}`);
  }

  const authSecret = preservedAuthSecret ?? randomBytes(32).toString('hex');

  const envLines = [
    `GATEWAY_PORT=${port.toString()}`,
    `BETTER_AUTH_SECRET=${authSecret}`,
    `BETTER_AUTH_URL=http://${opts.host}:${port.toString()}`,
    `GATEWAY_CORS_ORIGIN=${corsOrigin}`,
    `OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318`,
    `OTEL_SERVICE_NAME=mosaic-gateway`,
  ];

  if ((tier === 'standalone' || tier === 'federated') && databaseUrl && valkeyUrl) {
    envLines.push(`DATABASE_URL=${databaseUrl}`);
    envLines.push(`VALKEY_URL=${valkeyUrl}`);
  }

  if (anthropicKey) {
    if (opts.providerType === 'openai') {
      envLines.push(`OPENAI_API_KEY=${anthropicKey}`);
    } else {
      envLines.push(`ANTHROPIC_API_KEY=${anthropicKey}`);
    }
  }

  writeFileSync(opts.envFile, envLines.join('\n') + '\n', { mode: 0o600 });
  p.log(`Config written to ${opts.envFile}`);

  const mosaicConfig =
    tier === 'local'
      ? {
          tier: 'local',
          storage: { type: 'pglite', dataDir: join(opts.gatewayHome, 'storage-pglite') },
          queue: { type: 'local', dataDir: join(opts.gatewayHome, 'queue') },
          memory: { type: 'keyword' },
        }
      : tier === 'federated'
        ? {
            tier: 'federated',
            storage: { type: 'postgres', url: databaseUrl },
            queue: { type: 'bullmq', url: valkeyUrl },
            memory: { type: 'pgvector' },
          }
        : {
            tier: 'standalone',
            storage: { type: 'postgres', url: databaseUrl },
            queue: { type: 'bullmq', url: valkeyUrl },
            memory: { type: 'keyword' },
          };

  writeFileSync(opts.mosaicConfigFile, JSON.stringify(mosaicConfig, null, 2) + '\n', {
    mode: 0o600,
  });
  p.log(`Config written to ${opts.mosaicConfigFile}`);

  return {
    host: opts.host,
    port,
    tier,
    databaseUrl,
    valkeyUrl,
    anthropicKey: anthropicKey || undefined,
    corsOrigin,
    hostname,
    regeneratedConfig: true,
  };
}

// ── Log tail ──────────────────────────────────────────────────────────────────

function printLogTailViaPrompter(p: WizardPrompter, logFile: string, maxLines = 30): void {
  if (!existsSync(logFile)) {
    p.warn(`(no log file at ${logFile})`);
    return;
  }
  try {
    const lines = readFileSync(logFile, 'utf-8')
      .split('\n')
      .filter((l) => l.trim().length > 0);
    const tail = lines.slice(-maxLines);
    if (tail.length === 0) {
      p.warn('(log file is empty)');
      return;
    }
    p.note(tail.join('\n'), `Last ${tail.length.toString()} log lines`);
  } catch (err) {
    p.warn(`Could not read log file: ${err instanceof Error ? err.message : String(err)}`);
  }
}
