import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { PLATFORM_DIR } from '../data-path';
import type { DiscoveredPlugin } from './manifest';

// Creating a plugin's tables.
//
// ── The trap this is shaped around ──
//
// `drizzle-kit push` DROPS anything absent from the schema it is given. So a generated barrel that
// tracked what is INSTALLED would delete a plugin's tables the moment it was uninstalled — turning "stop
// running this" into "delete my data", which is the one thing the whole install model refuses to do.
//
// So the barrel tracks the DIRECTORY, not the install row:
//
//   directory present, not installed   in the barrel. tables exist, unused. costs nothing.
//   installed                          in the barrel. tables exist and are used.
//   uninstalled                        STILL in the barrel. tables and every row survive.
//   directory deleted                  out of the barrel — the code is gone, and a push may drop them.
//
// Which makes reinstall a restore rather than a fresh start, and makes the destructive case require
// deliberately deleting a plugin's source. `bun db:push` by hand is then safe at any moment, which it
// would not be if this followed the install table.

const GENERATED = join(PLATFORM_DIR, 'src/databases/officer_db/src/plugin-schemas.gen.ts');

const HEADER = `// GENERATED by servers/plugins/schema.ts. Do not edit, and do not commit.
//
// One re-export per plugin DIRECTORY that has a db/schema.ts — not per installed plugin, deliberately.
// drizzle-kit push drops what it cannot see, so following the install table would delete a plugin's
// tables on uninstall. Following the directory means data survives uninstall and only a deleted plugin
// can lose it.
`;

/** Import specifier from the generated file to a plugin's schema, POSIX-style. */
const specifier = (target: string): string => {
  const rel = relative(dirname(GENERATED), target).split('\\').join('/');
  return (rel.startsWith('.') ? rel : `./${rel}`).replace(/\.ts$/, '');
};

/** Write the barrel. Always written, even empty — `schema.ts` imports it unconditionally. */
export function generatePluginSchemas(plugins: DiscoveredPlugin[]): void {
  const withSchema = plugins.filter((p) => p.schema);
  const lines = withSchema.map((p) => `export * from '${specifier(p.schema!)}'; // ${p.appName}`);
  const body = `${HEADER}\n${lines.join('\n')}\n`;

  const previous = existsSync(GENERATED) ? readFileSync(GENERATED, 'utf-8') : '';
  if (previous !== body) writeFileSync(GENERATED, body);
}

export type PushResult = { ok: boolean; ms: number; output: string };

/**
 * Apply the schema to Postgres.
 *
 * Shells out to the same `bun db:push` a human runs, rather than driving drizzle-kit in-process: it is
 * one definition of "apply the schema" instead of two that can disagree, and an owner can reproduce
 * exactly what an install did with a command they already know.
 *
 * `--force` because there is nothing to prompt about by construction — the barrel only ever gains entries
 * unless a plugin's source is deleted — and a prompt with no terminal attached would hang the install
 * forever rather than fail it.
 */
export async function pushSchema(): Promise<PushResult> {
  const started = Date.now();
  const proc = Bun.spawn(['bun', 'run', 'db:push', '--force'], {
    cwd: PLATFORM_DIR,
    stdout: 'pipe',
    stderr: 'pipe',
  });
  const [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
  const code = await proc.exited;
  // drizzle writes its progress to stderr, so a non-empty stderr is not a failure — only the exit code is.
  const output = `${out}${err}`.replace(/\u001b\[[0-9;]*[A-Za-z]/g, '').trim();
  return { ok: code === 0, ms: Date.now() - started, output: output.slice(-600) };
}
