import * as drizzle from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import { settingsKv, type SettingsSchema } from './settings-schema.js'

// Re-export the schema so a consumer can type their db handle (`Querier<SettingsSchema>`)
// and run the table DDL from the `./settings` subpath alone — the factory's first param
// requires `SettingsSchema`, so the seam is callable from types only (agent-legibility).
export { settingsKv, settingsSchema, type SettingsSchema } from './settings-schema.js'

export interface SettingsStore {
  get<T = unknown>(key: string): Promise<T | null>
  getMany(keys: string[]): Promise<Record<string, unknown>>
  set(key: string, value: unknown): Promise<void>
  all(): Promise<Record<string, unknown>>
  remove(key: string): Promise<void>
}

export class SettingsValidationError extends Error {
  override readonly name = 'SettingsValidationError'
  constructor(
    readonly field: string,
    readonly detail: string,
  ) {
    super(`settings invalid: ${field} — ${detail}`)
  }
}

export const SETTINGS_KEYS = {
  siteName: 'site.name',
  siteUrl: 'site.url',
  logo: 'site.logo',
  homepage: 'site.homepage',
  maintenance: 'site.maintenance',
} as const

const MAX_KEY_LEN = 256
// Prototype-pollution floor: these keys must never reach a result object via a key write.
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype'])

function validateKey(key: string): void {
  if (typeof key !== 'string' || key.length === 0) {
    throw new SettingsValidationError('key', 'required')
  }
  if (key.length > MAX_KEY_LEN) {
    throw new SettingsValidationError('key', `exceeds ${MAX_KEY_LEN} chars`)
  }
  if (FORBIDDEN_KEYS.has(key)) {
    throw new SettingsValidationError('key', 'reserved key not allowed')
  }
}

function assertSerializable(value: unknown): void {
  try {
    JSON.stringify(value)
  } catch {
    throw new SettingsValidationError('value', 'not JSON-serializable')
  }
}

function tolerantRead(value: unknown): unknown | null {
  if (value === null || value === undefined) return null
  try {
    JSON.stringify(value)
    return value
  } catch {
    return null
  }
}

export function settingsTableSql(table = 'settings_kv'): string {
  return `
CREATE TABLE IF NOT EXISTS ${table} (
  key text PRIMARY KEY,
  value jsonb NOT NULL,
  updated_at timestamptz(3) NOT NULL DEFAULT NOW()
);
`.trim()
}

export function createDbSettingsStore(db: Querier<SettingsSchema>, _opts?: { table?: string }): SettingsStore {
  return {
    async get<T = unknown>(key: string): Promise<T | null> {
      validateKey(key)
      const [row] = await db.select().from(settingsKv).where(drizzle.eq(settingsKv.key, key)).limit(1)
      if (!row) return null
      return tolerantRead(row.value) as T | null
    },

    async getMany(keys: string[]): Promise<Record<string, unknown>> {
      for (const key of keys) validateKey(key)
      if (keys.length === 0) return {}
      const rows = await db.select().from(settingsKv).where(drizzle.inArray(settingsKv.key, keys))
      // null-proto result: a row keyed __proto__ (inserted out-of-band) becomes a plain own property, never pollution.
      const out: Record<string, unknown> = Object.create(null)
      for (const row of rows) {
        const val = tolerantRead(row.value)
        if (val !== null) out[row.key] = val
      }
      return out
    },

    async set(key: string, value: unknown): Promise<void> {
      validateKey(key)
      assertSerializable(value)
      await db
        .insert(settingsKv)
        .values({ key, value })
        .onConflictDoUpdate({
          target: settingsKv.key,
          set: { value, updatedAt: new Date() },
        })
    },

    async all(): Promise<Record<string, unknown>> {
      const rows = await db.select().from(settingsKv)
      // null-proto result (see getMany): a stored __proto__ row can never corrupt the returned object.
      const out: Record<string, unknown> = Object.create(null)
      for (const row of rows) {
        const val = tolerantRead(row.value)
        if (val !== null) out[row.key] = val
      }
      return out
    },

    async remove(key: string): Promise<void> {
      validateKey(key)
      await db.delete(settingsKv).where(drizzle.eq(settingsKv.key, key))
    },
  }
}
