import { prisma } from "../lib/db";
import { decrypt } from "../lib/encryption";
import { connectSsh, type SshSession } from "./ssh";
import { createLogger } from "../lib/logger";

const log = createLogger("APPLOGS");

// The live app-log stream is deliberately separate from a preview's persisted
// `logs` (which hold the PP deploy engine's own output). The running app's
// stdout/stderr lives on the EC2 instance — in /opt/app/pp.log for a plain
// process run, or in the container logs for docker-compose mode — so we tail it
// on demand over an SSH channel while clients are watching. Nothing here is
// written to the database; it's a passthrough to the WebSocket.
//
// A single tail per preview is shared (multiplexed) across all connected
// viewers: the first subscriber opens the SSH connection, later ones attach to
// it, and the connection is torn down once the last viewer disconnects. Recent
// output is kept in a ring buffer so a late joiner sees history immediately
// instead of a blank pane until the next line arrives.

function tailCommand(useDockerCompose: boolean, composeFilePath: string | null): string {
  if (useDockerCompose) {
    const composePath = composeFilePath || "docker-compose.yml";
    // `--tail=200` seeds recent history so the viewer isn't blank on connect;
    // `-f` follows new output. Compose writes container logs to stdout/stderr,
    // both of which execStream captures.
    return `cd /opt/app && sudo docker compose -f '${composePath.replace(/'/g, `'"'"'`)}' logs -f --tail=200`;
  }
  // `-F` (not `-f`) follows by name and retries if the file is missing or gets
  // rotated — the app may not have written pp.log yet when a client connects.
  return `tail -n 200 -F /opt/app/pp.log 2>&1`;
}

const RING_BUFFER_BYTES = 64 * 1024;

interface StreamEntry {
  subscribers: Set<(chunk: string) => void>;
  buffer: string[]; // recent chunks, replayed to late joiners
  bufferBytes: number;
  connecting: Promise<void>;
  session: SshSession | null;
  streamHandle: { close(): void } | null;
  torndown: boolean;
}

const streams = new Map<number, StreamEntry>();

function pushToBuffer(entry: StreamEntry, chunk: string) {
  entry.buffer.push(chunk);
  entry.bufferBytes += Buffer.byteLength(chunk, "utf8");
  while (entry.bufferBytes > RING_BUFFER_BYTES && entry.buffer.length > 1) {
    const dropped = entry.buffer.shift()!;
    entry.bufferBytes -= Buffer.byteLength(dropped, "utf8");
  }
}

function broadcast(entry: StreamEntry, chunk: string) {
  pushToBuffer(entry, chunk);
  for (const cb of entry.subscribers) {
    try { cb(chunk); } catch {}
  }
}

async function openStream(previewId: number, entry: StreamEntry) {
  const preview = await prisma.preview.findUnique({
    where: { id: previewId },
    include: { repoConfig: true },
  });

  if (!preview || !preview.instanceIp || !preview.sshPrivateKey) {
    broadcast(entry, "[app-logs] No running instance for this preview — app logs are only available while it is live.\n");
    return;
  }

  const privateKey = decrypt(preview.sshPrivateKey);
  const cmd = tailCommand(preview.repoConfig.useDockerCompose, preview.repoConfig.composeFilePath);

  try {
    const session = await connectSsh(preview.instanceIp, privateKey, 30_000);
    // The last viewer may have left while we were connecting.
    if (entry.torndown || entry.subscribers.size === 0) {
      session.close();
      return;
    }
    entry.session = session;
    entry.streamHandle = session.execStream(cmd, (chunk) => broadcast(entry, chunk));
  } catch (e: any) {
    broadcast(entry, `[app-logs] Failed to connect to instance: ${e.message}\n`);
    log.warn({ previewId, error: e.message }, "App log stream failed to connect");
  }
}

function teardown(previewId: number, entry: StreamEntry) {
  entry.torndown = true;
  streams.delete(previewId);
  try { entry.streamHandle?.close(); } catch {}
  try { entry.session?.close(); } catch {}
}

// Subscribe to a preview's live app logs. The returned function unsubscribes;
// when the last subscriber leaves, the shared SSH tail is torn down.
export async function streamAppLogs(
  previewId: number,
  onData: (chunk: string) => void,
): Promise<() => void> {
  let entry = streams.get(previewId);

  if (!entry) {
    entry = {
      subscribers: new Set(),
      buffer: [],
      bufferBytes: 0,
      connecting: Promise.resolve(),
      session: null,
      streamHandle: null,
      torndown: false,
    };
    streams.set(previewId, entry);
    entry.connecting = openStream(previewId, entry);
  }

  entry.subscribers.add(onData);
  // Replay buffered history so a late joiner isn't staring at a blank pane.
  for (const chunk of entry.buffer) {
    try { onData(chunk); } catch {}
  }

  const current = entry;
  return () => {
    current.subscribers.delete(onData);
    if (current.subscribers.size === 0) teardown(previewId, current);
  };
}
