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

// InvoiceShelf account registry for the officer-invoiceshelf sidecar. Callers deal in PLAINTEXT — encryption
// to and from at-rest ciphertext happens here. See ../crypto.ts and ../schema/invoiceshelf.ts.
//
// Two return types, and the split is the safety property:
//   InvoiceshelfAccount     — safe to serialize to the browser. Has NO token field at all, not even masked.
//   InvoiceshelfCredentials — url + decrypted token + company, for the sidecar's own upstream calls.
// `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 InvoiceshelfAccount = {
  id: number;
  label: string;
  url: string;
  companyId: number | null;
  companyName: string | null;
  version: string | null;
  isActive: boolean;
  lastSeenAt: Date | null;
  createdAt: Date;
};

export type InvoiceshelfCredentials = {
  id: number;
  label: string;
  url: string;
  token: string;
  companyId: number | null;
};

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

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

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

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

type CreateInvoiceshelfAccountParams = {
  userId: number;
  label: string;
  url: string;
  token: string;
  companyId: number | null;
  companyName: string | null;
  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 token is encrypted before write; the returned row carries no token. */
export async function createInvoiceshelfAccount(params: CreateInvoiceshelfAccountParams): Promise<InvoiceshelfAccount> {
  const { userId, label, url, token, companyId, companyName, version, activate } = params;
  return db.transaction(async (tx) => {
    if (activate) {
      await tx
        .update(invoiceshelfAccounts)
        .set({ isActive: false, updatedAt: new Date() })
        .where(and(eq(invoiceshelfAccounts.userId, userId), eq(invoiceshelfAccounts.isActive, true)));
    }
    const [row] = await tx
      .insert(invoiceshelfAccounts)
      .values({
        userId,
        label,
        url,
        token: encryptSecret('invoiceshelf', token),
        companyId,
        companyName,
        version,
        isActive: activate,
        lastSeenAt: version ? new Date() : null,
      })
      .returning(accountCols);
    return row!;
  });
}

type UpdateInvoiceshelfAccountParams = {
  label?: string;
  url?: string;
  token?: string;
  companyId?: number | null;
  companyName?: string | null;
  version?: string | null;
};

/** Edit an account. Omitted fields are left alone; a supplied token is re-encrypted. */
export async function updateInvoiceshelfAccount(
  userId: number,
  id: number,
  params: UpdateInvoiceshelfAccountParams,
): Promise<InvoiceshelfAccount | 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.token !== undefined) set.token = encryptSecret('invoiceshelf', params.token);
  if (params.companyId !== undefined) set.companyId = params.companyId;
  if (params.companyName !== undefined) set.companyName = params.companyName;
  if (params.version !== undefined) set.version = params.version;

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

/** Switch accounts. Clearing the others first keeps the one-active partial index satisfied. */
export async function setActiveInvoiceshelfAccount(userId: number, id: number): Promise<InvoiceshelfAccount | null> {
  return db.transaction(async (tx) => {
    await tx
      .update(invoiceshelfAccounts)
      .set({ isActive: false, updatedAt: new Date() })
      .where(and(eq(invoiceshelfAccounts.userId, userId), eq(invoiceshelfAccounts.isActive, true)));
    const [row] = await tx
      .update(invoiceshelfAccounts)
      .set({ isActive: true, updatedAt: new Date() })
      .where(and(eq(invoiceshelfAccounts.userId, userId), eq(invoiceshelfAccounts.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 deleteInvoiceshelfAccount(userId: number, id: number): Promise<boolean> {
  return db.transaction(async (tx) => {
    const [deleted] = await tx
      .delete(invoiceshelfAccounts)
      .where(and(eq(invoiceshelfAccounts.userId, userId), eq(invoiceshelfAccounts.id, id)))
      .returning({ id: invoiceshelfAccounts.id, wasActive: invoiceshelfAccounts.isActive });
    if (!deleted) return false;

    if (deleted.wasActive) {
      const [next] = await tx
        .select({ id: invoiceshelfAccounts.id })
        .from(invoiceshelfAccounts)
        .where(eq(invoiceshelfAccounts.userId, userId))
        .orderBy(desc(invoiceshelfAccounts.createdAt))
        .limit(1);
      if (next) {
        await tx
          .update(invoiceshelfAccounts)
          .set({ isActive: true, updatedAt: new Date() })
          .where(eq(invoiceshelfAccounts.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 recordInvoiceshelfProbe(userId: number, id: number, version: string | null): Promise<void> {
  await db
    .update(invoiceshelfAccounts)
    .set({ version, lastSeenAt: new Date() })
    .where(and(eq(invoiceshelfAccounts.userId, userId), eq(invoiceshelfAccounts.id, id)));
}
