/**
 * Query helpers for tenant_modules (module-management spec).
 *
 * Route files MUST NOT import raw Drizzle tables (`no-raw-drizzle-from-routes`).
 * All DB access for module state lives here.
 *
 * `system` is always-on by convention and is never stored in tenant_modules.
 * Any attempt to pass 'system' to a setter throws synchronously before any
 * DB write.
 */
import { eq, and } from 'drizzle-orm'
import type { Db } from '../client'
import { tenantModules } from '../schema/tenant-modules'
import { auditLog } from './_audit-forward'
import type { TenantModuleRow } from '../schema/tenant-modules'
import type { ModuleId } from '@zync/modules'

export type { TenantModuleRow }

/**
 * Drizzle transaction handle — the methods used here match the top-level Db.
 * This is the canonical pattern used by auth-writes.ts seedSystemRolesTx.
 * Note: setModuleStates requires the full Db (it opens its own transaction).
 */
type Tx = Parameters<Parameters<Db['transaction']>[0]>[0]

/** Accept either the top-level Db or a transaction handle. */
type DbOrTx = Db | Tx

/** Throws if a caller attempts to write the always-on system module. */
function assertNotSystem(moduleId: ModuleId): void {
  if (moduleId === 'system') {
    throw new Error('The system module is always-on and cannot be toggled.')
  }
}

/** List of all 14 toggleable module IDs for seeding. */
const TOGGLEABLE_IDS: readonly ModuleId[] = [
  'crm',
  'customers',
  'time_management',
  'projects',
  'tasks',
  'invoices',
  'expenses',
  'billing',
  'calendar',
  'marketing',
  'reports',
  'kb',
  'contractor_payouts',
  'ai_assistant',
] as const

/**
 * Seed all 14 toggleable modules as enabled=true for a newly created tenant.
 * Idempotent (ON CONFLICT DO NOTHING). Never inserts 'system'.
 * Called by the auth tenant-creation service after creating the tenant row.
 * Accepts both the top-level Db and a Drizzle transaction handle (tx).
 */
export async function seedTenantModules(db: DbOrTx, tenantId: string): Promise<void> {
  const now = new Date()
  await db
    .insert(tenantModules)
    .values(
      TOGGLEABLE_IDS.map((moduleId) => ({
        tenantId,
        moduleId,
        enabled: true,
        enabledAt: now,
      })),
    )
    .onConflictDoNothing()
}

/**
 * Return all tenant_modules rows for a tenant.
 */
export async function getTenantModules(db: DbOrTx, tenantId: string): Promise<TenantModuleRow[]> {
  return db.select().from(tenantModules).where(eq(tenantModules.tenantId, tenantId))
}

/**
 * Return the list of enabled ModuleIds for a tenant.
 * Always includes 'system' synthetically (no DB row for it).
 */
export async function getEnabledModuleIds(db: DbOrTx, tenantId: string): Promise<ModuleId[]> {
  const rows = await db
    .select({ moduleId: tenantModules.moduleId })
    .from(tenantModules)
    .where(and(eq(tenantModules.tenantId, tenantId), eq(tenantModules.enabled, true)))

  const ids: ModuleId[] = ['system', ...rows.map((r) => r.moduleId as ModuleId)]
  return ids
}

/**
 * Transactional upsert of multiple module states.
 * - On disable: sets disabled_at=now(), disabled_by=actorUserId
 * - On enable:  sets enabled_at=now(), disabled_by=null
 * - Always bumps updated_at
 * Throws before any DB write if 'system' is included.
 */
export async function setModuleStates(
  db: Db,
  tenantId: string,
  updates: Array<{ moduleId: ModuleId; enabled: boolean }>,
  actorUserId: string,
): Promise<void> {
  for (const { moduleId } of updates) {
    assertNotSystem(moduleId)
  }

  await db.transaction(async (tx) => {
    const now = new Date()
    for (const { moduleId, enabled } of updates) {
      if (enabled) {
        await tx
          .insert(tenantModules)
          .values({
            tenantId,
            moduleId,
            enabled: true,
            enabledAt: now,
            disabledAt: null,
            disabledBy: null,
            updatedAt: now,
          })
          .onConflictDoUpdate({
            target: [tenantModules.tenantId, tenantModules.moduleId],
            set: {
              enabled: true,
              enabledAt: now,
              disabledAt: null,
              disabledBy: null,
              updatedAt: now,
            },
          })
      } else {
        await tx
          .insert(tenantModules)
          .values({
            tenantId,
            moduleId,
            enabled: false,
            disabledAt: now,
            disabledBy: actorUserId,
            updatedAt: now,
          })
          .onConflictDoUpdate({
            target: [tenantModules.tenantId, tenantModules.moduleId],
            set: {
              enabled: false,
              disabledAt: now,
              disabledBy: actorUserId,
              updatedAt: now,
            },
          })
      }
    }

    await tx.insert(auditLog).values({
      tenantId,
      actorId: actorUserId,
      actorType: 'user',
      entityType: 'tenant_module',
      entityId: tenantId,
      action: 'tenant_module.bulk_updated',
    })
  })
}

/**
 * Unconditional single-row upsert by system admin.
 * Sets override_by_system_admin=true and bypasses dependency checks.
 * Throws before any DB write if 'system' is passed.
 */
export async function setModuleStateAdmin(
  db: Db,
  tenantId: string,
  moduleId: ModuleId,
  enabled: boolean,
  actorUserId: string,
  reason: string,
): Promise<void> {
  assertNotSystem(moduleId)

  await db.transaction(async (tx) => {
    const now = new Date()
    if (enabled) {
      await tx
        .insert(tenantModules)
        .values({
          tenantId,
          moduleId,
          enabled: true,
          enabledAt: now,
          disabledAt: null,
          overrideBySystemAdmin: true,
          updatedAt: now,
        })
        .onConflictDoUpdate({
          target: [tenantModules.tenantId, tenantModules.moduleId],
          set: {
            enabled: true,
            enabledAt: now,
            disabledAt: null,
            overrideBySystemAdmin: true,
            updatedAt: now,
          },
        })
    } else {
      await tx
        .insert(tenantModules)
        .values({
          tenantId,
          moduleId,
          enabled: false,
          disabledAt: now,
          overrideBySystemAdmin: true,
          updatedAt: now,
        })
        .onConflictDoUpdate({
          target: [tenantModules.tenantId, tenantModules.moduleId],
          set: {
            enabled: false,
            disabledAt: now,
            overrideBySystemAdmin: true,
            updatedAt: now,
          },
        })
    }

    await tx.insert(auditLog).values({
      tenantId,
      actorId: actorUserId,
      actorType: 'admin',
      entityType: 'tenant_module',
      entityId: tenantId,
      action: 'tenant_module.admin_override',
      changes: {
        moduleId: [null, moduleId],
        enabled: [null, enabled],
        reason: [null, reason],
        overrideBySystemAdmin: [null, true],
      },
    })
  })
}
