import 'dotenv/config'

// @ts-ignore - bcryptjs ships its own types in this repo setup
import bcrypt from 'bcryptjs'

import { prisma } from '../src/db/prisma.js'

type SeedRole = 'admin' | 'user'
type SeedUserStatus = 'active' | 'banned'
type SeedHostStatus = 'online' | 'offline' | 'maintenance'
type SeedHostInstanceType = 'container' | 'vm' | 'both'
type SeedPackageInstanceType = 'container' | 'vm'
type SeedNetworkMode = 'nat' | 'nat_ipv6' | 'nat_ipv6_nat' | 'ipv6_only' | 'ipv6_nat'
type SeedInstanceStatus = 'running' | 'stopped' | 'suspended' | 'error'
type SeedTrafficStatus = 'NORMAL' | 'WARNING' | 'LIMITED'
type SeedProtocol = 'tcp' | 'udp'
type SeedProxyStatus = 'pending' | 'active' | 'error'
type SeedBackupStatus = 'ready' | 'error'
type SeedTicketPriority = 'low' | 'normal' | 'high' | 'urgent'
type SeedTicketStatus = 'open' | 'in_progress' | 'resolved' | 'closed'
type SeedBalanceLogType = 'recharge' | 'consume' | 'gift' | 'admin_adjust'
type SeedNotificationChannelType = 'telegram' | 'discord' | 'email' | 'webhook'
type SeedAnnouncementType = 'system_broadcast' | 'host_broadcast' | 'admin_message' | 'host_message'
type SeedPaymentProviderType = 'yipay' | 'stripe' | 'alipay_direct' | 'wechat_direct' | 'manual'
type SeedRechargeStatus = 'pending' | 'paid' | 'completed' | 'failed' | 'cancelled' | 'refunded'
type SeedAffLogType = 'new_purchase' | 'renew' | 'convert'
type SeedAffWithdrawalStatus = 'pending' | 'approved' | 'rejected'
type SeedRedeemCodeType = 'c' | 'r' | 'd' | 't' | 'p'

type SeedQuota = {
  hostLimit: number
  packageLimit: number
  monthlyTrafficLimit: bigint | null
}

type SeedUserSpec = {
  username: string
  email: string
  role: SeedRole
  status: SeedUserStatus
  banReason: string | null
  avatarStyle: string
  quota: SeedQuota
}

type SeedHostSpec = {
  ownerUsername: string
  name: string
  url: string
  location: string
  countryCode: string
  architecture: 'x86_64' | 'aarch64'
  status: SeedHostStatus
  instanceType: SeedHostInstanceType
  natPublicIp: string
  natPublicIpv6: string
  natBindIp: string
  natBindIpv6: string
  natPortStart: number
  natPortEnd: number
  ipAddress: string
  ipv6Subnet: string
  ipv6Gateway: string
  tags: string[]
  storageSize: number
  cpuAllowanceMax: number
  memoryMax: number
  caddyEnabled: boolean
}

type SeedStoragePoolSpec = {
  name: string
  driver: string
  purpose: 'instance_data' | 'instance_storage'
  description: string
}

type SeedPlanSpec = {
  name: string
  description: string
  cpu: number
  memory: number
  disk: number
  portLimit: number
  snapshotLimit: number
  backupLimit: number
  siteLimit: number
  swapSize: number
  trafficLimit: bigint
  trafficLimitSpeed: string
  price: number
  billingCycle: number
  setupFee: number
  isActive: boolean
  sortOrder: number
  slaGuarantee: number | null
}

type SeedPackageSpec = {
  key: string
  ownerUsername: string
  name: string
  description: string
  hostNames: string[]
  cpuMax: number
  memoryMax: number
  diskMax: number
  bandwidthMax: number
  networkMode: SeedNetworkMode
  instanceType: SeedPackageInstanceType
  privileged: boolean
  nested: boolean
  active: boolean
  portLimit: number
  snapshotLimit: number
  backupLimit: number
  siteLimit: number
  monthlyTrafficLimit: bigint
  ioLimitMode: 'throughput' | 'iops'
  limitsRead: string
  limitsWrite: string
  limitsReadIops: number
  limitsWriteIops: number
  limitsIngress: string
  limitsEgress: string
  limitsProcesses: number
  limitsCpuPriority: number
  bootAutostart: boolean
  bootAutostartPriority: number
  bootAutostartDelay: number
  bootHostShutdownTimeout: number
  globalShared: boolean
  globalQuotaMultiplier: number | null
  globalMaxInstances: number | null
  allowInstanceDeletion: boolean
  plans: SeedPlanSpec[]
}

type GeneratedPackageRecord = {
  id: number
  spec: SeedPackageSpec
  planIdsByName: Map<string, number>
}

type SeedInstanceSpec = {
  name: string
  incusId: string
  userUsername: string
  packageKey: string
  planName: string
  hostName: string
  image: string
  status: SeedInstanceStatus
  createdAt: Date
  expiresAt: Date | null
  suspendedAt: Date | null
  suspendedBy: number | null
  suspendReason: string | null
  autoRenew: boolean
  cloudInitState: string | null
  cloudInitSource: string | null
  ipv4: string | null
  ipv6: string | null
  sshPort: number | null
  monthlyTrafficUsed: bigint
  trafficStatus: SeedTrafficStatus
  lastIpv6ReassignAt: Date | null
  lastPlanChangeAt: Date | null
  lastSyncedAt: Date
}

type SeedBalanceLogSpec = {
  userId: number
  type: SeedBalanceLogType
  amount: number
  balanceBefore: number
  balanceAfter: number
  orderId: string | null
  remark: string
}

type GeneratedInstanceRecord = {
  id: number
  userId: number
  hostId: number
  packageId: number
  packagePlanId: number
  spec: SeedInstanceSpec
  packageSpec: SeedPackageSpec
  plan: SeedPlanSpec
}

type CreatedNotificationChannel = {
  id: number
  userId: number
  type: SeedNotificationChannelType
}

type CreatedAffCode = {
  id: number
  userId: number
  packagePlanId: number | null
  code: string
}

const SEED_PREFIX = 'bulkseed'
const SEED_MARK = `[${SEED_PREFIX}]`
const SEED_CODE_PREFIX = SEED_PREFIX.toUpperCase()
const SHARED_PASSWORD = 'BulkSeed123!'
const LOCAL_PASSWORD_SALT = '$2a$10$1234567890123456789012'
const TODAY = new Date()

const avatarStyles = ['bigSmile', 'croodles', 'notionists', 'lorelei']
const countries = ['us', 'sg', 'jp', 'de', 'nl', 'au']
const cities = ['Los Angeles', 'Singapore', 'Tokyo', 'Frankfurt', 'Amsterdam', 'Sydney']
const containerImages = [
  'images:ubuntu/24.04/cloud',
  'images:debian/12/cloud',
  'images:almalinux/9/cloud',
  'images:rockylinux/9/cloud'
]
const vmImages = [
  'images:ubuntu/24.04/cloud',
  'images:debian/12/cloud',
  'images:almalinux/9/cloud'
]
const redeemCodeTypes: SeedRedeemCodeType[] = ['c', 'r', 'd', 't', 'p']

const ownerUsers: SeedUserSpec[] = [
  createOwnerUser(1, 'admin', 'admin', 24, 40),
  createOwnerUser(2, 'apac', 'user', 18, 32),
  createOwnerUser(3, 'eu', 'user', 18, 32),
  createOwnerUser(4, 'us', 'user', 18, 32),
  createOwnerUser(5, 'edge', 'user', 18, 32),
  createOwnerUser(6, 'lab', 'user', 18, 32)
]

