const fs = require('fs');
const path = require('path');
const { once } = require('events');

const WORKSPACE_ROOT = path.resolve(__dirname, '../../../..');
const DEFAULT_ROWS = 1000000;
const DEFAULT_OUTPUT = path.join(
  WORKSPACE_ROOT,
  'data/geoseek/generated/geo_places_1M.csv',
);

const HEADER = [
  'id',
  'nama_tempat',
  'kategori',
  'subkategori',
  'alamat',
  'kecamatan',
  'kota',
  'provinsi',
  'latitude',
  'longitude',
  'no_telp',
  'jam_operasional',
  'rating',
  'jumlah_review',
  'sumber_data',
  'status_verifikasi',
];

const CITY_TARGETS = [
  {
    kota: 'Jakarta',
    provinsi: 'DKI Jakarta',
    target: 200000,
    lat: -6.2,
    lng: 106.8167,
    phonePrefix: '021',
    districts: [
      'Menteng',
      'Gambir',
      'Tanah Abang',
      'Setiabudi',
      'Tebet',
      'Kebayoran Baru',
      'Cilandak',
      'Kebon Jeruk',
      'Taman Sari',
      'Kelapa Gading',
    ],
  },
  {
    kota: 'Surabaya',
    provinsi: 'Jawa Timur',
    target: 100000,
    lat: -7.2575,
    lng: 112.7521,
    phonePrefix: '031',
    districts: [
      'Tegalsari',
      'Genteng',
      'Gubeng',
      'Wonokromo',
      'Sukolilo',
      'Rungkut',
      'Bubutan',
      'Kenjeran',
    ],
  },
  {
    kota: 'Bandung',
    provinsi: 'Jawa Barat',
    target: 90000,
    lat: -6.9175,
    lng: 107.6191,
    phonePrefix: '022',
    districts: [
      'Coblong',
      'Sumur Bandung',
      'Bandung Wetan',
      'Regol',
      'Cicendo',
      'Sukajadi',
      'Batununggal',
      'Lengkong',
    ],
  },
  {
    kota: 'Medan',
    provinsi: 'Sumatera Utara',
    target: 80000,
    lat: 3.5952,
    lng: 98.6722,
    phonePrefix: '061',
    districts: [
      'Medan Baru',
      'Medan Kota',
      'Medan Petisah',
      'Medan Timur',
      'Medan Selayang',
      'Medan Amplas',
    ],
  },
  {
    kota: 'Bekasi',
    provinsi: 'Jawa Barat',
    target: 70000,
    lat: -6.2383,
    lng: 106.9756,
    phonePrefix: '021',
    districts: [
      'Bekasi Selatan',
      'Bekasi Timur',
      'Bekasi Barat',
      'Bekasi Utara',
      'Rawalumbu',
      'Jatiasih',
    ],
  },
  {
    kota: 'Tangerang',
    provinsi: 'Banten',
    target: 70000,
    lat: -6.1783,
    lng: 106.6319,
    phonePrefix: '021',
    districts: [
      'Tangerang',
      'Cipondoh',
      'Karawaci',
      'Ciledug',
      'Pinang',
      'Benda',
      'Kelapa Dua',
    ],
  },
  {
    kota: 'Semarang',
    provinsi: 'Jawa Tengah',
    target: 60000,
    lat: -6.9667,
    lng: 110.4167,
    phonePrefix: '024',
    districts: [
      'Semarang Tengah',
      'Semarang Selatan',
      'Semarang Timur',
      'Tembalang',
      'Banyumanik',
      'Pedurungan',
    ],
  },
  {
    kota: 'Makassar',
    provinsi: 'Sulawesi Selatan',
    target: 50000,
    lat: -5.1477,
    lng: 119.4327,
    phonePrefix: '0411',
    districts: [
      'Ujung Pandang',
      'Panakkukang',
      'Rappocini',
      'Tamalanrea',
      'Biringkanaya',
      'Manggala',
    ],
  },
  {
    kota: 'Palembang',
    provinsi: 'Sumatera Selatan',
    target: 45000,
    lat: -2.9761,
    lng: 104.7754,
    phonePrefix: '0711',
    districts: [
      'Ilir Barat I',
      'Ilir Timur I',
      'Bukit Kecil',
      'Kemuning',
      'Sukarami',
      'Seberang Ulu I',
    ],
  },
  {
    kota: 'Depok',
    provinsi: 'Jawa Barat',
    target: 45000,
    lat: -6.4025,
    lng: 106.7942,
    phonePrefix: '021',
    districts: ['Beji', 'Pancoran Mas', 'Sukmajaya', 'Cimanggis', 'Sawangan'],
  },
  {
    kota: 'Bogor',
    provinsi: 'Jawa Barat',
    target: 40000,
    lat: -6.5971,
    lng: 106.806,
    phonePrefix: '0251',
    districts: [
      'Bogor Tengah',
      'Bogor Utara',
      'Bogor Selatan',
      'Bogor Timur',
      'Tanah Sareal',
    ],
  },
  {
    kota: 'Yogyakarta',
    provinsi: 'DI Yogyakarta',
    target: 35000,
    lat: -7.7956,
    lng: 110.3695,
    phonePrefix: '0274',
    districts: [
      'Gedongtengen',
      'Gondomanan',
      'Kotagede',
      'Umbulharjo',
      'Gondokusuman',
      'Danurejan',
    ],
  },
  {
    kota: 'Denpasar',
    provinsi: 'Bali',
    target: 30000,
    lat: -8.6705,
    lng: 115.2126,
    phonePrefix: '0361',
    districts: [
      'Denpasar Barat',
      'Denpasar Selatan',
      'Denpasar Timur',
      'Denpasar Utara',
    ],
  },
  {
    kota: 'Malang',
    provinsi: 'Jawa Timur',
    target: 30000,
    lat: -7.9666,
    lng: 112.6326,
    phonePrefix: '0341',
    districts: ['Klojen', 'Blimbing', 'Lowokwaru', 'Sukun', 'Kedungkandang'],
  },
  {
    kota: 'Kota lain',
    provinsi: 'Nasional',
    target: 85000,
    lat: -2.5489,
    lng: 118.0149,
    phonePrefix: '021',
    districts: ['Pusat Kota', 'Kecamatan Utama', 'Kawasan Niaga'],
  },
];

