import { mkdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import {
  Global,
  Inject,
  Logger,
  Module,
  type OnApplicationShutdown,
  type OnModuleInit,
} from '@nestjs/common';
import {
  createDb,
  createPgliteDb,
  runPgliteMigrations,
  type Db,
  type DbHandle,
} from '@mosaicstack/db';
import { createStorageAdapter, type StorageAdapter } from '@mosaicstack/storage';
import type { MosaicConfig } from '@mosaicstack/config';
import { MOSAIC_CONFIG } from '../config/config.module.js';

export const DB_HANDLE = 'DB_HANDLE';
export const DB = 'DB';
export const STORAGE_ADAPTER = 'STORAGE_ADAPTER';

@Global()
@Module({
  providers: [
    {
      provide: DB_HANDLE,
      useFactory: (config: MosaicConfig): DbHandle => {
        if (config.tier === 'local') {
          const dataDir = join(homedir(), '.config', 'mosaic', 'gateway', 'pglite');
          mkdirSync(dataDir, { recursive: true });
          return createPgliteDb(dataDir);
        }
        return createDb(config.storage.type === 'postgres' ? config.storage.url : undefined);
      },
      inject: [MOSAIC_CONFIG],
    },
    {
      provide: DB,
      useFactory: (handle: DbHandle): Db => handle.db,
      inject: [DB_HANDLE],
    },
    {
      provide: STORAGE_ADAPTER,
      useFactory: (config: MosaicConfig): StorageAdapter => createStorageAdapter(config.storage),
      inject: [MOSAIC_CONFIG],
    },
  ],
  exports: [DB, STORAGE_ADAPTER],
})
export class DatabaseModule implements OnApplicationShutdown, OnModuleInit {
  private readonly logger = new Logger(DatabaseModule.name);

  constructor(
    @Inject(DB_HANDLE) private readonly handle: DbHandle,
    @Inject(STORAGE_ADAPTER) private readonly storageAdapter: StorageAdapter,
    @Inject(MOSAIC_CONFIG) private readonly config: MosaicConfig,
  ) {}

  // Migrations must complete before any module that injects DB starts serving
  // requests. NestJS awaits onModuleInit before app.listen(), and modules that
  // inject DB are initialized after this one — so all DB-dependent code sees a
  // populated schema before the first HTTP request lands.
  //
  // Local (PGlite) tier: we run gateway-DB migrations explicitly here. The
  // storage adapter writes to a separate PGlite directory and only manages its
  // own KV tables, so we still call its migrate() afterwards.
  //
  // Postgres tier: PostgresAdapter.migrate() already calls runMigrations() on
  // the same DATABASE_URL, so a single call covers both the gateway DB and
  // the storage tables. We deliberately do NOT call runMigrations() here to
  // avoid opening a second short-lived connection and doubling startup cost.
  async onModuleInit(): Promise<void> {
    if (this.config.tier === 'local') {
      this.logger.log('Applying PGlite schema migrations...');
      await runPgliteMigrations(this.handle);
    }
    this.logger.log(`Initializing storage adapter (${this.storageAdapter.name})...`);
    await this.storageAdapter.migrate();
  }

  async onApplicationShutdown(): Promise<void> {
    await Promise.all([this.handle.close(), this.storageAdapter.close()]);
  }
}