const customerUsers: SeedUserSpec[] = Array.from({ length: 30 }, (_, index) => {
  const id = index + 1
  const status: SeedUserStatus = id % 17 === 0 ? 'banned' : 'active'
  return {
    username: `${SEED_PREFIX}-user-${pad(id, 2)}`,
    email: `${SEED_PREFIX}-user-${pad(id, 2)}@incudal.local`,
    role: 'user',
    status,
    banReason: status === 'banned' ? `${SEED_MARK} banned sample user` : null,
    avatarStyle: avatarStyles[index % avatarStyles.length],
    quota: {
      hostLimit: 0,
      packageLimit: 0,
      monthlyTrafficLimit: bytesFromGb(800)
    }
  }
})

const allSeedUsers = [...ownerUsers, ...customerUsers]

const hostSpecs: SeedHostSpec[] = [
  createHostSpec(1, ownerUsers[0].username, 'official-us-west', 'Los Angeles, US', 'us', 'x86_64', 'both', 'online'),
  createHostSpec(2, ownerUsers[0].username, 'official-eu-vm', 'Frankfurt, DE', 'de', 'x86_64', 'vm', 'online'),
  createHostSpec(3, ownerUsers[1].username, 'apac-sg-edge', 'Singapore, SG', 'sg', 'x86_64', 'both', 'online'),
  createHostSpec(4, ownerUsers[1].username, 'apac-tokyo-arm', 'Tokyo, JP', 'jp', 'aarch64', 'container', 'online'),
  createHostSpec(5, ownerUsers[2].username, 'eu-amsterdam', 'Amsterdam, NL', 'nl', 'x86_64', 'both', 'online'),
  createHostSpec(6, ownerUsers[2].username, 'eu-madrid-vm', 'Madrid, ES', 'us', 'x86_64', 'vm', 'online'),
  createHostSpec(7, ownerUsers[3].username, 'us-ashburn', 'Ashburn, US', 'us', 'x86_64', 'both', 'online'),
  createHostSpec(8, ownerUsers[3].username, 'us-dallas-container', 'Dallas, US', 'us', 'x86_64', 'container', 'online'),
  createHostSpec(9, ownerUsers[4].username, 'edge-seoul', 'Seoul, KR', 'jp', 'aarch64', 'container', 'online'),
  createHostSpec(10, ownerUsers[4].username, 'edge-sydney', 'Sydney, AU', 'au', 'x86_64', 'both', 'online'),
  createHostSpec(11, ownerUsers[5].username, 'lab-london', 'London, GB', 'de', 'x86_64', 'both', 'online'),
  createHostSpec(12, ownerUsers[5].username, 'lab-warsaw-vm', 'Warsaw, PL', 'nl', 'x86_64', 'vm', 'online')
]

const storagePools: SeedStoragePoolSpec[] = [
  {
    name: 'default',
    driver: 'zfs',
    purpose: 'instance_data',
    description: `${SEED_MARK} default pool`
  },
  {
    name: 'archive',
    driver: 'dir',
    purpose: 'instance_storage',
    description: `${SEED_MARK} archive pool`
  }
]

const packageSpecs = buildPackageSpecs()
const instanceSpecs = buildInstanceSpecs(packageSpecs)

function createOwnerUser(index: number, slug: string, role: SeedRole, hostLimit: number, packageLimit: number): SeedUserSpec {
  return {
    username: `${SEED_PREFIX}-owner-${slug}`,
    email: `${SEED_PREFIX}-owner-${slug}@incudal.local`,
    role,
    status: 'active',
    banReason: null,
    avatarStyle: avatarStyles[(index - 1) % avatarStyles.length],
    quota: {
      hostLimit,
      packageLimit,
      monthlyTrafficLimit: bytesFromGb(3000)
    }
  }
}

function createHostSpec(
  index: number,
  ownerUsername: string,
  slug: string,
  location: string,
  countryCode: string,
  architecture: 'x86_64' | 'aarch64',
  instanceType: SeedHostInstanceType,
  status: SeedHostStatus
): SeedHostSpec {
  const natBase = 40 + index
  const hex = (0x40 + index).toString(16)
  return {
    ownerUsername,
    name: `${SEED_PREFIX}-host-${pad(index, 2)}-${slug}`,
    url: `https://${SEED_PREFIX}-host-${pad(index, 2)}-${slug}.local`,
    location,
    countryCode,
    architecture,
    status,
    instanceType,
    natPublicIp: `198.51.100.${natBase}`,
    natPublicIpv6: `2001:db8:feed:${hex}::${index}`,
    natBindIp: `10.255.${index}.10`,
    natBindIpv6: `fd00:255:${hex}::10`,
    natPortStart: 20000 + (index - 1) * 400,
    natPortEnd: 20000 + (index - 1) * 400 + 399,
    ipAddress: `10.20.${index}.10`,
    ipv6Subnet: `2001:db8:${hex}::/64`,
    ipv6Gateway: `2001:db8:${hex}::1`,
    tags: [SEED_PREFIX, slug, architecture, instanceType],
    storageSize: 200 + index * 15,
    cpuAllowanceMax: 2200 + index * 180,
    memoryMax: 65536 + index * 4096,
    caddyEnabled: index % 2 === 0
  }
}

function bytesFromGb(gb: number): bigint {
  return BigInt(gb) * 1024n * 1024n * 1024n
}

function pad(value: number, digits: number): string {
  return String(value).padStart(digits, '0')
}

function daysAgo(days: number, hour = 10): Date {
  const date = new Date(TODAY)
  date.setUTCDate(date.getUTCDate() - days)
  date.setUTCHours(hour, 0, 0, 0)
  return date
}

function daysFromNow(days: number, hour = 10): Date {
  const date = new Date(TODAY)
  date.setUTCDate(date.getUTCDate() + days)
  date.setUTCHours(hour, 0, 0, 0)
  return date
}

function addMonths(input: Date, months: number): Date {
  const output = new Date(input)
  output.setUTCMonth(output.getUTCMonth() + months)
  return output
}

function roundToStep(value: number, step: number): number {
  return Math.max(step, Math.round(value / step) * step)
}

function getBaseRechargeAmount(user: SeedUserSpec, index: number, spend: number): number {
  return Math.max(
    user.role === 'admin' ? 30000 + index * 500 : 9000 + index * 120,
    spend + 2000 + index * 50
  )
}

function getGiftAmount(user: SeedUserSpec, index: number): number {
  return user.role === 'admin' ? 1500 : 300 + index * 10
}

function getSeedOrderNo(index: number): string {
  return `${SEED_PREFIX}-recharge-${pad(index + 1, 3)}`
}

function buildDemoPublicKey(username: string, index: number): string {
  const body = Buffer.from(`${SEED_PREFIX}:${username}:${index}`).toString('base64')
  return `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI${body.slice(0, 40)} ${username}@seed.local`
}

function buildDemoFingerprint(username: string, index: number): string {
  const body = Buffer.from(`${username}:${index}:${SEED_PREFIX}`).toString('base64')
  return `SHA256:${body.slice(0, 32)}`
}

function getOwnerHosts(ownerUsername: string): SeedHostSpec[] {
  return hostSpecs.filter(host => host.ownerUsername === ownerUsername)
}

