import type {
  SecretService,
} from '@pikku/core'
import type { ContentService } from '@pikku/core/services'
import type { KyselyDB } from './database-types.js'
import {
  JsonConsoleLogger,
  LocalSecretService,
  LocalVariablesService,
} from '@pikku/core/services'
import { CFWorkerSchemaService } from '@pikku/schema-cfworker'
import { LibsqlWebDialect } from '@pikku/kysely-sqlite'
import { Kysely, CamelCasePlugin } from 'kysely'
import './middleware.js'
import { ProjectEmailService } from './services/email-service.js'
import { MailgunEmailService } from './services/mailgun/mailgun.js'
import { RecognitionDossierService } from './services/recognition-dossier-service.js'
import { TranslationService } from './services/translation-service.js'
import {
  pikkuServices,
  pikkuWireServices,
} from '#pikku/function/pikku-function-types.gen.js'

export const createSingletonServices = pikkuServices(async (config, existingServices) => {
  const variables = existingServices?.variables ?? new LocalVariablesService()
  const logger = existingServices?.logger ?? new JsonConsoleLogger()
  const secrets: SecretService = existingServices?.secrets ?? new LocalSecretService(variables)

  if (config.logLevel) {
    logger.setLevel(config.logLevel)
  }

  const schema = existingServices?.schema ?? new CFWorkerSchemaService(logger)

  // On CF Workers Fabric injects DATABASE_URL as a variable but does NOT inject a
  // kysely service (the SQL dialect is project-specific), so construct our own —
  // mirroring the green germantax app. pikku-dev / sandbox still inject kysely.
  let kysely = existingServices?.kysely as Kysely<KyselyDB> | undefined
  if (!kysely) {
    const databaseUrl = await variables.get('DATABASE_URL')
    if (!databaseUrl) {
      throw new Error(
        'DATABASE_URL not set — in dev enable `dev.db` in the pikku config; in prod Fabric injects it',
      )
    }
    kysely = new Kysely<KyselyDB>({
      dialect: new LibsqlWebDialect({ url: databaseUrl }),
      plugins: [new CamelCasePlugin()],
    })
  }

  // Content (R2/asset storage) is only consumed by dossier generation, never on
  // the auth/RPC hot path. If Fabric doesn't inject it on CF, tolerate its
  // absence rather than 500-ing every route (mirrors the green germantax app).
  if (!existingServices?.content) {
    logger.warn('content service not injected — dossier generation disabled for this worker')
  }
  const content = existingServices?.content as ContentService

  // Mailgun is only needed to actually send mail. A missing API key (e.g. an
  // ephemeral stage without the secret) must degrade to "email disabled", never
  // crash singleton init — so fetch the secret defensively.
  let mailgunApiKey: string | undefined
  if (variables.get('NODE_ENV') === 'production') {
    try {
      mailgunApiKey = await secrets.getSecret(config.secrets.mailgunApiKey)
    } catch (error) {
      logger.warn(`mailgun API key unavailable — outbound email disabled: ${error instanceof Error ? error.message : String(error)}`)
    }
  }
  const emailDelegate =
    existingServices?.emailService ?? new MailgunEmailService(mailgunApiKey, config.domain)
  const emailService = new ProjectEmailService(emailDelegate)
  const recognitionDossierService =
    existingServices?.recognitionDossierService ??
    new RecognitionDossierService(kysely, content, logger)

  const translation = existingServices?.translation ?? new TranslationService(kysely)
  if (!existingServices?.translation) {
    // A translation-snapshot sync failure must not take down the whole API.
    try {
      await translation.init()
    } catch (error) {
      logger.error(`translation.init() failed — serving without synced translations: ${error instanceof Error ? error.message : String(error)}`)
    }
  }

  return {
    ...(existingServices ?? {}),
    config,
    variables,
    secrets,
    schema,
    logger,
    kysely,
    content,
    emailService,
    recognitionDossierService,
    translation,
  }
})

export const createWireServices = pikkuWireServices(async () => ({
}))