const OTHER_CITIES = [
  {
    kota: 'Batam',
    provinsi: 'Kepulauan Riau',
    lat: 1.1301,
    lng: 104.0529,
    phonePrefix: '0778',
    districts: ['Batam Kota', 'Sekupang', 'Lubuk Baja', 'Batu Aji'],
  },
  {
    kota: 'Pekanbaru',
    provinsi: 'Riau',
    lat: 0.5071,
    lng: 101.4478,
    phonePrefix: '0761',
    districts: ['Sukajadi', 'Tampan', 'Marpoyan Damai', 'Tenayan Raya'],
  },
  {
    kota: 'Padang',
    provinsi: 'Sumatera Barat',
    lat: -0.9471,
    lng: 100.4172,
    phonePrefix: '0751',
    districts: ['Padang Barat', 'Padang Timur', 'Koto Tangah', 'Lubuk Begalung'],
  },
  {
    kota: 'Balikpapan',
    provinsi: 'Kalimantan Timur',
    lat: -1.2379,
    lng: 116.8529,
    phonePrefix: '0542',
    districts: [
      'Balikpapan Kota',
      'Balikpapan Selatan',
      'Balikpapan Utara',
    ],
  },
  {
    kota: 'Banjarmasin',
    provinsi: 'Kalimantan Selatan',
    lat: -3.3186,
    lng: 114.5944,
    phonePrefix: '0511',
    districts: ['Banjarmasin Tengah', 'Banjarmasin Timur', 'Banjarmasin Utara'],
  },
  {
    kota: 'Pontianak',
    provinsi: 'Kalimantan Barat',
    lat: -0.0263,
    lng: 109.3425,
    phonePrefix: '0561',
    districts: ['Pontianak Kota', 'Pontianak Selatan', 'Pontianak Timur'],
  },
  {
    kota: 'Manado',
    provinsi: 'Sulawesi Utara',
    lat: 1.4748,
    lng: 124.8421,
    phonePrefix: '0431',
    districts: ['Wenang', 'Malalayang', 'Mapanget', 'Tuminting'],
  },
  {
    kota: 'Mataram',
    provinsi: 'Nusa Tenggara Barat',
    lat: -8.5833,
    lng: 116.1167,
    phonePrefix: '0370',
    districts: ['Mataram', 'Cakranegara', 'Ampenan', 'Sekarbela'],
  },
  {
    kota: 'Ambon',
    provinsi: 'Maluku',
    lat: -3.6954,
    lng: 128.1814,
    phonePrefix: '0911',
    districts: ['Sirimau', 'Nusaniwe', 'Baguala'],
  },
  {
    kota: 'Jayapura',
    provinsi: 'Papua',
    lat: -2.5916,
    lng: 140.669,
    phonePrefix: '0967',
    districts: ['Jayapura Utara', 'Jayapura Selatan', 'Abepura', 'Heram'],
  },
];

