import { serializeError } from '@finalytic/utils';
import { CamelCasePlugin, Kysely as K, type Transaction as T } from 'kysely';
import { PostgresJSDialect } from 'kysely-postgres-js';
import postgres, { type Sql } from 'postgres';

export type {
  ColumnType,
  Expression,
  ExpressionBuilder,
  ExpressionOrFactory,
  Generated,
  Insertable,
  JSONColumnType,
  OperandExpression,
  Selectable,
  SelectQueryBuilder,
  SqlBool,
  Updateable,
} from 'kysely';
export { jsonArrayFrom } from 'kysely/helpers/postgres';
export * from './v1.generated';

import { join, relative } from 'node:path';
import type { DB } from './v1.generated';

export type Transaction = T<DB>;
export { sql } from 'kysely';

type PgOpt = {
  forceDisableSSL?: boolean;
  retries?: false | number;
};

export type Kysely = K<DB>;
export type Postgres = Sql;
/**
 * @deprecated This method is deprecated in favor of the useAsyncKysely method.
 */
export function useKysely(postgres: Sql, options: { log?: boolean } = {}) {
  console.debug('Creating Kysely instance');
  const kysely = new K<DB>({
    dialect: new PostgresJSDialect({
      postgres,
    }),
    plugins: [new CamelCasePlugin()],
    log: options?.log ? ['query', 'error'] : undefined,
  });

  const destroy = kysely.destroy;
  kysely.destroy = async () => {
    console.debug('Closing Kysely connection');
    return destroy();
  };

  return kysely;
}

const _connections: postgres.Sql[] = [];
export async function useAsyncPostgres(connectionString: string, opt?: PgOpt) {
  const delayMs = 2500;
  const retries = opt?.retries === false ? 0 : opt?.retries || 3;
  const ssl = opt?.forceDisableSSL !== true;

  console.debug(
    `Creating Postgres connection with ${retries} retries and ssl=${ssl}`
  );
  if (_connections.length > 0) {
    console.warn(`We already have ${_connections.length} Postgres connections`);
  }

  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const sql = postgres(connectionString, {
        ssl: ssl ? 'require' : false,
        // connect_timeout: 10
        // idle_timeout: 10
        // disable prepare statements since we use pgbouncer
        prepare: false,
        connect_timeout: 10,
        idle_timeout: 60,
      });

      await sql`select 1`;
      try {
        // connect 30s, idle 60s, max lifetime 45min, statement 60s, lock 10s, log_min_duration 20s, log_lock_waits off
        await sql`set statement_timeout to '30s';`;
        await sql`set lock_timeout to '10s';`;
      } catch (e) {
        console.warn('Failed to set statement timeout, continuing', e);
      }

      _connections.push(sql);
      return sql;
    } catch (err) {
      if (attempt === retries) throw err;
      console.warn(
        `Postgres connection failed (attempt ${attempt}), retrying...`
      );
      console.error('error', err);
      await new Promise((res) => setTimeout(res, delayMs));
    }
  }
  throw new Error('Unable to connect to Postgres');
}

export async function useAsyncKysely(connectionString: string, opt?: PgOpt) {
  const sql = await useAsyncPostgres(connectionString, opt);
  return useKysely(sql);
}

/**
 * @deprecated This method is deprecated in favor of the useAsyncKysely method.
 */
export function usePostgres(
  connectionString?: string,
  opt?: Pick<PgOpt, 'forceDisableSSL'>
) {
  if (!connectionString) throw new Error('No connection string');
  const ssl = opt?.forceDisableSSL !== true;
  const pg = postgres(connectionString, {
    ssl: ssl ? 'require' : false,
  });
  _connections.push(pg);
  return pg;
}

export async function forceCloseAllPostgres(timeoutMs = 5000) {
  await Promise.all(
    _connections.map((pg) =>
      pg.end({ timeout: timeoutMs }).catch(() => undefined)
    )
  );
  _connections.length = 0;
}

export class DatabaseError extends Error {
  constructor(message: string, cause: any) {
    super(message);
    this.cause = cause;
  }
}

const dirname =
  typeof __dirname === 'string' ? join(__dirname, '../../..') : '';

export async function withStack<T>(a: Promise<T>) {
  // skip if we can't get the stack, probably in a worker
  if (!dirname) return await a;

  const now = performance.now();
  const error = new DatabaseError('Error in execute', undefined);
  const stack = error.stack?.split('\n').slice(3) || undefined;
  const firstLine =
    stack?.length && stack[0]?.includes('(') && dirname
      ? relative(dirname, stack[0].split('(')[1].split(')')[0])
      : undefined;
  try {
    const result = await a;
    console.log(
      `[${firstLine}] Execution time: ${Math.round(performance.now() - now)}ms`
    );
    return result;
  } catch (e: any) {
    if (stack) error.stack = stack.join('\n').trim();
    else error.stack = e.stack;
    // error.cause = e;
    error.message = e.message;
    const serialized = serializeError(e);
    delete serialized.stack;
    error.cause = serialized;
    console.error(`Error in execute: ${firstLine}`, serializeError(error));
    console.log(
      `[${firstLine}] Execution time: ${Math.round(performance.now() - now)}ms`
    );
    throw error;
  }
}
