import {
  and,
  eq,
  getTableColumns,
  type InferInsertModel,
  type SQL,
  type Table,
} from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import {
  ScopeViolationError,
  type IsolationAdapter,
  type ScopedInsertValues,
  type ScopedQuerier,
} from './index.js'

export type FkIsolationConfig<C extends string = 'tenant_id'> = {
  column?: C
  tables: readonly Table[]
}

function scopeViolation(tenantId: string, reason: string): never {
  throw new ScopeViolationError(reason, tenantId)
}

function assertRegistered(
  table: Table,
  registered: ReadonlySet<Table>,
  tenantId: string,
): void {
  if (!registered.has(table)) {
    scopeViolation(tenantId, 'table is not registered for tenant scoping')
  }
}

function findTenantColumn(table: Table, columnName: string, tenantId: string) {
  const columns = getTableColumns(table)
  for (const col of Object.values(columns)) {
    if (col.name === columnName) return col
  }
  scopeViolation(tenantId, `registered table is missing tenant column "${columnName}"`)
}

function tenantPropertyKey(table: Table, columnName: string, tenantId: string): string {
  const columns = getTableColumns(table)
  for (const [key, col] of Object.entries(columns)) {
    if (col.name === columnName) return key
  }
  scopeViolation(tenantId, `registered table is missing tenant column "${columnName}"`)
}

function guardBuilder(
  builder: object,
  tenantId: string,
  tenantCol: NonNullable<ReturnType<typeof findTenantColumn>>,
): object {
  const joinProps = new Set([
    'innerJoin',
    'leftJoin',
    'rightJoin',
    'fullJoin',
    'crossJoin',
    'union',
    'unionAll',
    'except',
    'intersect',
  ])

  return new Proxy(builder, {
    get(target, prop, receiver) {
      if (typeof prop === 'string' && joinProps.has(prop)) {
        scopeViolation(tenantId, `join shape "${prop}" cannot be proven tenant-scoped`)
      }
      const value = Reflect.get(target, prop, receiver)
      if (prop === 'where' && typeof value === 'function') {
        return (condition?: SQL, ...rest: unknown[]) => {
          const merged = condition
            ? (and(eq(tenantCol, tenantId), condition) as SQL)
            : eq(tenantCol, tenantId)
          const next = (value as (...a: unknown[]) => unknown).apply(target, [merged, ...rest])
          return typeof next === 'object' && next !== null
            ? guardBuilder(next as object, tenantId, tenantCol)
            : next
        }
      }
      if (typeof value === 'function') {
        return (...args: unknown[]) => {
          const next = (value as (...a: unknown[]) => unknown).apply(target, args)
          return typeof next === 'object' && next !== null
            ? guardBuilder(next as object, tenantId, tenantCol)
            : next
        }
      }
      return value
    },
  })
}

function createScopeQuerier<
  S extends Record<string, unknown>,
  C extends string,
>(
  q: Querier<S>,
  tenantId: string,
  columnName: C,
  registered: ReadonlySet<Table>,
): ScopedQuerier<S, C> {
  return {
    select(...args: unknown[]) {
      const builder = q.select(...(args as Parameters<Querier<S>['select']>))
      return {
        from(table: Table, ...rest: unknown[]) {
          assertRegistered(table, registered, tenantId)
          const tenantCol = findTenantColumn(table, columnName, tenantId)
          const next = (builder.from as (t: Table, ...r: unknown[]) => object).call(
            builder,
            table,
            ...rest,
          )
          const scoped = (next as { where: (c: unknown) => object }).where(eq(tenantCol, tenantId))
          return guardBuilder(scoped, tenantId, tenantCol)
        },
      }
    },
    selectDistinct(...args: unknown[]) {
      const builder = q.selectDistinct(...(args as Parameters<Querier<S>['selectDistinct']>))
      return {
        from(table: Table, ...rest: unknown[]) {
          assertRegistered(table, registered, tenantId)
          const tenantCol = findTenantColumn(table, columnName, tenantId)
          const next = (builder.from as (t: Table, ...r: unknown[]) => object).call(
            builder,
            table,
            ...rest,
          )
          const scoped = (next as { where: (c: unknown) => object }).where(eq(tenantCol, tenantId))
          return guardBuilder(scoped, tenantId, tenantCol)
        },
      }
    },
    selectDistinctOn(...args: unknown[]) {
      const builder = q.selectDistinctOn(...(args as Parameters<Querier<S>['selectDistinctOn']>))
      return {
        from(table: Table, ...rest: unknown[]) {
          assertRegistered(table, registered, tenantId)
          const tenantCol = findTenantColumn(table, columnName, tenantId)
          const next = (builder.from as (t: Table, ...r: unknown[]) => object).call(
            builder,
            table,
            ...rest,
          )
          const scoped = (next as { where: (c: unknown) => object }).where(eq(tenantCol, tenantId))
          return guardBuilder(scoped, tenantId, tenantCol)
        },
      }
    },
    insert<TTable extends Table>(table: TTable) {
      assertRegistered(table, registered, tenantId)
      const propertyKey = tenantPropertyKey(table, columnName, tenantId)
      const builder = q.insert(table)
      return {
        values(
          values: ScopedInsertValues<TTable, C> | ScopedInsertValues<TTable, C>[],
        ) {
          const stamped = Array.isArray(values)
            ? values.map((row) => ({ ...row, [propertyKey]: tenantId }))
            : { ...values, [propertyKey]: tenantId }
          return builder.values(stamped as InferInsertModel<TTable>)
        },
      }
    },
    update<TTable extends Table>(table: TTable) {
      assertRegistered(table, registered, tenantId)
      const tenantCol = findTenantColumn(table, columnName, tenantId)
      const propertyKey = tenantPropertyKey(table, columnName, tenantId)
      const builder = q.update(table)
      return {
        set(patch: Partial<ScopedInsertValues<TTable, C>>) {
          if (Object.prototype.hasOwnProperty.call(patch as object, propertyKey)) {
            scopeViolation(
              tenantId,
              `cannot reassign the tenant column "${columnName}" through a scoped update`,
            )
          }
          const next = builder.set(patch as Parameters<typeof builder.set>[0])
          const scoped = next.where(eq(tenantCol, tenantId))
          return guardBuilder(scoped, tenantId, tenantCol)
        },
      }
    },
    delete<TTable extends Table>(table: TTable) {
      assertRegistered(table, registered, tenantId)
      const tenantCol = findTenantColumn(table, columnName, tenantId)
      const builder = q.delete(table)
      const scoped = builder.where(eq(tenantCol, tenantId))
      return guardBuilder(scoped, tenantId, tenantCol)
    },
    get execute() {
      return scopeViolation(tenantId, 'escape hatch "execute" is not available on a scoped querier')
    },
    get $client() {
      return scopeViolation(tenantId, 'escape hatch "$client" is not available on a scoped querier')
    },
    get transaction() {
      return scopeViolation(tenantId, 'escape hatch "transaction" is not available on a scoped querier')
    },
  } as ScopedQuerier<S, C>
}

export function createFkIsolation<
  S extends Record<string, unknown> = Record<string, never>,
  C extends string = 'tenant_id',
>(config: FkIsolationConfig<C>): IsolationAdapter<S, C> {
  const column = (config.column ?? 'tenant_id') as C
  const registered = new Set(config.tables)

  return {
    scopeQuerier(q, tenantId) {
      return createScopeQuerier(q, tenantId, column, registered)
    },
  }
}
