import { eq } from 'drizzle-orm';
import { db } from '../db';
import { sidecarInstalls } from './schema';

// What the owner has installed from the app store. See ../schema/app-store.ts for why there is no
// userId and why `installed` and `enabled` are separate.

export type SidecarInstall = typeof sidecarInstalls.$inferSelect;

export async function listSidecarInstalls(): Promise<SidecarInstall[]> {
  return db.select().from(sidecarInstalls);
}

export async function getSidecarInstall(sidecarId: string): Promise<SidecarInstall | null> {
  const [row] = await db.select().from(sidecarInstalls).where(eq(sidecarInstalls.sidecarId, sidecarId));
  return row ?? null;
}

/**
 * Create the row for an install that is about to start, or pick up the one a previous attempt left.
 *
 * Returning the existing row rather than replacing it is what makes a retry a RESUME: `completedSteps`
 * is how the installer knows not to provision a second container, and starting fresh would throw that
 * away every time someone pressed the button again after a failure.
 */
export async function beginInstall(sidecarId: string, mode: string): Promise<SidecarInstall> {
  const existing = await getSidecarInstall(sidecarId);
  if (existing) {
    const [row] = await db
      .update(sidecarInstalls)
      // `lastError` cleared on the way in: it describes the PREVIOUS attempt, and leaving it visible
      // while a new one runs is how a UI ends up showing a stale failure next to a working service.
      .set({ status: 'installing', mode, lastError: null, updatedAt: new Date() })
      .where(eq(sidecarInstalls.sidecarId, sidecarId))
      .returning();
    return row!;
  }

  const [row] = await db.insert(sidecarInstalls).values({ sidecarId, mode, status: 'installing' }).returning();
  return row!;
}

/** Record progress mid-install, so an interrupted run can be resumed rather than restarted. */
export async function recordSteps(sidecarId: string, completedSteps: string[], composeDir?: string): Promise<void> {
  await db
    .update(sidecarInstalls)
    .set({ completedSteps, ...(composeDir ? { composeDir } : {}), updatedAt: new Date() })
    .where(eq(sidecarInstalls.sidecarId, sidecarId));
}

/** Install finished. `enabled` goes true here because installing a thing is asking for it to run. */
export async function markInstalled(sidecarId: string, completedSteps: string[]): Promise<void> {
  await db
    .update(sidecarInstalls)
    .set({
      status: 'installed',
      enabled: true,
      completedSteps,
      lastError: null,
      installedAt: new Date(),
      updatedAt: new Date(),
    })
    .where(eq(sidecarInstalls.sidecarId, sidecarId));
}

/**
 * Install stopped and cannot continue on its own.
 *
 * `completedSteps` is still written: what worked stays recorded, so resuming picks up rather than
 * repeating. A failure that forgot its progress would re-provision on every retry.
 */
export async function markFailed(sidecarId: string, completedSteps: string[], error: string): Promise<void> {
  await db
    .update(sidecarInstalls)
    .set({ status: 'failed', completedSteps, lastError: error, updatedAt: new Date() })
    .where(eq(sidecarInstalls.sidecarId, sidecarId));
}

/**
 * Waiting for a human — a token only the service's own UI can mint.
 *
 * Deliberately NOT `failed`. The container is up and healthy and everything so far worked; calling it a
 * failure would make a normal install look broken and invite the user to tear down a working service.
 * `lastError` carries the instruction instead of an error.
 */
export async function markBlocked(sidecarId: string, completedSteps: string[], reason: string): Promise<void> {
  await db
    .update(sidecarInstalls)
    .set({ status: 'blocked', completedSteps, lastError: reason, updatedAt: new Date() })
    .where(eq(sidecarInstalls.sidecarId, sidecarId));
}

/** Enable or disable — the process and its container, without touching anything installed. */
export async function setEnabled(sidecarId: string, enabled: boolean): Promise<void> {
  await db
    .update(sidecarInstalls)
    .set({ enabled, updatedAt: new Date() })
    .where(eq(sidecarInstalls.sidecarId, sidecarId));
}

/**
 * Forget this install.
 *
 * Only this row. The sidecar's tables, the service directory and every byte of data under it survive —
 * see the schema comment. Uninstalling is "stop running this", not "delete my library".
 */
export async function removeInstall(sidecarId: string): Promise<void> {
  await db.delete(sidecarInstalls).where(eq(sidecarInstalls.sidecarId, sidecarId));
}