function buildPackageSpecs(): SeedPackageSpec[] {
  const thirdModes: SeedNetworkMode[] = ['nat_ipv6_nat', 'ipv6_only', 'ipv6_nat']
  const packageList: SeedPackageSpec[] = []

  ownerUsers.forEach((owner, ownerIndex) => {
    const hosts = getOwnerHosts(owner.username)
    const containerHosts = hosts.filter(host => host.instanceType !== 'vm')
    const vmHosts = hosts.filter(host => host.instanceType !== 'container')
    const cpuBase = 220 + ownerIndex * 20
    const memoryBase = 8192 + ownerIndex * 1024
    const diskBase = 102400 + ownerIndex * 10240
    const thirdMode = thirdModes[ownerIndex % thirdModes.length]

    packageList.push(
      createPackageSpec({
        index: packageList.length + 1,
        ownerUsername: owner.username,
        suffix: 'nat-box',
        title: `NAT Box ${ownerIndex + 1}`,
        description: `${SEED_MARK} public IPv4 NAT container package`,
        hostNames: uniqueNames(containerHosts.map(host => host.name)),
        networkMode: 'nat',
        instanceType: 'container',
        cpuMax: cpuBase,
        memoryMax: memoryBase,
        diskMax: diskBase,
        bandwidthMax: 1200 + ownerIndex * 120,
        globalShared: true,
        portLimit: 24,
        snapshotLimit: 5,
        backupLimit: 3,
        siteLimit: 4,
        monthlyTrafficLimit: bytesFromGb(480 + ownerIndex * 40),
        ioLimitMode: ownerIndex % 2 === 0 ? 'throughput' : 'iops',
        globalQuotaMultiplier: 1.5
      }),
      createPackageSpec({
        index: packageList.length + 2,
        ownerUsername: owner.username,
        suffix: 'dual-stack',
        title: `Dual Stack ${ownerIndex + 1}`,
        description: `${SEED_MARK} IPv4 NAT + routed IPv6 package`,
        hostNames: uniqueNames(containerHosts.map(host => host.name)),
        networkMode: 'nat_ipv6',
        instanceType: 'container',
        cpuMax: cpuBase + 30,
        memoryMax: memoryBase + 1024,
        diskMax: diskBase + 10240,
        bandwidthMax: 1400 + ownerIndex * 120,
        globalShared: ownerIndex % 2 === 0,
        portLimit: 28,
        snapshotLimit: 6,
        backupLimit: 3,
        siteLimit: 5,
        monthlyTrafficLimit: bytesFromGb(560 + ownerIndex * 40),
        ioLimitMode: 'throughput',
        globalQuotaMultiplier: 1.8
      }),
      createPackageSpec({
        index: packageList.length + 3,
        ownerUsername: owner.username,
        suffix: `ipv6-${thirdMode}`,
        title: `IPv6 Mix ${ownerIndex + 1}`,
        description: `${SEED_MARK} IPv6-heavy container package`,
        hostNames: uniqueNames(containerHosts.map(host => host.name)),
        networkMode: thirdMode,
        instanceType: 'container',
        cpuMax: cpuBase + 10,
        memoryMax: memoryBase + 512,
        diskMax: diskBase,
        bandwidthMax: 1000 + ownerIndex * 100,
        globalShared: true,
        portLimit: thirdMode === 'ipv6_only' ? 0 : 18,
        snapshotLimit: 4,
        backupLimit: 2,
        siteLimit: 4,
        monthlyTrafficLimit: bytesFromGb(360 + ownerIndex * 30),
        ioLimitMode: 'throughput',
        globalQuotaMultiplier: 1.2
      }),
      createPackageSpec({
        index: packageList.length + 4,
        ownerUsername: owner.username,
        suffix: 'vm-flex',
        title: `VM Flex ${ownerIndex + 1}`,
        description: `${SEED_MARK} KVM package for billing and lifecycle screens`,
        hostNames: uniqueNames(vmHosts.map(host => host.name)),
        networkMode: ownerIndex % 2 === 0 ? 'nat_ipv6' : 'nat',
        instanceType: 'vm',
        cpuMax: 300 + ownerIndex * 30,
        memoryMax: 12288 + ownerIndex * 1024,
        diskMax: 153600 + ownerIndex * 10240,
        bandwidthMax: 1800 + ownerIndex * 100,
        globalShared: ownerIndex % 3 !== 0,
        portLimit: 10,
        snapshotLimit: 4,
        backupLimit: 3,
        siteLimit: 1,
        monthlyTrafficLimit: bytesFromGb(700 + ownerIndex * 50),
        ioLimitMode: 'iops',
        globalQuotaMultiplier: 1.0
      })
    )
  })

  return packageList
}

function createPackageSpec(input: {
  index: number
  ownerUsername: string
  suffix: string
  title: string
  description: string
  hostNames: string[]
  networkMode: SeedNetworkMode
  instanceType: SeedPackageInstanceType
  cpuMax: number
  memoryMax: number
  diskMax: number
  bandwidthMax: number
  globalShared: boolean
  portLimit: number
  snapshotLimit: number
  backupLimit: number
  siteLimit: number
  monthlyTrafficLimit: bigint
  ioLimitMode: 'throughput' | 'iops'
  globalQuotaMultiplier: number | null
}): SeedPackageSpec {
  const isVm = input.instanceType === 'vm'
  const key = `${SEED_PREFIX}-pkg-${pad(input.index, 2)}`
  const planNames = isVm ? ['Starter', 'Business', 'Scale'] : ['Nano', 'Plus', 'Prime']
  const planScales = isVm ? [0.28, 0.46, 0.68] : [0.18, 0.36, 0.58]

  const plans = planNames.map((planName, index) => {
    const scale = planScales[index]
    const baseCpu = roundToStep(input.cpuMax * scale, isVm ? 20 : 5)
    const baseMemory = roundToStep(input.memoryMax * scale, 256)
    const baseDisk = roundToStep(input.diskMax * scale, 1024)
    return {
      name: planName,
      description: `${SEED_MARK} ${planName} plan for ${input.title}`,
      cpu: baseCpu,
      memory: baseMemory,
      disk: baseDisk,
      portLimit: input.networkMode === 'ipv6_only' ? 0 : Math.max(0, Math.min(input.portLimit, 6 + index * (isVm ? 2 : 6))),
      snapshotLimit: Math.min(input.snapshotLimit, 2 + index),
      backupLimit: Math.min(input.backupLimit, 1 + Math.floor(index / 1)),
      siteLimit: isVm ? Math.min(input.siteLimit, index === 2 ? 1 : 0) : Math.min(input.siteLimit, 1 + index * 2),
      swapSize: isVm ? 0 : index === 0 ? 0 : index * 512,
      trafficLimit: bytesFromGb((isVm ? 180 : 90) + index * (isVm ? 160 : 80) + input.index * 5),
      trafficLimitSpeed: ['2Mbit', '5Mbit', '10Mbit'][index],
      price: (isVm ? 2990 : 890) + input.index * (isVm ? 60 : 25) + index * (isVm ? 1800 : 900),
      billingCycle: index === 2 && isVm ? 3 : 1,
      setupFee: isVm ? index * 300 : 0,
      isActive: true,
      sortOrder: (index + 1) * 10,
      slaGuarantee: [99.5, 99.9, 99.95][index]
    }
  })

  return {
    key,
    ownerUsername: input.ownerUsername,
    name: `${key}-${input.suffix}`,
    description: input.description,
    hostNames: input.hostNames,
    networkMode: input.networkMode,
    instanceType: input.instanceType,
    cpuMax: input.cpuMax,
    memoryMax: input.memoryMax,
    diskMax: input.diskMax,
    bandwidthMax: input.bandwidthMax,
    privileged: !isVm,
    nested: !isVm,
    active: true,
    portLimit: input.portLimit,
    snapshotLimit: input.snapshotLimit,
    backupLimit: input.backupLimit,
    siteLimit: input.siteLimit,
    monthlyTrafficLimit: input.monthlyTrafficLimit,
    ioLimitMode: input.ioLimitMode,
    limitsRead: isVm ? '350MB' : '220MB',
    limitsWrite: isVm ? '350MB' : '220MB',
    limitsReadIops: isVm ? 2200 : 1100,
    limitsWriteIops: isVm ? 2200 : 1100,
    limitsIngress: isVm ? '1Gbit' : '600Mbit',
    limitsEgress: isVm ? '1Gbit' : '600Mbit',
    limitsProcesses: isVm ? 800 : 1200,
    limitsCpuPriority: isVm ? 8 : 10,
    bootAutostart: true,
    bootAutostartPriority: 10 + (input.index % 10),
    bootAutostartDelay: 10 + (input.index % 5) * 5,
    bootHostShutdownTimeout: isVm ? 60 : 45,
    globalShared: input.globalShared,
    globalQuotaMultiplier: input.globalQuotaMultiplier,
    globalMaxInstances: 12,
    allowInstanceDeletion: true,
    plans
  }
}

