/**
 * Onboarding step 2 — module selection side effect.
 *
 * Validates client module toggles server-side, applies hard-dependency cascade
 * via @zync/modules helpers, and persists with the same upsert semantics as
 * setModuleStates (tenant-modules query helper).
 */
import type { Db, DbTx } from '@zync/db'
import { auditLog, tenantModules } from '@zync/db/schema'
import {
  canEnable,
  getCascadeDisables,
  type ModuleId,
} from '@zync/modules'
import {
  ONBOARDING_MODULE_SLUGS,
  SLUG_TO_MODULE_ID,
} from '../routes/onboarding.schema'
import { getEnabledModuleIds } from '@zync/db/queries'

type DbOrTx = Db | DbTx

const ONBOARDING_MODULE_IDS: ModuleId[] = ONBOARDING_MODULE_SLUGS.map(
  (slug) => SLUG_TO_MODULE_ID[slug],
)

export class OnboardingModuleValidationError extends Error {
  constructor(
    message: string,
    public readonly code: 'invalid_slug' | 'missing_hard_dependencies',
    public readonly missingDeps?: ModuleId[],
  ) {
    super(message)
    this.name = 'OnboardingModuleValidationError'
  }
}

function parseClientModules(modules: Record<string, boolean>): Map<ModuleId, boolean> {
  for (const key of Object.keys(modules)) {
    if (!(ONBOARDING_MODULE_SLUGS as readonly string[]).includes(key)) {
      throw new OnboardingModuleValidationError(
        `Unknown module slug: ${key}`,
        'invalid_slug',
      )
    }
  }

  const states = new Map<ModuleId, boolean>()
  for (const slug of ONBOARDING_MODULE_SLUGS) {
    if (!(slug in modules)) {
      throw new OnboardingModuleValidationError(
        `Missing module slug: ${slug}`,
        'invalid_slug',
      )
    }
    states.set(SLUG_TO_MODULE_ID[slug], modules[slug]!)
  }
  return states
}

function computeTargetEnabledIds(
  clientStates: Map<ModuleId, boolean>,
  currentEnabled: ModuleId[],
): Set<ModuleId> {
  const enabled = new Set<ModuleId>(['system'])

  for (const [moduleId, isEnabled] of clientStates) {
    if (isEnabled) enabled.add(moduleId)
  }

  for (const [moduleId, isEnabled] of clientStates) {
    if (!isEnabled) {
      enabled.delete(moduleId)
      for (const cascaded of getCascadeDisables(moduleId, currentEnabled)) {
        enabled.delete(cascaded)
      }
    }
  }

  const enabledList = [...enabled]
  for (const moduleId of enabledList) {
    if (moduleId === 'system') continue
    const check = canEnable(moduleId, enabledList)
    if (!check.allowed) {
      throw new OnboardingModuleValidationError(
        'Module selection violates hard dependencies',
        'missing_hard_dependencies',
        check.missingHardDeps,
      )
    }
  }

  return enabled
}

async function upsertModuleState(
  tx: DbOrTx,
  tenantId: string,
  moduleId: ModuleId,
  enabled: boolean,
  actorUserId: string,
): Promise<void> {
  const now = new Date()
  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,
        },
      })
  }
}

export async function applyModuleSelection(
  db: DbOrTx,
  tenantId: string,
  modules: Record<string, boolean>,
  actorUserId: string,
): Promise<void> {
  const clientStates = parseClientModules(modules)
  const currentEnabled = await getEnabledModuleIds(db, tenantId)
  const targetEnabled = computeTargetEnabledIds(clientStates, currentEnabled)

  let changed = false
  for (const moduleId of ONBOARDING_MODULE_IDS) {
    const shouldEnable = targetEnabled.has(moduleId)
    const isEnabled = currentEnabled.includes(moduleId)
    if (shouldEnable === isEnabled) continue
    changed = true
    await upsertModuleState(db, tenantId, moduleId, shouldEnable, actorUserId)
  }

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