const CATEGORY_DEFINITIONS = [
  {
    kategori: 'Restoran',
    subcategories: ['Makanan Sunda', 'Nasi Padang', 'Bakso', 'Sate', 'Seafood'],
    target: 150000,
    prefixes: ['Warung', 'Rumah Makan', 'Dapur', 'Resto'],
    nouns: ['Nusantara', 'Sederhana', 'Ibu Sari', 'Pak Rudi', 'Selera Kota'],
    openingHours: ['07:00-22:00', '08:00-23:00', '10:00-22:00'],
  },
  {
    kategori: 'Cafe',
    subcategories: ['Coffee Shop', 'Kedai Kopi', 'Dessert Cafe', 'Tea House'],
    target: 50000,
    prefixes: ['Cafe', 'Kedai Kopi', 'Coffee House', 'Kopi'],
    nouns: ['Senja', 'Urban', 'Teras', 'Kota', 'Ruang Temu'],
    openingHours: ['08:00-23:00', '09:00-22:00', '10:00-24:00'],
  },
  {
    kategori: 'Hotel',
    subcategories: ['Hotel Bintang 3', 'Hotel Bintang 4', 'Hotel Bintang 5'],
    target: 40000,
    prefixes: ['Hotel', 'Grand Hotel', 'Royal Hotel', 'City Hotel'],
    nouns: ['Nusantara', 'Sentral', 'Prima', 'Indah', 'Metropolitan'],
    openingHours: ['24 Jam'],
  },
  {
    kategori: 'Rumah Sakit',
    subcategories: ['Rumah Sakit Umum', 'Rumah Sakit Ibu dan Anak'],
    target: 25000,
    prefixes: ['RS', 'Rumah Sakit', 'RSU'],
    nouns: ['Sehat Sentosa', 'Medika', 'Harapan', 'Bunda', 'Kasih Ibu'],
    openingHours: ['24 Jam'],
  },
  {
    kategori: 'Apotek',
    subcategories: ['Apotek Umum', 'Farmasi', 'Toko Obat'],
    target: 50000,
    prefixes: ['Apotek', 'Farmasi', 'Toko Obat'],
    nouns: ['Sehat', 'Keluarga', 'Medika', 'Prima', 'Sentosa'],
    openingHours: ['08:00-22:00', '07:00-21:00', '24 Jam'],
  },
  {
    kategori: 'ATM',
    subcategories: ['ATM Bank', 'ATM Center'],
    target: 25000,
    prefixes: ['ATM', 'ATM Center'],
    nouns: ['Mandiri', 'BRI', 'BNI', 'BCA', 'CIMB Niaga'],
    openingHours: ['24 Jam'],
  },
  {
    kategori: 'SPBU',
    subcategories: ['Pom Bensin', 'Stasiun Pengisian BBM'],
    target: 15000,
    prefixes: ['SPBU', 'Pom Bensin'],
    nouns: ['Pertamina', 'Shell', 'BP AKR', 'Vivo', 'Kota'],
    openingHours: ['24 Jam', '05:00-23:00'],
  },
  {
    kategori: 'Bengkel',
    subcategories: ['Bengkel Motor', 'Bengkel Mobil', 'Service AC Mobil'],
    target: 50000,
    prefixes: ['Bengkel', 'Auto Service', 'Service Motor'],
    nouns: ['Maju Jaya', 'Prima Motor', 'Karya Auto', 'Mandiri', 'Sentosa'],
    openingHours: ['08:00-17:00', '08:00-20:00'],
  },
  {
    kategori: 'Wisata',
    subcategories: ['Tempat Wisata', 'Taman', 'Museum', 'Pantai'],
    target: 50000,
    prefixes: ['Wisata', 'Taman', 'Museum', 'Kawasan'],
    nouns: ['Kota', 'Budaya', 'Bahari', 'Alam Indah', 'Heritage'],
    openingHours: ['08:00-17:00', '09:00-18:00', '24 Jam'],
  },
  {
    kategori: 'Mall',
    subcategories: ['Pusat Perbelanjaan', 'Trade Center'],
    target: 30000,
    prefixes: ['Mall', 'Plaza', 'Trade Center'],
    nouns: ['Sentral', 'City Square', 'Metropolitan', 'Garden', 'Junction'],
    openingHours: ['10:00-22:00'],
  },
  {
    kategori: 'Minimarket',
    subcategories: ['Toko Harian', 'Convenience Store'],
    target: 30000,
    prefixes: ['Minimarket', 'Toko Harian', 'Gerai'],
    nouns: ['Kita', 'Maju', 'Sejahtera', 'Keluarga', 'Hemat'],
    openingHours: ['07:00-22:00', '24 Jam'],
  },
  {
    kategori: 'Klinik',
    subcategories: ['Klinik Umum', 'Klinik Gigi', 'Klinik Kecantikan'],
    target: 15000,
    prefixes: ['Klinik', 'Klinik Pratama', 'Medical Center'],
    nouns: ['Sehat', 'Medika', 'Utama', 'Prima', 'Keluarga'],
    openingHours: ['08:00-20:00', '09:00-21:00'],
  },
  {
    kategori: 'Dokter',
    subcategories: ['Praktik Dokter', 'Dokter Gigi', 'Dokter Anak'],
    target: 15000,
    prefixes: ['Praktik Dokter', 'Dokter', 'Klinik Dokter'],
    nouns: ['dr Andi', 'dr Sari', 'dr Budi', 'dr Ratna', 'dr Hadi'],
    openingHours: ['16:00-21:00', '09:00-15:00'],
  },
  {
    kategori: 'Laundry',
    subcategories: ['Laundry Kiloan', 'Dry Cleaning', 'Laundry Sepatu'],
    target: 40000,
    prefixes: ['Laundry', 'Cuci Express', 'Dry Clean'],
    nouns: ['Cepat', 'Bersih', 'Wangi', 'Fresh', 'Kilat'],
    openingHours: ['07:00-21:00', '08:00-20:00'],
  },
  {
    kategori: 'Bank',
    subcategories: ['Kantor Bank', 'Cabang Bank'],
    target: 15000,
    prefixes: ['Bank', 'Kantor Cabang'],
    nouns: ['Mandiri', 'BRI', 'BNI', 'BCA', 'BTN'],
    openingHours: ['08:00-15:00'],
  },
  {
    kategori: 'Sekolah',
    subcategories: ['SD', 'SMP', 'SMA', 'SMK'],
    target: 50000,
    prefixes: ['Sekolah', 'SD', 'SMP', 'SMA', 'SMK'],
    nouns: ['Negeri', 'Harapan Bangsa', 'Bina Insani', 'Cendekia', 'Nusantara'],
    openingHours: ['07:00-15:00'],
  },
  {
    kategori: 'Kampus',
    subcategories: ['Perguruan Tinggi', 'Akademi', 'Politeknik'],
    target: 30000,
    prefixes: ['Universitas', 'Politeknik', 'Institut', 'Akademi'],
    nouns: ['Nusantara', 'Teknologi', 'Mandiri', 'Cendekia', 'Indonesia'],
    openingHours: ['07:00-18:00'],
  },
  {
    kategori: 'Masjid',
    subcategories: ['Masjid', 'Mushola', 'Masjid Raya'],
    target: 110000,
    prefixes: ['Masjid', 'Mushola'],
    nouns: ['Al Ikhlas', 'Al Hidayah', 'At Taqwa', 'Nurul Iman', 'Baiturrahman'],
    openingHours: ['24 Jam'],
  },
  {
    kategori: 'Gereja',
    subcategories: ['Gereja', 'Kapel'],
    target: 40000,
    prefixes: ['Gereja', 'Kapel'],
    nouns: ['Bethel', 'Kristus Raja', 'Immanuel', 'Maranatha', 'Santo Yosef'],
    openingHours: ['06:00-20:00'],
  },
  {
    kategori: 'Penginapan',
    subcategories: ['Guest House', 'Homestay', 'Losmen'],
    target: 25000,
    prefixes: ['Guest House', 'Homestay', 'Penginapan'],
    nouns: ['Nyaman', 'Asri', 'Indah', 'Keluarga', 'Transit'],
    openingHours: ['24 Jam'],
  },
  {
    kategori: 'Kost',
    subcategories: ['Kost Putra', 'Kost Putri', 'Kost Campur'],
    target: 30000,
    prefixes: ['Kost', 'Rumah Kost', 'Kost Eksklusif'],
    nouns: ['Nyaman', 'Dago', 'Mawar', 'Melati', 'Transit'],
    openingHours: ['08:00-20:00'],
  },
  {
    kategori: 'Pasar',
    subcategories: ['Pasar Tradisional', 'Pasar Modern', 'Pasar Grosir'],
    target: 25000,
    prefixes: ['Pasar', 'Pasar Modern', 'Pusat Grosir'],
    nouns: ['Sentral', 'Rakyat', 'Induk', 'Pagi', 'Kota'],
    openingHours: ['05:00-17:00', '06:00-18:00'],
  },
  {
    kategori: 'Rental Mobil',
    subcategories: ['Sewa Mobil', 'Rental Harian'],
    target: 15000,
    prefixes: ['Rental Mobil', 'Sewa Mobil', 'Rent Car'],
    nouns: ['Prima', 'Mandiri', 'Jaya', 'Travelink', 'Kota'],
    openingHours: ['08:00-21:00', '24 Jam'],
  },
  {
    kategori: 'Rental Motor',
    subcategories: ['Sewa Motor', 'Rental Harian'],
    target: 10000,
    prefixes: ['Rental Motor', 'Sewa Motor'],
    nouns: ['Cepat', 'Kita', 'Mandiri', 'Prima', 'Kota'],
    openingHours: ['08:00-21:00'],
  },
  {
    kategori: 'Salon',
    subcategories: ['Salon Kecantikan', 'Hair Studio'],
    target: 20000,
    prefixes: ['Salon', 'Beauty Studio', 'Hair Salon'],
    nouns: ['Cantik', 'Mawar', 'Ayu', 'Glow', 'Ratu'],
    openingHours: ['09:00-21:00'],
  },
  {
    kategori: 'Barbershop',
    subcategories: ['Pangkas Rambut', 'Mens Grooming'],
    target: 20000,
    prefixes: ['Barbershop', 'Pangkas Rambut', 'Urban Cut'],
    nouns: ['Keren', 'Rapi', 'Gentleman', 'Kota', 'Prime'],
    openingHours: ['10:00-22:00'],
  },
  {
    kategori: 'Gym',
    subcategories: ['Pusat Kebugaran', 'Fitness Center'],
    target: 15000,
    prefixes: ['Gym', 'Fitness Center', 'Fit Club'],
    nouns: ['Prima', 'Urban', 'Strong', 'Sehat', 'Active'],
    openingHours: ['06:00-22:00'],
  },
  {
    kategori: 'Kolam Renang',
    subcategories: ['Kolam Renang Umum', 'Waterpark'],
    target: 10000,
    prefixes: ['Kolam Renang', 'Waterpark'],
    nouns: ['Tirta', 'Bahagia', 'Keluarga', 'Segar', 'Aqua'],
    openingHours: ['07:00-18:00'],
  },
  {
    kategori: 'Toko Bangunan',
    subcategories: ['Material Bangunan', 'Perkakas', 'Cat'],
    target: 40000,
    prefixes: ['Toko Bangunan', 'Material', 'Depo Bangunan'],
    nouns: ['Maju Jaya', 'Mandiri', 'Sentosa', 'Karya', 'Prima'],
    openingHours: ['08:00-17:00'],
  },
  {
    kategori: 'Toko Elektronik',
    subcategories: ['Elektronik & Gadget', 'Komputer', 'Aksesoris HP'],
    target: 35000,
    prefixes: ['Toko Elektronik', 'Gadget Store', 'Computer Center'],
    nouns: ['Nusantara', 'Digital', 'Prima', 'Jaya', 'Mega'],
    openingHours: ['09:00-21:00'],
  },
  {
    kategori: 'Toko Pakaian',
    subcategories: ['Fashion', 'Butik', 'Distro'],
    target: 35000,
    prefixes: ['Toko Pakaian', 'Butik', 'Distro'],
    nouns: ['Cantik', 'Modis', 'Urban', 'Mawar', 'Kota'],
    openingHours: ['09:00-21:00'],
  },
  {
    kategori: 'Logistik',
    subcategories: ['Jasa Pengiriman', 'Cargo', 'Ekspedisi'],
    target: 20000,
    prefixes: ['Ekspedisi', 'Cargo', 'Logistik'],
    nouns: ['Cepat', 'Nusantara', 'Express', 'Kilat', 'Jaya'],
    openingHours: ['08:00-20:00'],
  },
  {
    kategori: 'Kurir',
    subcategories: ['Jasa Kurir Lokal', 'Same Day Delivery'],
    target: 10000,
    prefixes: ['Kurir', 'Delivery', 'Jasa Kurir'],
    nouns: ['Cepat', 'Kilat', 'Kota', 'Express', 'Mandiri'],
    openingHours: ['08:00-22:00'],
  },
  {
    kategori: 'Jasa',
    subcategories: ['Layanan Umum', 'Service AC', 'Jasa Kebersihan'],
    target: 30000,
    prefixes: ['Jasa', 'Layanan', 'Service'],
    nouns: ['Mandiri', 'Cepat', 'Profesional', 'Prima', 'Kota'],
    openingHours: ['08:00-18:00'],
  },
  {
    kategori: 'UMKM',
    subcategories: ['Usaha Lokal', 'Produk Rumahan', 'Kerajinan'],
    target: 75000,
    prefixes: ['UMKM', 'Toko', 'Produk Lokal'],
    nouns: ['Mandiri', 'Kreatif', 'Nusantara', 'Rumahan', 'Karya'],
    openingHours: ['08:00-21:00'],
  },
];