function uniqueNames(values: string[]): string[] {
  return Array.from(new Set(values))
}

function buildInstanceSpecs(packages: SeedPackageSpec[]): SeedInstanceSpec[] {
  return Array.from({ length: 64 }, (_, index) => {
    const id = index + 1
    const packageSpec = packages[index % packages.length]
    const planName = packageSpec.plans[index % packageSpec.plans.length].name
    const hostName = packageSpec.hostNames[index % packageSpec.hostNames.length]
    const status = pickInstanceStatus(index)
    const createdAt = daysAgo(4 + (index % 120), 8 + (index % 6))
    const expiresAt = status === 'suspended' ? daysAgo(1 + (index % 4), 9) : daysFromNow(20 + (index % 90), 9)
    const ipv4 = ['nat', 'nat_ipv6', 'nat_ipv6_nat'].includes(packageSpec.networkMode)
      ? `10.${60 + (index % 20)}.${1 + Math.floor(index / 20)}.${20 + (index % 180)}`
      : null
    const routedIpv6 = packageSpec.networkMode === 'nat_ipv6' || packageSpec.networkMode === 'ipv6_only'
      ? `2001:db8:${(0x900 + index).toString(16)}::${100 + id}`
      : null
    const sharedIpv6 = packageSpec.networkMode === 'nat_ipv6_nat' || packageSpec.networkMode === 'ipv6_nat'
      ? `2001:db8:share::${id}`
      : null
    const primaryIpv6 = routedIpv6 ?? sharedIpv6
    const firstPublicPort = packageSpec.networkMode === 'ipv6_only' ? null : 21000 + index * 6
    const trafficStatus: SeedTrafficStatus = index % 11 === 0 ? 'LIMITED' : index % 7 === 0 ? 'WARNING' : 'NORMAL'
    const monthlyTrafficUsed = trafficStatus === 'LIMITED'
      ? bytesFromGb(240 + index * 3)
      : trafficStatus === 'WARNING'
        ? bytesFromGb(180 + index * 2)
        : bytesFromGb(70 + index * 2)

    return {
      name: `${SEED_PREFIX}-inst-${pad(id, 3)}`,
      incusId: `${SEED_PREFIX}-incus-${pad(id, 3)}`,
      userUsername: customerUsers[(index * 7) % customerUsers.length].username,
      packageKey: packageSpec.key,
      planName,
      hostName,
      image: packageSpec.instanceType === 'vm'
        ? vmImages[index % vmImages.length]
        : containerImages[index % containerImages.length],
      status,
      createdAt,
      expiresAt,
      suspendedAt: status === 'suspended' ? daysAgo(index % 4, 9) : null,
      suspendedBy: status === 'suspended' && index % 2 === 0 ? 1 : null,
      suspendReason: status === 'suspended' ? `${SEED_MARK} expired sample` : null,
      autoRenew: index % 3 !== 0,
      cloudInitState: status === 'error' ? 'error' : 'done',
      cloudInitSource: packageSpec.instanceType === 'vm' ? 'cloud-init' : 'lxd-agent',
      ipv4,
      ipv6: primaryIpv6,
      sshPort: firstPublicPort ?? 22,
      monthlyTrafficUsed,
      trafficStatus,
      lastIpv6ReassignAt: packageSpec.networkMode === 'nat_ipv6' && index % 5 === 0 ? daysAgo(index % 3, 7) : null,
      lastPlanChangeAt: index % 6 === 0 ? daysAgo(2 + (index % 9), 11) : null,
      lastSyncedAt: daysAgo(index % 5, 14)
    }
  })
}

function pickInstanceStatus(index: number): SeedInstanceStatus {
  const mod = index % 20
  if (mod === 17) return 'error'
  if (mod === 18) return 'suspended'
  if (mod === 19 || mod === 14 || mod === 15) return 'stopped'
  return 'running'
}

async function ensureLocalDevGuard(): Promise<void> {
  const databaseUrl = process.env.DATABASE_URL || ''
  const isDevDb = databaseUrl.includes('incudal_dev_password@db:5432/incudal')
  if (!isDevDb && process.env.FORCE_BULK_SEED !== '1') {
    throw new Error(
      `Refusing to run ${SEED_PREFIX} seed outside the local dev database. Set FORCE_BULK_SEED=1 to override.`
    )
  }
}

async function buildPasswordHash(password: string): Promise<string> {
  return bcrypt.hash(password, LOCAL_PASSWORD_SALT)
}

async function syncUsers(passwordHash: string): Promise<Map<string, number>> {
  const userIds = new Map<string, number>()

  for (const [index, user] of allSeedUsers.entries()) {
    const saved = await prisma.user.upsert({
      where: { username: user.username },
      create: {
        username: user.username,
        email: user.email,
        passwordHash,
        role: user.role,
        status: user.status,
        banReason: user.banReason,
        balance: 0,
        avatarStyle: user.avatarStyle
      },
      update: {
        email: user.email,
        passwordHash,
        role: user.role,
        status: user.status,
        banReason: user.banReason,
        avatarStyle: user.avatarStyle
      }
    })

    await prisma.userQuota.upsert({
      where: { userId: saved.id },
      create: {
        userId: saved.id,
        hostLimit: user.quota.hostLimit,
        packageLimit: user.quota.packageLimit,
        monthlyTrafficLimit: user.quota.monthlyTrafficLimit
      },
      update: {
        hostLimit: user.quota.hostLimit,
        packageLimit: user.quota.packageLimit,
        monthlyTrafficLimit: user.quota.monthlyTrafficLimit
      }
    })

    userIds.set(user.username, saved.id)
  }

  const seedUserIds = Array.from(userIds.values())
  await prisma.loginRecord.deleteMany({
    where: {
      userId: { in: seedUserIds },
      userAgent: { startsWith: `${SEED_PREFIX}/` }
    }
  })

  for (const [index, user] of allSeedUsers.entries()) {
    const userId = userIds.get(user.username)!
    const firstCountry = countries[index % countries.length]
    const secondCountry = countries[(index + 2) % countries.length]
    await prisma.loginRecord.createMany({
      data: [
        {
          userId,
          ip: `172.19.${10 + (index % 20)}.${20 + (index % 180)}`,
          country: firstCountry.toUpperCase(),
          region: cities[index % cities.length],
          city: cities[index % cities.length],
          isp: 'BulkSeedNet',
          timezone: 'Asia/Shanghai',
          userAgent: `${SEED_PREFIX}/web-chrome`
        },
        {
          userId,
          ip: `172.20.${10 + (index % 20)}.${40 + (index % 180)}`,
          country: secondCountry.toUpperCase(),
          region: cities[(index + 1) % cities.length],
          city: cities[(index + 1) % cities.length],
          isp: 'BulkSeedMobile',
          timezone: 'Asia/Shanghai',
          userAgent: `${SEED_PREFIX}/mobile-safari`
        }
      ]
    })
  }

  return userIds
}

