import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { photosConfig } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';

// Immich account registry for the officer-photos sidecar. Callers deal in PLAINTEXT — encryption to and from
// at-rest ciphertext happens here. See ../crypto.ts and ../schema/photos.ts.
//
// Two return types, and the split is the safety property:
//   PhotosAccount     — safe to serialize to the browser. Has NO api key field at all, not even a masked one.
//   PhotosCredentials — url + decrypted key, for the sidecar's own upstream calls. Never returned by a route.
// `accountCols` is what enforces it: a bare `select()` would put the ciphertext column into every list
// response the moment someone forgot to strip it.

export type PhotosAccount = {
  id: number;
  label: string;
  url: string;
  version: string | null;
  isActive: boolean;
  lastSeenAt: Date | null;
  createdAt: Date;
};

export type PhotosCredentials = { id: number; label: string; url: string; apiKey: string };

const accountCols = {
  id: photosConfig.id,
  label: photosConfig.label,
  url: photosConfig.url,
  version: photosConfig.version,
  isActive: photosConfig.isActive,
  lastSeenAt: photosConfig.lastSeenAt,
  createdAt: photosConfig.createdAt,
};

/** Every account the owner has added, active first then newest. Never includes the API key. */
export async function listPhotosAccounts(userId: number): Promise<PhotosAccount[]> {
  return db
    .select(accountCols)
    .from(photosConfig)
    .where(eq(photosConfig.userId, userId))
    .orderBy(desc(photosConfig.isActive), desc(photosConfig.createdAt));
}

/** The selected account with its key decrypted, or null when none is added. */
export async function getActivePhotosCredentials(userId: number): Promise<PhotosCredentials | null> {
  const [row] = await db
    .select()
    .from(photosConfig)
    .where(and(eq(photosConfig.userId, userId), eq(photosConfig.isActive, true)));
  if (!row) return null;
  return { id: row.id, label: row.label, url: row.url, apiKey: decryptSecret('photos', row.apiKey) };
}

/** One account's credentials by id — for probing a specific account rather than the active one. */
export async function getPhotosCredentials(userId: number, id: number): Promise<PhotosCredentials | null> {
  const [row] = await db
    .select()
    .from(photosConfig)
    .where(and(eq(photosConfig.userId, userId), eq(photosConfig.id, id)));
  if (!row) return null;
  return { id: row.id, label: row.label, url: row.url, apiKey: decryptSecret('photos', row.apiKey) };
}

type CreatePhotosAccountParams = {
  userId: number;
  label: string;
  url: string;
  apiKey: string;
  version: string | null;
  /** Select it. True for the first account, so the UI is never left with accounts added but none chosen. */
  activate: boolean;
};

/** Add an account. The key is encrypted before write; the returned row carries no key. */
export async function createPhotosAccount(params: CreatePhotosAccountParams): Promise<PhotosAccount> {
  const { userId, label, url, apiKey, version, activate } = params;
  return db.transaction(async (tx) => {
    if (activate) {
      await tx
        .update(photosConfig)
        .set({ isActive: false, updatedAt: new Date() })
        .where(and(eq(photosConfig.userId, userId), eq(photosConfig.isActive, true)));
    }
    const [row] = await tx
      .insert(photosConfig)
      .values({
        userId,
        label,
        url,
        apiKey: encryptSecret('photos', apiKey),
        version,
        isActive: activate,
        lastSeenAt: version ? new Date() : null,
      })
      .returning(accountCols);
    return row!;
  });
}

type UpdatePhotosAccountParams = { label?: string; url?: string; apiKey?: string; version?: string | null };

/** Edit an account. Omitted fields are left alone; a supplied key is re-encrypted. */
export async function updatePhotosAccount(
  userId: number,
  id: number,
  params: UpdatePhotosAccountParams,
): Promise<PhotosAccount | null> {
  const set: Record<string, unknown> = { updatedAt: new Date() };
  if (params.label !== undefined) set.label = params.label;
  if (params.url !== undefined) set.url = params.url;
  if (params.apiKey !== undefined) set.apiKey = encryptSecret('photos', params.apiKey);
  if (params.version !== undefined) set.version = params.version;

  const [row] = await db
    .update(photosConfig)
    .set(set)
    .where(and(eq(photosConfig.userId, userId), eq(photosConfig.id, id)))
    .returning(accountCols);
  return row ?? null;
}

/** Switch accounts. Clearing the others first keeps the one-active partial index satisfied. */
export async function setActivePhotosAccount(userId: number, id: number): Promise<PhotosAccount | null> {
  return db.transaction(async (tx) => {
    await tx
      .update(photosConfig)
      .set({ isActive: false, updatedAt: new Date() })
      .where(and(eq(photosConfig.userId, userId), eq(photosConfig.isActive, true)));
    const [row] = await tx
      .update(photosConfig)
      .set({ isActive: true, updatedAt: new Date() })
      .where(and(eq(photosConfig.userId, userId), eq(photosConfig.id, id)))
      .returning(accountCols);
    return row ?? null;
  });
}

/**
 * Remove an account. If it was the active one the newest survivor is promoted — otherwise removing the
 * account in use would leave the owner with accounts added but none selected, which reads as "not connected"
 * and is a confusing place to land.
 */
export async function deletePhotosAccount(userId: number, id: number): Promise<boolean> {
  return db.transaction(async (tx) => {
    const [deleted] = await tx
      .delete(photosConfig)
      .where(and(eq(photosConfig.userId, userId), eq(photosConfig.id, id)))
      .returning({ id: photosConfig.id, wasActive: photosConfig.isActive });
    if (!deleted) return false;

    if (deleted.wasActive) {
      const [next] = await tx
        .select({ id: photosConfig.id })
        .from(photosConfig)
        .where(eq(photosConfig.userId, userId))
        .orderBy(desc(photosConfig.createdAt))
        .limit(1);
      if (next) {
        await tx
          .update(photosConfig)
          .set({ isActive: true, updatedAt: new Date() })
          .where(eq(photosConfig.id, next.id));
      }
    }
    return true;
  });
}

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