import { pgTable, serial, integer, text, boolean, timestamp, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from 'officerdb/auth/schema';

// The Headscale servers the owner manages, for the officer-headscale sidecar. Officer targets no single
// Headscale: the owner registers one or more servers (URL + an admin API key generated on that server) and
// toggles between them, so this is configuration the user creates at runtime rather than env vars.
//
// `api_key` is a Headscale *admin* credential — it can delete every node on a tailnet — so it is encrypted
// at rest via ../crypto.ts, exactly like the vault token set. Encryption/decryption is confined to
// queries/headscale.ts; nothing outside that file ever sees ciphertext, and list callers never see the key
// at all. SECURITY_AUDIT.md L2 records plaintext credential storage as an open finding, so the plaintext
// email/integrations tables are debt to avoid copying, not a precedent to follow.
//
// Every table here is `headscale_`-prefixed and this file holds nothing else: when sidecars own their own
// schema it moves wholesale into src/servers/sidecar/headscale/ with no untangling. Only the
// officer-headscale sidecar reads or writes these tables.

export const headscaleServers = pgTable(
  'headscale_servers',
  {
    id: serial('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    name: text('name').notNull(),
    // Normalized without a trailing slash before write, so `${url}/api/v1/...` never doubles the separator.
    url: text('url').notNull(),
    apiKey: text('api_key').notNull(), // encrypted
    // Last version seen from the server's unauthenticated GET /version. Null until first probed; the
    // literal 'dev' when the server was built without VCS info, which is unknown rather than too-old.
    version: text('version'),
    // Where to SSH for a shell on the box running this Headscale — the last-resort escape hatch for when the
    // API cannot answer (headscale is down, the tailnet is down, the logs are the only evidence). Deliberately
    // NOT derived from `url`: the whole point is to reach the machine when the control plane's own hostname
    // stops resolving, so this is usually a raw IP on a different path. No port, user or key material — the
    // connection uses whatever ~/.ssh already knows, so there is no credential here to protect.
    sshHost: text('ssh_host'),
    isActive: boolean('is_active').notNull().default(false),
    // Last successful probe, so the UI can distinguish "never reached" from "was reachable, now isn't".
    lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    // One registration per URL — re-registering the same server should be an edit, not a duplicate.
    uniqueIndex('uq_headscale_servers_user_url').on(t.userId, t.url),
    // At most one active server per owner, enforced by the DB rather than by convention: a partial unique
    // index over the active rows only. setActiveHeadscaleServer still clears the others in a transaction,
    // but a bug there fails loudly here instead of silently leaving two servers active.
    uniqueIndex('uq_headscale_servers_one_active')
      .on(t.userId)
      .where(sql`${t.isActive}`),
  ],
);