async function syncHosts(userIds: Map<string, number>): Promise<Map<string, number>> {
  const hostIds = new Map<string, number>()

  for (const host of hostSpecs) {
    const userId = userIds.get(host.ownerUsername)
    if (!userId) {
      throw new Error(`Missing owner for host ${host.name}`)
    }

    const saved = await prisma.host.upsert({
      where: {
        userId_name: {
          userId,
          name: host.name
        }
      },
      create: {
        userId,
        name: host.name,
        url: host.url,
        location: host.location,
        countryCode: host.countryCode,
        architecture: host.architecture,
        status: host.status,
        natPublicIp: host.natPublicIp,
        natPublicIpv6: host.natPublicIpv6,
        natBindIp: host.natBindIp,
        natBindIpv6: host.natBindIpv6,
        natPortStart: host.natPortStart,
        natPortEnd: host.natPortEnd,
        tags: host.tags,
        cpuAllowanceMax: host.cpuAllowanceMax,
        memoryMax: host.memoryMax,
        instanceType: host.instanceType,
        ipAddress: host.ipAddress,
        storageDriver: 'zfs',
        storageType: 'loop',
        storageSize: host.storageSize,
        ipv6Mode: 1,
        ipv6Subnet: host.ipv6Subnet,
        ipv6Gateway: host.ipv6Gateway,
        ipv6ParentInterface: 'eth0',
        enableApi: true,
        isInstalled: true,
        caddyEnabled: host.caddyEnabled,
        caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null,
        caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null,
        caddyPort: 8444 + (host.natPortStart % 10),
        transferEnabled: true,
        trafficResetDay: 1 + (host.natPortStart % 20),
        announcement: `${SEED_MARK} ${host.location} demo host`,
        notifyPurchase: true,
        notifyRenew: true,
        notifyDestroy: host.instanceType === 'vm',
        enableResourcePool: true,
        probeUrl: `https://${host.name}.probe.local/health`
      },
      update: {
        url: host.url,
        location: host.location,
        countryCode: host.countryCode,
        architecture: host.architecture,
        status: host.status,
        natPublicIp: host.natPublicIp,
        natPublicIpv6: host.natPublicIpv6,
        natBindIp: host.natBindIp,
        natBindIpv6: host.natBindIpv6,
        natPortStart: host.natPortStart,
        natPortEnd: host.natPortEnd,
        tags: host.tags,
        cpuAllowanceMax: host.cpuAllowanceMax,
        memoryMax: host.memoryMax,
        instanceType: host.instanceType,
        ipAddress: host.ipAddress,
        storageDriver: 'zfs',
        storageType: 'loop',
        storageSize: host.storageSize,
        ipv6Mode: 1,
        ipv6Subnet: host.ipv6Subnet,
        ipv6Gateway: host.ipv6Gateway,
        ipv6ParentInterface: 'eth0',
        enableApi: true,
        isInstalled: true,
        caddyEnabled: host.caddyEnabled,
        caddyUsername: host.caddyEnabled ? `${SEED_PREFIX}-caddy` : null,
        caddyPassword: host.caddyEnabled ? `${SEED_PREFIX}-password` : null,
        transferEnabled: true,
        announcement: `${SEED_MARK} ${host.location} demo host`,
        probeUrl: `https://${host.name}.probe.local/health`
      }
    })

    hostIds.set(host.name, saved.id)

    for (const pool of storagePools) {
      await prisma.storagePool.upsert({
        where: {
          hostId_name: {
            hostId: saved.id,
            name: pool.name
          }
        },
        create: {
          hostId: saved.id,
          name: pool.name,
          driver: pool.driver,
          purpose: pool.purpose,
          description: pool.description,
          config: {
            generatedBy: SEED_PREFIX,
            host: host.name
          }
        },
        update: {
          driver: pool.driver,
          purpose: pool.purpose,
          description: pool.description,
          config: {
            generatedBy: SEED_PREFIX,
            host: host.name
          }
        }
      })
    }
  }

  return hostIds
}

async function syncPackages(
  userIds: Map<string, number>,
  hostIds: Map<string, number>
): Promise<Map<string, GeneratedPackageRecord>> {
  const packageMap = new Map<string, GeneratedPackageRecord>()

  for (const pkg of packageSpecs) {
    const userId = userIds.get(pkg.ownerUsername)
    if (!userId) {
      throw new Error(`Missing package owner ${pkg.ownerUsername}`)
    }

    const saved = await prisma.package.upsert({
      where: {
        userId_name: {
          userId,
          name: pkg.name
        }
      },
      create: {
        userId,
        name: pkg.name,
        description: pkg.description,
        cpuMax: pkg.cpuMax,
        memoryMax: pkg.memoryMax,
        diskMax: pkg.diskMax,
        bandwidthMax: pkg.bandwidthMax,
        networkMode: pkg.networkMode,
        instanceType: pkg.instanceType,
        privileged: pkg.privileged,
        nested: pkg.nested,
        active: pkg.active,
        portLimit: pkg.portLimit,
        snapshotLimit: pkg.snapshotLimit,
        backupLimit: pkg.backupLimit,
        siteLimit: pkg.siteLimit,
        monthlyTrafficLimit: pkg.monthlyTrafficLimit,
        ioLimitMode: pkg.ioLimitMode,
        limitsRead: pkg.limitsRead,
        limitsWrite: pkg.limitsWrite,
        limitsReadIops: pkg.limitsReadIops,
        limitsWriteIops: pkg.limitsWriteIops,
        limitsIngress: pkg.limitsIngress,
        limitsEgress: pkg.limitsEgress,
        limitsProcesses: pkg.limitsProcesses,
        limitsCpuPriority: pkg.limitsCpuPriority,
        bootAutostart: pkg.bootAutostart,
        bootAutostartPriority: pkg.bootAutostartPriority,
        bootAutostartDelay: pkg.bootAutostartDelay,
        bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
        globalShared: pkg.globalShared,
        globalQuotaMultiplier: pkg.globalQuotaMultiplier,
        globalMaxInstances: pkg.globalMaxInstances,
        allowInstanceDeletion: pkg.allowInstanceDeletion
      },
      update: {
        description: pkg.description,
        cpuMax: pkg.cpuMax,
        memoryMax: pkg.memoryMax,
        diskMax: pkg.diskMax,
        bandwidthMax: pkg.bandwidthMax,
        networkMode: pkg.networkMode,
        instanceType: pkg.instanceType,
        privileged: pkg.privileged,
        nested: pkg.nested,
        active: pkg.active,
        portLimit: pkg.portLimit,
        snapshotLimit: pkg.snapshotLimit,
        backupLimit: pkg.backupLimit,
        siteLimit: pkg.siteLimit,
        monthlyTrafficLimit: pkg.monthlyTrafficLimit,
        ioLimitMode: pkg.ioLimitMode,
        limitsRead: pkg.limitsRead,
        limitsWrite: pkg.limitsWrite,
        limitsReadIops: pkg.limitsReadIops,
        limitsWriteIops: pkg.limitsWriteIops,
        limitsIngress: pkg.limitsIngress,
        limitsEgress: pkg.limitsEgress,
        limitsProcesses: pkg.limitsProcesses,
        limitsCpuPriority: pkg.limitsCpuPriority,
        bootAutostart: pkg.bootAutostart,
        bootAutostartPriority: pkg.bootAutostartPriority,
        bootAutostartDelay: pkg.bootAutostartDelay,
        bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
        globalShared: pkg.globalShared,
        globalQuotaMultiplier: pkg.globalQuotaMultiplier,
        globalMaxInstances: pkg.globalMaxInstances,
        allowInstanceDeletion: pkg.allowInstanceDeletion
      }
    })

    await prisma.packageHost.deleteMany({ where: { packageId: saved.id } })
    await prisma.packageHost.createMany({
      data: pkg.hostNames.map(hostName => {
        const hostId = hostIds.get(hostName)
        if (!hostId) {
          throw new Error(`Missing host ${hostName} for package ${pkg.name}`)
        }
        return {
          packageId: saved.id,
          hostId,
          storagePoolName: 'default'
        }
      })
    })

    for (const plan of pkg.plans) {
      await prisma.packagePlan.upsert({
        where: {
          packageId_name: {
            packageId: saved.id,
            name: plan.name
          }
        },
        create: {
          packageId: saved.id,
          name: plan.name,
          description: plan.description,
          cpu: plan.cpu,
          memory: plan.memory,
          disk: plan.disk,
          portLimit: plan.portLimit,
          snapshotLimit: plan.snapshotLimit,
          backupLimit: plan.backupLimit,
          siteLimit: plan.siteLimit,
          swapSize: plan.swapSize,
          trafficLimit: plan.trafficLimit,
          trafficLimitSpeed: plan.trafficLimitSpeed,
          price: plan.price,
          billingCycle: plan.billingCycle,
          setupFee: plan.setupFee,
          isActive: plan.isActive,
          sortOrder: plan.sortOrder,
          slaGuarantee: plan.slaGuarantee
        },
        update: {
          description: plan.description,
          cpu: plan.cpu,
          memory: plan.memory,
          disk: plan.disk,
          portLimit: plan.portLimit,
          snapshotLimit: plan.snapshotLimit,
          backupLimit: plan.backupLimit,
          siteLimit: plan.siteLimit,
          swapSize: plan.swapSize,
          trafficLimit: plan.trafficLimit,
          trafficLimitSpeed: plan.trafficLimitSpeed,
          price: plan.price,
          billingCycle: plan.billingCycle,
          setupFee: plan.setupFee,
          isActive: plan.isActive,
          sortOrder: plan.sortOrder,
          slaGuarantee: plan.slaGuarantee
        }
      })
    }

    const savedPlans = await prisma.packagePlan.findMany({
      where: { packageId: saved.id },
      orderBy: { sortOrder: 'asc' }
    })

    packageMap.set(pkg.key, {
      id: saved.id,
      spec: pkg,
      planIdsByName: new Map(savedPlans.map(plan => [plan.name, plan.id]))
    })
  }

  return packageMap
}