const STREET_NAMES = [
  'Jl Sudirman',
  'Jl Ahmad Yani',
  'Jl Merdeka',
  'Jl Diponegoro',
  'Jl Gatot Subroto',
  'Jl Gajah Mada',
  'Jl Veteran',
  'Jl Pemuda',
  'Jl Pahlawan',
  'Jl Kartini',
  'Jl Imam Bonjol',
  'Jl Asia Afrika',
  'Jl Nusantara',
  'Jl Raya Kota',
  'Jl Pasar Baru',
];

function resolveWorkspacePath(inputPath) {
  const resolvedPath = path.resolve(WORKSPACE_ROOT, inputPath);
  const relativePath = path.relative(WORKSPACE_ROOT, resolvedPath);

  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
    throw new Error('--output must stay inside the workspace root');
  }

  return resolvedPath;
}

function parseArgs(argv) {
  return argv.slice(2).reduce(
    (acc, item) => {
      if (item.startsWith('--rows=')) {
        acc.rows = Number(item.split('=')[1]);
      } else if (item.startsWith('--output=')) {
        acc.output = resolveWorkspacePath(item.split('=')[1]);
      } else if (item === '--help' || item === '-h') {
        acc.help = true;
      }

      return acc;
    },
    { rows: DEFAULT_ROWS, output: DEFAULT_OUTPUT, help: false },
  );
}

