import { eq, and } from 'drizzle-orm';
import { db } from '../db';
import { serviceConnections } from './schema';
import { getOwnerUser } from '../auth/queries';
import { encryptSecret, decryptSecret } from '../crypto';

// Single-connection services (transmission, slskd) for their sidecars. Callers deal in PLAINTEXT —
// encryption to and from at-rest ciphertext happens here. See ../crypto.ts and ../schema/service-connections.ts.
//
// Two return types, and the split is the safety property:
//   ServiceConnection   — safe to serialize to the browser. Has NO secret field, only whether one is set.
//   ServiceCredentials  — the decrypted secret, for the sidecar's own upstream calls. Never returned by a route.
// `connectionCols` is what enforces it: a bare `select()` would put the ciphertext into every response the
// moment someone forgot to strip it.

/** The services that keep a connection here. Extending it is a one-line change, not a migration. */
export type ServiceName = 'transmission' | 'slskd' | 'esplora' | 'nbxplorer' | 'memos' | 'opengist' | 'gitea';

export type ServiceConnection = {
  id: number;
  service: string;
  /** Null on a credential-only row — see the column comment in ../schema/service-connections.ts. */
  url: string | null;
  username: string | null;
  path: string | null;
  /** Whether a secret is stored. The secret itself never crosses this boundary, not even masked. */
  hasSecret: boolean;
  version: string | null;
  lastSeenAt: Date | null;
  createdAt: Date;
};

export type ServiceCredentials = {
  id: number;
  /** Null when this row inherits the instance — resolve it with getServiceInstanceUrl. */
  url: string | null;
  username: string | null;
  secret: string | null;
  path: string | null;
};

const connectionCols = {
  id: serviceConnections.id,
  service: serviceConnections.service,
  url: serviceConnections.url,
  username: serviceConnections.username,
  path: serviceConnections.path,
  version: serviceConnections.version,
  lastSeenAt: serviceConnections.lastSeenAt,
  createdAt: serviceConnections.createdAt,
  secret: serviceConnections.secret,
};

type Row = typeof serviceConnections.$inferSelect;

/** Drop the ciphertext, keep the fact of it. The only shape a route is allowed to return. */
const toSafe = (row: Pick<Row, keyof typeof connectionCols>): ServiceConnection => ({
  id: row.id,
  service: row.service,
  url: row.url,
  username: row.username,
  path: row.path,
  hasSecret: !!row.secret,
  version: row.version,
  lastSeenAt: row.lastSeenAt,
  createdAt: row.createdAt,
});

/** What the owner has configured for this service, or null. Never includes the secret. */
export async function getServiceConnection(userId: number, service: ServiceName): Promise<ServiceConnection | null> {
  const [row] = await db
    .select(connectionCols)
    .from(serviceConnections)
    .where(and(eq(serviceConnections.userId, userId), eq(serviceConnections.service, service)));
  return row ? toSafe(row) : null;
}

/** The same row with its secret decrypted, for the sidecar's upstream calls. */
export async function getServiceCredentials(userId: number, service: ServiceName): Promise<ServiceCredentials | null> {
  const [row] = await db
    .select()
    .from(serviceConnections)
    .where(and(eq(serviceConnections.userId, userId), eq(serviceConnections.service, service)));
  if (!row) return null;
  return {
    id: row.id,
    url: row.url,
    username: row.username,
    secret: row.secret ? decryptSecret('service-connections', row.secret) : null,
    path: row.path,
  };
}

type SaveServiceConnectionParams = {
  userId: number;
  service: ServiceName;
  /**
   * Null writes a CREDENTIAL row — one that inherits the instance from the owner's row on read. Only
   * the inheriting kind of service does this, and only for non-owner users. See the column comment in
   * ../schema/service-connections.ts.
   */
  url: string | null;
  username?: string | null;
  /** Encrypted before write. Undefined leaves a stored secret alone; null clears it. */
  secret?: string | null;
  path?: string | null;
  version?: string | null;
};

/**
 * Create or update the one row for this service.
 *
 * An upsert rather than separate add/edit routes, because there is nothing to add a second of: the UI shows
 * one form whose Save means "this is where the daemon is now", whether or not it was ever filled in before.
 */
export async function saveServiceConnection(params: SaveServiceConnectionParams): Promise<ServiceConnection> {
  const { userId, service, url, username, secret, path, version } = params;
  const now = new Date();

  const set: Partial<Row> = { url, updatedAt: now };
  if (username !== undefined) set.username = username;
  if (secret !== undefined) set.secret = secret === null ? null : encryptSecret('service-connections', secret);
  if (path !== undefined) set.path = path;
  if (version !== undefined) {
    set.version = version;
    set.lastSeenAt = version ? now : null;
  }

  const [row] = await db
    .insert(serviceConnections)
    .values({
      userId,
      service,
      url,
      username: username ?? null,
      secret: secret ? encryptSecret('service-connections', secret) : null,
      path: path ?? null,
      version: version ?? null,
      lastSeenAt: version ? now : null,
    })
    .onConflictDoUpdate({ target: [serviceConnections.userId, serviceConnections.service], set })
    .returning(connectionCols);
  return toSafe(row!);
}

/** Forget the connection entirely — the sidecar then 503s and the UI offers the setup form again. */
export async function deleteServiceConnection(userId: number, service: ServiceName): Promise<boolean> {
  const [row] = await db
    .delete(serviceConnections)
    .where(and(eq(serviceConnections.userId, userId), eq(serviceConnections.service, service)))
    .returning({ id: serviceConnections.id });
  return !!row;
}

/** Stamp a successful probe, so the UI can tell "never reached" from "was reachable, now isn't". */
export async function recordServiceProbe(userId: number, service: ServiceName, version: string | null): Promise<void> {
  await db
    .update(serviceConnections)
    .set({ version, lastSeenAt: new Date() })
    .where(and(eq(serviceConnections.userId, userId), eq(serviceConnections.service, service)));
}

// ── Inherited instances ──
//
// Some services are ONE instance that several people authenticate to individually — Gitea is the first.
// The owner's row carries the URL and is the instance; everyone else's row carries only their own
// credential and leaves `url` null. See the column comment in ../schema/service-connections.ts.
//
// Resolution lives here rather than in each sidecar so there is one answer to "where is this service",
// and so a sidecar cannot accidentally use a member-supplied URL — the member has not got one to supply.

/**
 * The instance URL for a service: whoever owns the platform configured it. Null when the owner has not
 * connected this service yet, which is the signal to refuse a member's setup rather than let them invent
 * an instance of their own.
 */
export async function getServiceInstanceUrl(service: ServiceName): Promise<string | null> {
  const owner = await getOwnerUser();
  if (!owner) return null;
  const [row] = await db
    .select({ url: serviceConnections.url })
    .from(serviceConnections)
    .where(and(eq(serviceConnections.userId, owner.id), eq(serviceConnections.service, service)));
  return row?.url ?? null;
}

/**
 * This user's credentials with the base resolved: their own URL if they have one (the owner, or a
 * single-daemon service), otherwise the instance the owner configured. Returns null when there is no
 * usable pairing — no row, or a row that inherits from an instance nobody has set up.
 */
export async function getResolvedServiceCredentials(
  userId: number,
  service: ServiceName,
): Promise<(ServiceCredentials & { url: string }) | null> {
  const own = await getServiceCredentials(userId, service);
  if (!own) return null;
  if (own.url) return { ...own, url: own.url };
  const instance = await getServiceInstanceUrl(service);
  return instance ? { ...own, url: instance } : null;
}