async function syncInstances(
  userIds: Map<string, number>,
  hostIds: Map<string, number>,
  packagesByKey: Map<string, GeneratedPackageRecord>
): Promise<GeneratedInstanceRecord[]> {
  const records: GeneratedInstanceRecord[] = []

  for (const [index, instance] of instanceSpecs.entries()) {
    const userId = userIds.get(instance.userUsername)
    const pkg = packagesByKey.get(instance.packageKey)
    const hostId = hostIds.get(instance.hostName)
    const hostSpec = hostSpecs.find(candidate => candidate.name === instance.hostName)
    if (!userId || !pkg || !hostId || !hostSpec) {
      throw new Error(`Missing relation while seeding instance ${instance.name}`)
    }

    const planId = pkg.planIdsByName.get(instance.planName)
    const plan = pkg.spec.plans.find(candidate => candidate.name === instance.planName)
    if (!planId || !plan) {
      throw new Error(`Missing plan ${instance.planName} for ${instance.name}`)
    }

    const existing = await prisma.instance.findFirst({
      where: {
        userId,
        name: instance.name
      }
    })

    const saved = existing
      ? await prisma.instance.update({
          where: { id: existing.id },
          data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index)
        })
      : await prisma.instance.create({
          data: buildInstanceData(instance, userId, hostId, hostSpec, pkg.id, planId, pkg.spec, plan, index)
        })

    await syncInstanceChildren(saved.id, userId, hostId, hostSpec, instance, pkg.spec, plan, index)

    records.push({
      id: saved.id,
      userId,
      hostId,
      packageId: pkg.id,
      packagePlanId: planId,
      spec: instance,
      packageSpec: pkg.spec,
      plan
    })
  }

  return records
}

function buildInstanceData(
  spec: SeedInstanceSpec,
  userId: number,
  hostId: number,
  hostSpec: SeedHostSpec,
  packageId: number,
  packagePlanId: number,
  pkg: SeedPackageSpec,
  plan: SeedPlanSpec,
  index: number
) {
  return {
    incusId: spec.incusId,
    name: spec.name,
    userId,
    hostId,
    packageId,
    packagePlanId,
    storagePoolName: 'default',
    image: spec.image,
    status: spec.status,
    cpu: plan.cpu,
    memory: plan.memory,
    disk: plan.disk,
    ipv4: spec.ipv4,
    ipv6: spec.ipv6,
    networkMode: pkg.networkMode,
    sshPort: pkg.networkMode === 'ipv6_only' ? 22 : hostSpec.natPortStart + 20 + index * 4,
    rootPassword: `${SHARED_PASSWORD}!${pad(index + 1, 3)}`,
    snapshottedSpecs: {
      generatedBy: SEED_PREFIX,
      packageKey: pkg.key,
      planName: plan.name,
      cpu: plan.cpu,
      memory: plan.memory,
      disk: plan.disk
    },
    portLimit: plan.portLimit,
    snapshotLimit: plan.snapshotLimit,
    backupLimit: plan.backupLimit,
    siteLimit: plan.siteLimit,
    swapEnabled: plan.swapSize > 0,
    swapSize: plan.swapSize,
    monthlyTrafficLimit: plan.trafficLimit,
    monthlyTrafficUsed: spec.monthlyTrafficUsed,
    trafficStatus: spec.trafficStatus,
    limitsRead: pkg.limitsRead,
    limitsWrite: pkg.limitsWrite,
    limitsReadIops: pkg.limitsReadIops,
    limitsWriteIops: pkg.limitsWriteIops,
    limitsIngress: pkg.limitsIngress,
    limitsEgress: pkg.limitsEgress,
    limitsProcesses: pkg.limitsProcesses,
    limitsCpuPriority: pkg.limitsCpuPriority,
    bootAutostart: pkg.bootAutostart,
    bootAutostartPriority: pkg.bootAutostartPriority,
    bootAutostartDelay: pkg.bootAutostartDelay,
    bootHostShutdownTimeout: pkg.bootHostShutdownTimeout,
    expiresAt: spec.expiresAt,
    suspendedAt: spec.suspendedAt,
    suspendedBy: spec.suspendedBy,
    suspendReason: spec.suspendReason,
    billingPrice: plan.price,
    billingCycle: plan.billingCycle,
    autoRenew: spec.autoRenew,
    iconBadgeId: index % 9 === 0 ? 'fire' : null,
    autoRenewAttempts: spec.autoRenew ? index % 3 : 0,
    lastAutoRenewAttemptAt: spec.autoRenew ? daysAgo(index % 5, 16) : null,
    expiryNotifiedAt: spec.status === 'suspended' ? daysAgo(1, 17) : null,
    version: index % 5,
    lastIpv6ReassignAt: spec.lastIpv6ReassignAt,
    lastPlanChangeAt: spec.lastPlanChangeAt,
    cloudInitState: spec.cloudInitState,
    cloudInitSource: spec.cloudInitSource,
    cloudInitLastCheckedAt: spec.lastSyncedAt,
    cloudInitCompletedAt: spec.status === 'error' ? null : addMonths(spec.createdAt, 0),
    cloudInitManualCompletedAt: spec.status === 'error' ? null : null,
    cloudInitManualCompletedBy: null,
    createdAt: spec.createdAt,
    lastSyncedAt: spec.lastSyncedAt
  }
}