function printHelp() {
  console.log(`GeoSeek CSV Generator\n\nUsage:\n  node backend/src/scripts/geoseek/generateGeoPlacesCsv.js --rows=1000000 --output=data/geoseek/generated/geo_places_1M.csv\n\nNotes:\n  - Generated rows are synthetic development seed data, not verified real POI.\n  - The generator streams CSV output, so it can create large files without holding all rows in memory.\n`);
}

function createSeededRandom(seed) {
  let value = seed % 2147483647;

  if (value <= 0) {
    value += 2147483646;
  }

  return () => {
    value = (value * 16807) % 2147483647;
    return (value - 1) / 2147483646;
  };
}

const random = createSeededRandom(20260618);

function pick(items) {
  return items[Math.floor(random() * items.length)];
}

function weightedPick(items, weightKey = 'target') {
  const totalWeight = items.reduce((sum, item) => sum + item[weightKey], 0);
  let cursor = random() * totalWeight;

  for (const item of items) {
    cursor -= item[weightKey];
    if (cursor <= 0) {
      return item;
    }
  }

  return items[items.length - 1];
}

function allocateCounts(totalRows, targets) {
  const totalTarget = targets.reduce((sum, item) => sum + item.target, 0);
  const rawAllocations = targets.map((item) => {
    const exact = (item.target / totalTarget) * totalRows;

    return {
      ...item,
      count: Math.floor(exact),
      remainder: exact - Math.floor(exact),
    };
  });
  let remaining = totalRows - rawAllocations.reduce((sum, item) => sum + item.count, 0);
  const sortedByRemainder = [...rawAllocations].sort(
    (a, b) => b.remainder - a.remainder,
  );

  for (let index = 0; remaining > 0; index += 1) {
    sortedByRemainder[index % sortedByRemainder.length].count += 1;
    remaining -= 1;
  }

  return rawAllocations.map((item) => {
    const updated = sortedByRemainder.find(
      (allocation) => allocation.kota === item.kota,
    );

    return {
      ...item,
      count: updated.count,
    };
  });
}

