import type { Db, Row } from "./driver.ts";
import type { Model } from "./schema.ts";
import type { Dialect } from "./sql.ts";

const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
function ident(value: string): string {
  if (!IDENT.test(value)) throw new Error(`Unsafe identifier: ${value}`);
  return value;
}
function ph(dialect: Dialect, index: number): string {
  return dialect === "postgres" ? `$${index}` : "?";
}
function encode(value: unknown): string {
  return btoa(unescape(encodeURIComponent(JSON.stringify(value))))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}
function decode<T>(value: string): T {
  const pad = value.length % 4 ? "=".repeat(4 - (value.length % 4)) : "";
  return JSON.parse(
    decodeURIComponent(escape(atob(value.replace(/-/g, "+").replace(/_/g, "/") + pad))),
  ) as T;
}

export interface CursorPageOptions {
  limit?: number;
  after?: string;
  before?: string;
  column?: string;
  direction?: "asc" | "desc";
  maxLimit?: number;
}
export interface CursorPage<T> {
  items: T[];
  nextCursor?: string;
  previousCursor?: string;
  hasMore: boolean;
}
interface CursorValue {
  value: unknown;
  direction: "asc" | "desc";
}

export async function cursorPaginate<T extends Row = Row>(
  db: Db,
  query: { sql: string; params?: unknown[]; model?: Model<T> },
  options: CursorPageOptions = {},
): Promise<CursorPage<T>> {
  const column = ident(options.column ?? "id");
  const direction = options.direction ?? "asc";
  const max = options.maxLimit ?? 100;
  const limit = Math.min(max, Math.max(1, Math.floor(options.limit ?? 20)));
  const params = [...(query.params ?? [])];
  const cursor = options.after
    ? decode<CursorValue>(options.after)
    : options.before
      ? decode<CursorValue>(options.before)
      : undefined;
  const comparison = options.before
    ? direction === "asc"
      ? "<"
      : ">"
    : direction === "asc"
      ? ">"
      : "<";
  const reverse = !!options.before;
  let sql = `SELECT * FROM (${query.sql}) AS __wrn_cursor`;
  if (cursor) {
    params.push(cursor.value);
    sql += ` WHERE ${column} ${comparison} ${ph(db.driver.dialect, params.length)}`;
  }
  const order = reverse ? (direction === "asc" ? "DESC" : "ASC") : direction.toUpperCase();
  params.push(limit + 1);
  sql += ` ORDER BY ${column} ${order} LIMIT ${ph(db.driver.dialect, params.length)}`;
  let rows = await db.all<T>(sql, params, query.model);
  const hasMore = rows.length > limit;
  if (hasMore) rows = rows.slice(0, limit);
  if (reverse) rows.reverse();
  const first = rows[0]?.[column];
  const last = rows.at(-1)?.[column];
  return {
    items: rows,
    hasMore,
    ...(last !== undefined && (hasMore || rows.length === limit)
      ? { nextCursor: encode({ value: last, direction }) }
      : {}),
    ...(first !== undefined && (options.after || options.before)
      ? { previousCursor: encode({ value: first, direction }) }
      : {}),
  };
}

export async function optimisticUpdate(
  db: Db,
  input: {
    table: string;
    idColumn?: string;
    id: unknown;
    versionColumn?: string;
    version: number;
    values: Record<string, unknown>;
  },
): Promise<number> {
  const table = ident(input.table);
  const idColumn = ident(input.idColumn ?? "id");
  const versionColumn = ident(input.versionColumn ?? "version");
  const entries = Object.entries(input.values);
  if (!entries.length) return input.version;
  for (const [column] of entries) ident(column);
  const params = entries.map(([, value]) => value);
  const assignments = entries.map(
    ([column], index) => `${column} = ${ph(db.driver.dialect, index + 1)}`,
  );
  assignments.push(`${versionColumn} = ${versionColumn} + 1`);
  params.push(input.id, input.version);
  const result = await db.exec(
    `UPDATE ${table} SET ${assignments.join(", ")} WHERE ${idColumn} = ${ph(db.driver.dialect, params.length - 1)} AND ${versionColumn} = ${ph(db.driver.dialect, params.length)}`,
    params,
  );
  if (result.changes !== 1) throw new Error("WRN-DB-OPTIMISTIC-LOCK");
  return input.version + 1;
}

export function tenantScope(
  sql: string,
  tenantId: unknown,
  dialect: Dialect,
  existingParams = 0,
  column = "tenantId",
): { sql: string; params: unknown[] } {
  ident(column);
  const wrapped = `SELECT * FROM (${sql}) AS __wrn_tenant WHERE ${column} = ${ph(dialect, existingParams + 1)}`;
  return { sql: wrapped, params: [tenantId] };
}

export function softDeleteClause(column = "deletedAt"): string {
  return `${ident(column)} IS NULL`;
}