async function syncInstanceChildren(
  instanceId: number,
  userId: number,
  hostId: number,
  hostSpec: SeedHostSpec,
  spec: SeedInstanceSpec,
  pkg: SeedPackageSpec,
  plan: SeedPlanSpec,
  index: number
): Promise<void> {
  await prisma.portMapping.deleteMany({ where: { instanceId } })
  await prisma.snapshot.deleteMany({ where: { instanceId } })
  await prisma.backup.deleteMany({ where: { instanceId } })
  await prisma.proxySite.deleteMany({ where: { instanceId } })
  await prisma.ipAddress.deleteMany({ where: { instanceId } })
  await prisma.ipv6Subnet.deleteMany({ where: { instanceId } })
  await prisma.dailyTraffic.deleteMany({ where: { instanceId } })
  await prisma.instanceBillingRecord.deleteMany({ where: { instanceId } })

  const publicBase = hostSpec.natPortStart + 20 + index * 4
  const supportsPortMappings = pkg.networkMode !== 'ipv6_only' && plan.portLimit > 0
  if (supportsPortMappings) {
    const portSet = pickPortSet(index, pkg.instanceType)
    const mappings = portSet.map((privatePort, mappingIndex) => ({
      instanceId,
      hostId,
      protocol: pickProtocol(index, mappingIndex),
      publicPort: publicBase + mappingIndex,
      privatePort,
      remark: `${SEED_MARK} ${mappingIndex === 0 ? 'ssh-or-app' : 'service'}`
    }))
    await prisma.portMapping.createMany({ data: mappings })
  }

  const snapshotsToCreate = index % 3 === 0 ? 2 : index % 2 === 0 ? 1 : 0
  if (snapshotsToCreate > 0) {
    await prisma.snapshot.createMany({
      data: Array.from({ length: snapshotsToCreate }, (_, snapshotIndex) => ({
        instanceId,
        incusName: `${spec.name}/snap-${snapshotIndex + 1}`,
        name: `${SEED_PREFIX}-snap-${snapshotIndex + 1}`,
        description: `${SEED_MARK} snapshot ${snapshotIndex + 1}`,
        stateful: pkg.instanceType === 'vm' && snapshotIndex === 0,
        size: 1024 + snapshotIndex * 512
      }))
    })
  }

  const backupsToCreate = index % 4 === 0 ? 2 : index % 3 === 0 ? 1 : 0
  if (backupsToCreate > 0) {
    await prisma.backup.createMany({
      data: Array.from({ length: backupsToCreate }, (_, backupIndex) => ({
        instanceId,
        incusName: `${spec.name}/backup-${backupIndex + 1}`,
        name: `${SEED_PREFIX}-backup-${backupIndex + 1}`,
        description: `${SEED_MARK} backup ${backupIndex + 1}`,
        size: 2048 + backupIndex * 768,
        status: backupIndex === backupsToCreate - 1 && index % 8 === 0 ? 'error' : ('ready' as SeedBackupStatus),
        expiresAt: daysFromNow(14 + backupIndex * 7, 6)
      }))
    })
  }

  if (index % 4 === 0) {
    await prisma.backupPolicy.upsert({
      where: { instanceId },
      create: {
        instanceId,
        enabled: true,
        intervalMinutes: 720,
        lastRunAt: daysAgo(1, 2),
        nextRunAt: daysFromNow(1, 2)
      },
      update: {
        enabled: true,
        intervalMinutes: 720,
        lastRunAt: daysAgo(1, 2),
        nextRunAt: daysFromNow(1, 2)
      }
    })
  } else {
    await prisma.backupPolicy.deleteMany({ where: { instanceId } })
  }

  if (index % 3 === 0) {
    await prisma.snapshotPolicy.upsert({
      where: { instanceId },
      create: {
        instanceId,
        enabled: true,
        intervalMinutes: 360,
        lastRunAt: daysAgo(1, 4),
        nextRunAt: daysFromNow(1, 4)
      },
      update: {
        enabled: true,
        intervalMinutes: 360,
        lastRunAt: daysAgo(1, 4),
        nextRunAt: daysFromNow(1, 4)
      }
    })
  } else {
    await prisma.snapshotPolicy.deleteMany({ where: { instanceId } })
  }

  await prisma.trafficSnapshot.upsert({
    where: { instanceId },
    create: {
      instanceId,
      rxRaw: 100_000_000n + BigInt(index) * 10_000n,
      txRaw: 80_000_000n + BigInt(index) * 9_000n
    },
    update: {
      rxRaw: 100_000_000n + BigInt(index) * 10_000n,
      txRaw: 80_000_000n + BigInt(index) * 9_000n
    }
  })

  await prisma.dailyTraffic.createMany({
    data: Array.from({ length: 7 }, (_, dayOffset) => ({
      instanceId,
      date: daysAgo(6 - dayOffset, 0),
      rxTotal: 2_000_000_000n + BigInt(index + dayOffset) * 30_000_000n,
      txTotal: 1_200_000_000n + BigInt(index + dayOffset) * 20_000_000n
    }))
  })

  const shouldAddIpv6 = pkg.networkMode === 'nat_ipv6' || pkg.networkMode === 'ipv6_only'
  if (shouldAddIpv6 && spec.ipv6) {
    await prisma.ipAddress.createMany({
      data: [
        {
          instanceId,
          address: spec.ipv6,
          type: 'inet6',
          isPrimary: true,
          isCustom: false,
          device: 'eth1'
        },
        ...(index % 5 === 0
          ? [
              {
                instanceId,
                address: `${spec.ipv6.split('::')[0]}::${400 + index}`,
                type: 'inet6',
                isPrimary: false,
                isCustom: true,
                device: 'eth1'
              }
            ]
          : [])
      ]
    })
  }

  if (shouldAddIpv6 && spec.ipv6 && index % 6 === 0) {
    await prisma.ipv6Subnet.create({
      data: {
        instanceId,
        cidr: `${spec.ipv6.split('::')[0]}:100::/120`,
        primaryIp: `${spec.ipv6.split('::')[0]}:100::1`,
        device: 'eth1'
      }
    })
  }

  const proxyCount = pkg.instanceType === 'container' && plan.siteLimit > 0 && index % 3 !== 0 ? (index % 5 === 0 ? 2 : 1) : 0
  if (proxyCount > 0) {
    await prisma.proxySite.createMany({
      data: Array.from({ length: proxyCount }, (_, siteIndex) => ({
        instanceId,
        hostId,
        domain: `${spec.name}-${siteIndex + 1}.${SEED_PREFIX}.test`,
        targetPort: [80, 443, 8080][siteIndex % 3],
        httpsEnabled: siteIndex % 2 === 0,
        remark: `${SEED_MARK} proxy ${siteIndex + 1}`,
        status: pickProxyStatus(index, siteIndex),
        enabled: siteIndex % 4 !== 0,
        error: index % 9 === 0 && siteIndex === 0 ? `${SEED_MARK} DNS pending` : null
      }))
    })
  }

  await prisma.instanceBillingRecord.create({
    data: {
      instanceId,
      userId,
      type: 'newPurchase',
      amount: plan.price,
      months: plan.billingCycle,
      periodStart: spec.createdAt,
      periodEnd: addMonths(spec.createdAt, plan.billingCycle),
      remark: `${SEED_MARK} initial purchase`
    }
  })

  if (index % 6 === 0) {
    const renewedStart = addMonths(spec.createdAt, plan.billingCycle)
    await prisma.instanceBillingRecord.create({
      data: {
        instanceId,
        userId,
        type: 'renew',
        amount: plan.price,
        months: plan.billingCycle,
        periodStart: renewedStart,
        periodEnd: addMonths(renewedStart, plan.billingCycle),
        remark: `${SEED_MARK} renewal sample`
      }
    })
  }
}

function pickPortSet(index: number, instanceType: SeedPackageInstanceType): number[] {
  if (instanceType === 'vm') {
    return index % 2 === 0 ? [22, 3389] : [22, 8443]
  }
  const sets = [
    [22, 80],
    [22, 443],
    [25565, 19132],
    [8080, 8443]
  ]
  return sets[index % sets.length]
}

function pickProtocol(index: number, mappingIndex: number): SeedProtocol {
  if (mappingIndex === 1 && index % 4 === 2) return 'udp'
  return 'tcp'
}

function pickProxyStatus(index: number, siteIndex: number): SeedProxyStatus {
  if (index % 9 === 0 && siteIndex === 0) return 'error'
  if (index % 4 === 0) return 'pending'
  return 'active'
}