function csvEscape(value) {
  if (value === null || value === undefined) {
    return '';
  }

  const stringValue = String(value);

  if (
    stringValue.includes(',') ||
    stringValue.includes('"') ||
    stringValue.includes('\n')
  ) {
    return `"${stringValue.replace(/"/g, '""')}"`;
  }

  return stringValue;
}

function makeCoordinates(city) {
  const latJitter = (random() - 0.5) * 0.16;
  const lngJitter = (random() - 0.5) * 0.16;

  return {
    latitude: (city.lat + latJitter).toFixed(6),
    longitude: (city.lng + lngJitter).toFixed(6),
  };
}

function makePhone(prefix) {
  const suffix = String(Math.floor(1000000 + random() * 8999999));
  return `${prefix}${suffix}`;
}

function makeRating() {
  return (3.8 + random() * 1.1).toFixed(1);
}

function makeReviewCount(category) {
  const highTrafficCategories = new Set([
    'Restoran',
    'Cafe',
    'Mall',
    'Wisata',
    'Hotel',
    'Rumah Sakit',
  ]);
  const max = highTrafficCategories.has(category) ? 6000 : 1500;
  const min = highTrafficCategories.has(category) ? 80 : 10;

  return Math.floor(min + random() * max);
}

function resolveCity(cityTarget, rowOffset) {
  if (cityTarget.kota !== 'Kota lain') {
    return cityTarget;
  }

  return OTHER_CITIES[rowOffset % OTHER_CITIES.length];
}

function makePlaceRow(id, cityTarget, rowOffset) {
  const city = resolveCity(cityTarget, rowOffset);
  const category = weightedPick(CATEGORY_DEFINITIONS);
  const district = pick(city.districts);
  const street = pick(STREET_NAMES);
  const streetNumber = Math.floor(1 + random() * 299);
  const coords = makeCoordinates(city);
  const name = `${pick(category.prefixes)} ${pick(category.nouns)} ${city.kota}`;

  return [
    id,
    name,
    category.kategori,
    pick(category.subcategories),
    `${street} No ${streetNumber}`,
    district,
    city.kota,
    city.provinsi,
    coords.latitude,
    coords.longitude,
    makePhone(city.phonePrefix),
    pick(category.openingHours),
    makeRating(),
    makeReviewCount(category.kategori),
    'GeoSeekSynthetic',
    'unverified',
  ];
}

async function writeLine(stream, values) {
  const line = `${values.map(csvEscape).join(',')}\n`;

  if (!stream.write(line)) {
    await once(stream, 'drain');
  }
}

async function generateCsv({ rows, output }) {
  if (!Number.isInteger(rows) || rows < 1) {
    throw new Error('--rows must be a positive integer');
  }

  fs.mkdirSync(path.dirname(output), { recursive: true });

  const allocations = allocateCounts(rows, CITY_TARGETS);
  const stream = fs.createWriteStream(output, { encoding: 'utf8' });
  let id = 1;

  await writeLine(stream, HEADER);

  for (const cityAllocation of allocations) {
    for (let index = 0; index < cityAllocation.count; index += 1) {
      await writeLine(stream, makePlaceRow(id, cityAllocation, index));
      id += 1;
    }
  }

  stream.end();
  await once(stream, 'finish');

  return {
    output,
    rows: id - 1,
    rawCityTargetTotal: CITY_TARGETS.reduce((sum, item) => sum + item.target, 0),
  };
}

async function main() {
  const args = parseArgs(process.argv);

  if (args.help) {
    printHelp();
    return;
  }

  const result = await generateCsv(args);
  console.log(
    `Generated ${result.rows.toLocaleString('id-ID')} rows: ${path.relative(
      WORKSPACE_ROOT,
      result.output,
    )}`,
  );
  console.log(
    `Raw city target total is ${result.rawCityTargetTotal.toLocaleString(
      'id-ID',
    )}; allocations were scaled to requested row count.`,
  );
}

if (require.main === module) {
  main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
  });
}

module.exports = {
  allocateCounts,
  generateCsv,
  resolveWorkspacePath,
};