async function syncTickets(instances: GeneratedInstanceRecord[]): Promise<void> {
  await prisma.ticket.deleteMany({
    where: {
      subject: { startsWith: SEED_MARK }
    }
  })

  const selected = instances.filter((_, index) => index % 5 === 0).slice(0, 12)
  for (const [index, instance] of selected.entries()) {
    const hostOwner = await prisma.host.findUniqueOrThrow({
      where: { id: instance.hostId },
      select: { userId: true }
    })
    const status = (['open', 'in_progress', 'resolved', 'closed'] as SeedTicketStatus[])[index % 4]
    const createdAt = daysAgo(2 + index, 9)
    await prisma.ticket.create({
      data: {
        userId: instance.userId,
        hostId: instance.hostId,
        instanceId: instance.id,
        subject: `${SEED_MARK} ticket-${pad(index + 1, 2)} for ${instance.spec.name}`,
        category: ['general', 'billing', 'technical', 'abuse'][index % 4],
        priority: (['low', 'normal', 'high', 'urgent'] as SeedTicketPriority[])[index % 4],
        status,
        createdAt,
        resolvedAt: status === 'resolved' || status === 'closed' ? daysAgo(1 + index, 12) : null,
        closedAt: status === 'closed' ? daysAgo(index, 15) : null,
        messages: {
          create: [
            {
              senderId: instance.userId,
              content: `${SEED_MARK} user asks about ${instance.spec.name}`,
              isFromOwner: false,
              createdAt
            },
            {
              senderId: hostOwner.userId,
              content: `${SEED_MARK} host owner replied for ${instance.spec.name}`,
              isFromOwner: true,
              createdAt: new Date(createdAt.getTime() + 60 * 60 * 1000)
            },
            ...(index % 3 === 0
              ? [
                  {
                    senderId: instance.userId,
                    content: `${SEED_MARK} follow-up confirmation`,
                    isFromOwner: false,
                    createdAt: new Date(createdAt.getTime() + 2 * 60 * 60 * 1000)
                  }
                ]
              : [])
          ]
        }
      }
    })
  }
}

async function syncBalances(
  userIds: Map<string, number>,
  instances: GeneratedInstanceRecord[]
): Promise<void> {
  const seedUserIds = Array.from(userIds.values())
  await prisma.balanceLog.deleteMany({
    where: {
      userId: { in: seedUserIds },
      remark: { startsWith: SEED_MARK }
    }
  })

  const spendByUser = new Map<number, number>()
  for (const instance of instances) {
    spendByUser.set(instance.userId, (spendByUser.get(instance.userId) || 0) + instance.plan.price)
  }

  const logs: SeedBalanceLogSpec[] = []

  for (const [index, user] of allSeedUsers.entries()) {
    const userId = userIds.get(user.username)!
    const spend = spendByUser.get(userId) || 0
    const baseRecharge = getBaseRechargeAmount(user, index, spend)
    const giftAmount = getGiftAmount(user, index)
    let balanceBefore = 0

    logs.push({
      userId,
      type: user.role === 'admin' ? 'admin_adjust' : 'recharge',
      amount: baseRecharge,
      balanceBefore,
      balanceAfter: balanceBefore + baseRecharge,
      orderId: user.role === 'admin' ? null : getSeedOrderNo(index),
      remark: `${SEED_MARK} initial funding`
    })
    balanceBefore += baseRecharge

    logs.push({
      userId,
      type: 'gift',
      amount: giftAmount,
      balanceBefore,
      balanceAfter: balanceBefore + giftAmount,
      orderId: null,
      remark: `${SEED_MARK} promo gift`
    })
    balanceBefore += giftAmount

    if (spend > 0) {
      logs.push({
        userId,
        type: 'consume',
        amount: -spend,
        balanceBefore,
        balanceAfter: balanceBefore - spend,
        orderId: null,
        remark: `${SEED_MARK} instance purchases`
      })
      balanceBefore -= spend
    }

    await prisma.user.update({
      where: { id: userId },
      data: { balance: balanceBefore }
    })
  }

  await prisma.balanceLog.createMany({ data: logs })
}

async function refreshHostAndQuotaUsage(
  userIds: Map<string, number>,
  hostIds: Map<string, number>,
  instances: GeneratedInstanceRecord[]
): Promise<void> {
  const hostStats = new Map<number, { cpu: number; memory: number; disk: number; ports: number }>()
  for (const hostId of hostIds.values()) {
    hostStats.set(hostId, { cpu: 0, memory: 0, disk: 0, ports: 0 })
  }

  for (const instance of instances) {
    const stats = hostStats.get(instance.hostId)
    if (!stats) continue
    stats.cpu += instance.plan.cpu
    stats.memory += instance.plan.memory
    stats.disk += instance.plan.disk
  }

  const portCounts = await prisma.portMapping.groupBy({
    by: ['hostId'],
    where: {
      hostId: { in: Array.from(hostIds.values()) }
    },
    _count: {
      _all: true
    }
  })

  for (const row of portCounts) {
    const stats = hostStats.get(row.hostId)
    if (stats) {
      stats.ports = row._count._all
    }
  }

  for (const [hostName, hostId] of hostIds.entries()) {
    const stats = hostStats.get(hostId)!
    await prisma.host.update({
      where: { id: hostId },
      data: {
        cpuUsed: stats.cpu,
        memoryUsed: stats.memory,
        diskUsed: stats.disk,
        natPortsUsedCount: stats.ports
      }
    })
  }

  const hostCountByOwner = new Map<number, number>()
  const packageCountByOwner = new Map<number, number>()
  ownerUsers.forEach(owner => {
    const ownerId = userIds.get(owner.username)!
    hostCountByOwner.set(ownerId, hostSpecs.filter(host => host.ownerUsername === owner.username).length)
    packageCountByOwner.set(ownerId, packageSpecs.filter(pkg => pkg.ownerUsername === owner.username).length)
  })

  for (const user of allSeedUsers) {
    const userId = userIds.get(user.username)!
    await prisma.userQuota.update({
      where: { userId },
      data: {
        hostUsed: hostCountByOwner.get(userId) || 0,
        packageUsed: packageCountByOwner.get(userId) || 0
      }
    })
  }
}

async function printSummary(): Promise<void> {
  const [users, hosts, packages, plans, instances, ports, snapshots, backups, sites, tickets] = await Promise.all([
    prisma.user.count({ where: { username: { startsWith: SEED_PREFIX } } }),
    prisma.host.count({ where: { name: { startsWith: `${SEED_PREFIX}-host-` } } }),
    prisma.package.count({ where: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } }),
    prisma.packagePlan.count({ where: { package: { name: { startsWith: `${SEED_PREFIX}-pkg-` } } } }),
    prisma.instance.count({ where: { name: { startsWith: `${SEED_PREFIX}-inst-` } } }),
    prisma.portMapping.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
    prisma.snapshot.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
    prisma.backup.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
    prisma.proxySite.count({ where: { instance: { name: { startsWith: `${SEED_PREFIX}-inst-` } } } }),
    prisma.ticket.count({ where: { subject: { startsWith: SEED_MARK } } })
  ])

  console.log(`${SEED_MARK} completed`)
  console.log(`${SEED_MARK} users=${users}, hosts=${hosts}, packages=${packages}, plans=${plans}, instances=${instances}`)
  console.log(`${SEED_MARK} portMappings=${ports}, snapshots=${snapshots}, backups=${backups}, proxySites=${sites}, tickets=${tickets}`)
  console.log(`${SEED_MARK} shared password for generated users: ${SHARED_PASSWORD}`)
}

async function main(): Promise<void> {
  await ensureLocalDevGuard()
  const passwordHash = await buildPasswordHash(SHARED_PASSWORD)
  const userIds = await syncUsers(passwordHash)
  const hostIds = await syncHosts(userIds)
  const packagesByKey = await syncPackages(userIds, hostIds)
  const instances = await syncInstances(userIds, hostIds, packagesByKey)
  await syncTickets(instances)
  await syncBalances(userIds, instances)
  await refreshHostAndQuotaUsage(userIds, hostIds, instances)
  await printSummary()
}

void main()
  .catch(error => {
    console.error(`${SEED_MARK} failed:`, error)
    process.exitCode = 1
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
