/**
 * Dữ liệu mẫu cho bản demo.
 *
 * Sinh 12 tháng đơn hàng để các chức năng phụ thuộc lịch sử — phân hạng đại lý,
 * cảnh báo ngừng lấy hàng, dự báo mùa vụ — có số liệu thật khi trình diễn,
 * thay vì bảng trống.
 *
 * Số ngẫu nhiên dùng seed cố định nên chạy lại luôn ra cùng bộ dữ liệu; ảnh
 * chụp màn hình và kịch bản demo không bị lệch giữa các lần.
 *
 *   npm run db:reset    → xoá sạch và tạo lại từ đầu
 */
import "./load-env";
import { eq } from "drizzle-orm";
import { db } from "./index";
import * as t from "./schema";
import { hashPassword } from "../lib/password";
import { recomputeClassifications } from "../lib/classification";
import { recomputeKpi } from "../lib/kpi";
import { runAlertScan } from "../lib/alerts";
import { FORECAST_HORIZON_MONTHS, recomputeForecast } from "../lib/forecast";
import { storeFixedFile } from "../lib/storage";
import { today, monthKey, shiftMonth, periodRange, weekStart, weekDays } from "../lib/period";

/* -------------------------------------------------------------------------- */
/* Bộ sinh số ngẫu nhiên có seed (mulberry32)                                 */
/* -------------------------------------------------------------------------- */

function makeRandom(seed: number) {
  let state = seed;
  return () => {
    state |= 0;
    state = (state + 0x6d2b79f5) | 0;
    let x = Math.imul(state ^ (state >>> 15), 1 | state);
    x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
    return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
  };
}
const rnd = makeRandom(20260810);
const pick = <T>(list: T[]): T => list[Math.floor(rnd() * list.length)];
const between = (min: number, max: number) => min + Math.floor(rnd() * (max - min + 1));

/* -------------------------------------------------------------------------- */

const PASSWORD = "matkhau123";

async function main() {
  console.log("Đang tạo dữ liệu mẫu…\n");

  /* --- Xoá dữ liệu cũ, theo thứ tự ngược khoá ngoại --------------------- */
  for (const table of [
    t.tasks,
    t.forecasts,
    t.alerts,
    t.announcements,
    t.companyDocuments,
    t.proposalApprovers,
    t.proposals,
    t.customerCourseCompletions,
    t.trainingCourses,
    t.inviteEventGuests,
    t.inviteEventStaffQuotas,
    t.inviteEventTerritoryQuotas,
    t.inviteEvents,
    t.kpiMonthly,
    t.checkIns,
    t.customerLocations,
    t.customerNotes,
    t.customerContacts,
    t.dailyReports,
    t.planItems,
    t.weeklyPlans,
    t.events,
    t.targets,
    t.customerClassifications,
    t.dealerInactiveNotes,
    t.customerStageHistory,
    t.orderPayments,
    t.orderLines,
    t.orders,
    t.programProducts,
    t.programs,
    t.products,
    t.technologies,
    t.productGroups,
    t.customers,
    t.channels,
    t.userTerritories,
    t.auditLogs,
    t.users,
    t.departments,
    t.territories,
    t.tenants,
  ]) {
    await db.delete(table);
  }

  /* --- Tenant ----------------------------------------------------------- */
  const [tenant] = await db
    .insert(t.tenants)
    .values({ name: "Công ty TNHH Dược Mỹ phẩm Minh Anh", code: "MINHANH", taxCode: "0312345678" })
    .returning();
  const tenantId = tenant.id;

  /* --- Khu vực: miền → tỉnh/thành --------------------------------------- */
  const regionData = [
    { code: "MB", name: "Miền Bắc", provinces: ["Hà Nội", "Hải Phòng", "Quảng Ninh", "Bắc Ninh"] },
    { code: "MT", name: "Miền Trung", provinces: ["Đà Nẵng", "Huế", "Nghệ An", "Khánh Hoà"] },
    { code: "MN", name: "Miền Nam", provinces: ["TP. Hồ Chí Minh", "Cần Thơ", "Bình Dương", "Đồng Nai"] },
  ];

  const regions: { id: string; code: string; name: string }[] = [];
  const provinces: { id: string; name: string; regionCode: string }[] = [];

  for (const region of regionData) {
    const [row] = await db
      .insert(t.territories)
      .values({ tenantId, code: region.code, name: region.name, level: "REGION" })
      .returning();
    regions.push({ id: row.id, code: region.code, name: region.name });

    for (const [i, province] of region.provinces.entries()) {
      const [p] = await db
        .insert(t.territories)
        .values({
          tenantId,
          code: `${region.code}-${String(i + 1).padStart(2, "0")}`,
          name: province,
          level: "PROVINCE",
          parentId: row.id,
        })
        .returning();
      provinces.push({ id: p.id, name: province, regionCode: region.code });
    }
  }

  /* --- Cây phòng ban -----------------------------------------------------
     Dựng theo đúng cơ cấu Mediworld đang chạy trên Getfly: các miền là team nằm
     dưới Phòng kinh doanh, không phải một chiều địa lý riêng. */
  const [deptCompany] = await db
    .insert(t.departments)
    .values({ tenantId, code: "BGD", name: "Ban giám đốc", sortOrder: 0 })
    .returning();

  const [deptSales] = await db
    .insert(t.departments)
    .values({ tenantId, code: "KD", name: "Phòng kinh doanh", parentId: deptCompany.id, sortOrder: 1 })
    .returning();

  const teamRows = await db
    .insert(t.departments)
    .values([
      { tenantId, code: "KD-MB", name: "Team Miền Bắc", parentId: deptSales.id, sortOrder: 1 },
      { tenantId, code: "KD-MT", name: "Team Miền Trung", parentId: deptSales.id, sortOrder: 2 },
      { tenantId, code: "KD-MN", name: "Team Miền Nam", parentId: deptSales.id, sortOrder: 3 },
    ])
    .returning();

  const teamByRegion = new Map(
    teamRows.map((row) => [row.code.replace("KD-", ""), row.id] as [string, string]),
  );

  /* Phòng chuyên môn (phụ trách đào tạo/chuyển giao): nhánh riêng cạnh Phòng kinh doanh, chia team theo miền.
     Khách xác nhận có "trưởng phòng chuyên môn" và "quản lý chuyên môn khu vực" — mỗi người chỉ thấy nhân viên chuyên môn thuộc nhánh của mình. */
  const [deptSpecialist] = await db
    .insert(t.departments)
    .values({ tenantId, code: "CM", name: "Phòng chuyên môn", parentId: deptCompany.id, sortOrder: 2 })
    .returning();
  const specialistTeamRows = await db
    .insert(t.departments)
    .values([
      { tenantId, code: "CM-MB", name: "Chuyên môn Miền Bắc", parentId: deptSpecialist.id, sortOrder: 1 },
      { tenantId, code: "CM-MT", name: "Chuyên môn Miền Trung", parentId: deptSpecialist.id, sortOrder: 2 },
      { tenantId, code: "CM-MN", name: "Chuyên môn Miền Nam", parentId: deptSpecialist.id, sortOrder: 3 },
    ])
    .returning();
  const specialistTeamByRegion = new Map(
    specialistTeamRows.map((row) => [row.code.replace("CM-", ""), row.id] as [string, string]),
  );

  /* --- Người dùng -------------------------------------------------------- */
  const passwordHash = await hashPassword(PASSWORD);

  const [director] = await db
    .insert(t.users)
    .values({
      tenantId,
      email: "giamdoc@minhanh.vn",
      passwordHash,
      fullName: "Nguyễn Minh Anh",
      phone: "0901234567",
      role: "DIRECTOR",
      departmentId: deptCompany.id,
    })
    .returning();

  /* Giám đốc Khu vực — theo khách xác nhận, đây là cấp duyệt đơn hàng thật (1 cấp
     là đủ, không cần cả 4). Gán ở cấp "Phòng kinh doanh" (bao trọn cả 3 team miền)
     vì dữ liệu mẫu mới chỉ có 3 miền gộp, chưa tách đúng 5 khu vực thật của khách —
     việc đó để lại tới khi import dữ liệu thật. */
  const [regionalDirector] = await db
    .insert(t.users)
    .values({
      tenantId,
      email: "gdkv@minhanh.vn",
      passwordHash,
      fullName: "Đặng Quốc Việt",
      phone: `09${between(10000000, 99999999)}`,
      role: "GDKV",
      departmentId: deptSales.id,
    })
    .returning();
  await db.insert(t.userTerritories).values(regions.map((r) => ({ tenantId, userId: regionalDirector.id, territoryId: r.id })));

  const managerData = [
    { email: "quanly.bac@minhanh.vn", name: "Trần Thị Hồng", regionCode: "MB" },
    { email: "quanly.nam@minhanh.vn", name: "Lê Văn Cường", regionCode: "MN" },
  ];
  const managers: (typeof t.users.$inferSelect)[] = [];
  for (const m of managerData) {
    const region = regions.find((r) => r.code === m.regionCode)!;
    const [user] = await db
      .insert(t.users)
      .values({
        tenantId,
        email: m.email,
        passwordHash,
        fullName: m.name,
        phone: `09${between(10000000, 99999999)}`,
        role: "MANAGER",
        territoryId: region.id,
        departmentId: teamByRegion.get(m.regionCode) ?? deptSales.id,
      })
      .returning();
    await db.insert(t.userTerritories).values({ tenantId, userId: user.id, territoryId: region.id });
    managers.push(user);
  }

  const salesData = [
    { email: "nv.hanoi@minhanh.vn", name: "Phạm Thu Hà", province: "Hà Nội" },
    { email: "nv.haiphong@minhanh.vn", name: "Đỗ Quang Huy", province: "Hải Phòng" },
    { email: "nv.danang@minhanh.vn", name: "Võ Thị Lan", province: "Đà Nẵng" },
    { email: "nv.hcm1@minhanh.vn", name: "Bùi Tuấn Kiệt", province: "TP. Hồ Chí Minh" },
    { email: "nv.hcm2@minhanh.vn", name: "Ngô Mỹ Duyên", province: "Bình Dương" },
  ];
  type SalesUser = typeof t.users.$inferSelect & { provinceId: string; provinceName: string };
  const salesUsers: SalesUser[] = [];
  for (const s of salesData) {
    const province = provinces.find((p) => p.name === s.province)!;
    const [user] = await db
      .insert(t.users)
      .values({
        tenantId,
        email: s.email,
        passwordHash,
        fullName: s.name,
        phone: `09${between(10000000, 99999999)}`,
        role: "SALES",
        territoryId: province.id,
        departmentId: teamByRegion.get(province.regionCode) ?? deptSales.id,
      })
      .returning();
    salesUsers.push({ ...user, provinceId: province.id, provinceName: province.name });
  }

  // Gán trưởng phòng cho các team có quản lý vùng.
  for (const manager of managers) {
    if (manager.departmentId) {
      await db
        .update(t.departments)
        .set({ headUserId: manager.id })
        .where(eq(t.departments.id, manager.departmentId));
    }
  }

  // Trưởng bộ phận của Ban giám đốc / Phòng kinh doanh: dùng cho chuỗi duyệt phiếu đề xuất (Trưởng bộ phận → liên quan → TGĐ).
  await db.update(t.departments).set({ headUserId: director.id }).where(eq(t.departments.id, deptCompany.id));
  await db.update(t.departments).set({ headUserId: regionalDirector.id }).where(eq(t.departments.id, deptSales.id));

  /* --- Kênh bán ---------------------------------------------------------- */
  const channelRows = await db
    .insert(t.channels)
    .values([
      { tenantId, code: "BVPK", name: "Bệnh viện / Phòng khám" },
      { tenantId, code: "TMV", name: "Thẩm mỹ viện" },
      { tenantId, code: "SPA", name: "Spa / Clinic" },
      { tenantId, code: "NPP", name: "Nhà phân phối" },
    ])
    .returning();

  /* --- Danh mục sản phẩm -------------------------------------------------- */
  const groupRows = await db
    .insert(t.productGroups)
    .values([
      { tenantId, code: "SKIN", name: "Chăm sóc da" },
      { tenantId, code: "MESO", name: "Mesotherapy" },
      { tenantId, code: "FILL", name: "Filler & Botox" },
      { tenantId, code: "DEVICE", name: "Thiết bị & vật tư" },
    ])
    .returning();

  const techRows = await db
    .insert(t.technologies)
    .values([
      { tenantId, code: "HA", name: "Hyaluronic Acid" },
      { tenantId, code: "PEP", name: "Peptide" },
      { tenantId, code: "STEM", name: "Tế bào gốc" },
      { tenantId, code: "LASER", name: "Laser" },
    ])
    .returning();

  const productData: { code: string; name: string; group: string; tech: string; price: number; role: "CORE" | "POTENTIAL" | "RECOVERY" | "NORMAL" }[] = [
    { code: "SP001", name: "Serum HA Phục hồi 30ml", group: "SKIN", tech: "HA", price: 850_000, role: "CORE" },
    { code: "SP002", name: "Serum Vitamin C 20% 30ml", group: "SKIN", tech: "PEP", price: 920_000, role: "CORE" },
    { code: "SP003", name: "Kem chống nắng SPF50+ 50ml", group: "SKIN", tech: "PEP", price: 480_000, role: "CORE" },
    { code: "SP004", name: "Mặt nạ dưỡng ẩm cấp tốc (hộp 10)", group: "SKIN", tech: "HA", price: 350_000, role: "NORMAL" },
    { code: "SP005", name: "Sữa rửa mặt dịu nhẹ 150ml", group: "SKIN", tech: "PEP", price: 260_000, role: "NORMAL" },
    { code: "SP006", name: "Toner cân bằng pH 200ml", group: "SKIN", tech: "HA", price: 290_000, role: "NORMAL" },
    { code: "SP007", name: "Kem dưỡng tái tạo ban đêm 50g", group: "SKIN", tech: "STEM", price: 1_150_000, role: "POTENTIAL" },
    { code: "SP008", name: "Tinh chất tế bào gốc 5ml x 5", group: "SKIN", tech: "STEM", price: 2_400_000, role: "POTENTIAL" },
    { code: "MS001", name: "Meso trắng da HA 5ml x 10", group: "MESO", tech: "HA", price: 3_200_000, role: "CORE" },
    { code: "MS002", name: "Meso trị mụn Peptide 5ml x 10", group: "MESO", tech: "PEP", price: 2_950_000, role: "CORE" },
    { code: "MS003", name: "Meso căng bóng 2.5ml x 10", group: "MESO", tech: "HA", price: 4_100_000, role: "POTENTIAL" },
    { code: "MS004", name: "Meso nâng cơ Peptide 5ml x 5", group: "MESO", tech: "PEP", price: 3_800_000, role: "NORMAL" },
    { code: "MS005", name: "Meso phục hồi sau laser 5ml x 10", group: "MESO", tech: "STEM", price: 3_500_000, role: "RECOVERY" },
    { code: "FL001", name: "Filler HA 1ml — dòng cơ bản", group: "FILL", tech: "HA", price: 3_900_000, role: "CORE" },
    { code: "FL002", name: "Filler HA 1ml — dòng cao cấp", group: "FILL", tech: "HA", price: 6_500_000, role: "CORE" },
    { code: "FL003", name: "Filler HA 2ml — tạo hình", group: "FILL", tech: "HA", price: 8_900_000, role: "POTENTIAL" },
    { code: "FL004", name: "Chỉ collagen nâng cơ (hộp 10)", group: "FILL", tech: "PEP", price: 5_200_000, role: "NORMAL" },
    { code: "FL005", name: "Botox 100UI", group: "FILL", tech: "PEP", price: 4_800_000, role: "RECOVERY" },
    { code: "DV001", name: "Kim meso 32G (hộp 100)", group: "DEVICE", tech: "LASER", price: 420_000, role: "NORMAL" },
    { code: "DV002", name: "Đầu tip máy laser CO2", group: "DEVICE", tech: "LASER", price: 2_100_000, role: "NORMAL" },
    { code: "DV003", name: "Gel siêu âm 5L", group: "DEVICE", tech: "LASER", price: 180_000, role: "NORMAL" },
    { code: "DV004", name: "Bộ dụng cụ tiêm vô trùng", group: "DEVICE", tech: "LASER", price: 650_000, role: "NORMAL" },
    { code: "DV005", name: "Máy điện di mini cầm tay", group: "DEVICE", tech: "LASER", price: 12_500_000, role: "POTENTIAL" },
    { code: "SP009", name: "Serum ngừa lão hoá Retinol 30ml", group: "SKIN", tech: "PEP", price: 1_050_000, role: "POTENTIAL" },
    { code: "SP010", name: "Kem trị nám chuyên sâu 20g", group: "SKIN", tech: "STEM", price: 1_380_000, role: "RECOVERY" },
  ];

  const products = [];
  for (const p of productData) {
    // Giá bán lẻ luôn cao hơn giá đại lý — hệ số 1,2–1,4 lần tuỳ mặt hàng.
    const retailPrice = Math.round((p.price * (1.2 + rnd() * 0.2)) / 1000) * 1000;
    // Sản phẩm "Khôi phục" đang cần đẩy hàng tồn nên chiết khấu sâu hơn; "Chủ lực"
    // đang bán chạy nên hầu như không cần chiết khấu.
    const discountPercent =
      p.role === "RECOVERY" ? between(10, 20) : p.role === "CORE" ? (rnd() > 0.7 ? 5 : 0) : between(0, 10);

    const [row] = await db
      .insert(t.products)
      .values({
        tenantId,
        code: p.code,
        name: p.name,
        groupId: groupRows.find((g) => g.code === p.group)!.id,
        technologyId: techRows.find((tech) => tech.code === p.tech)!.id,
        unitPrice: p.price,
        retailPrice,
        discountPercent,
        role: p.role,
        unit: p.group === "DEVICE" ? "Cái" : "Hộp",
        volume: { MESO: "5ml x 10 ống", FILL: "1ml", DEVICE: null, SKIN: "30ml" }[p.group as "MESO" | "FILL" | "DEVICE" | "SKIN"] ?? null,
        note: p.role === "RECOVERY" ? "Đang đẩy hàng tồn — chiết khấu sâu hơn mức chuẩn" : p.role === "CORE" ? "Sản phẩm chủ lực quý" : null,
      })
      .returning();
    products.push(row);
  }

  /* --- Chương trình bán hàng --------------------------------------------- */
  const currentMonth = monthKey(today());
  const yearStart = `${currentMonth.slice(0, 4)}-01-01`;

  const [newProgram] = await db
    .insert(t.programs)
    .values({
      tenantId,
      code: "SPMOI2026",
      name: "Sản phẩm mới 2026",
      type: "NEW_PRODUCT",
      startDate: yearStart,
      revenueTarget: 800_000_000,
      qtyTarget: 400,
    })
    .returning();

  const [focusProgram] = await db
    .insert(t.programs)
    .values({
      tenantId,
      code: "TRONGTAM",
      name: "Sản phẩm trọng tâm quý",
      type: "FOCUS_PRODUCT",
      startDate: yearStart,
      revenueTarget: 2_500_000_000,
      qtyTarget: 900,
    })
    .returning();

  const newProgramProducts = products.filter((p) => ["SP008", "SP009", "MS003", "DV005"].includes(p.code));
  const focusProgramProducts = products.filter((p) => ["SP001", "SP002", "MS001", "FL002"].includes(p.code));

  await db.insert(t.programProducts).values([
    ...newProgramProducts.map((p) => ({ tenantId, programId: newProgram.id, productId: p.id })),
    ...focusProgramProducts.map((p) => ({ tenantId, programId: focusProgram.id, productId: p.id })),
  ]);

  const programByProduct = new Map<string, string>();
  for (const p of newProgramProducts) programByProduct.set(p.id, newProgram.id);
  for (const p of focusProgramProducts) programByProduct.set(p.id, focusProgram.id);

  /* --- Khách hàng --------------------------------------------------------- */
  const orgPrefix = ["Phòng khám", "TMV", "Spa", "Clinic", "Thẩm mỹ viện"];
  const orgName = [
    "Ngọc Dung", "Thu Cúc", "Kim Anh", "Hồng Ngọc", "Bảo Ngọc", "An Nhiên", "Mai Hương",
    "Thanh Vân", "Diệu Linh", "Hà My", "Tuệ Minh", "Phương Đông", "Sài Gòn", "Ánh Dương",
    "Việt Mỹ", "Đông Á", "Hoàng Gia", "Ngọc Trai", "Lavender", "Cẩm Tú", "Bích Ngọc",
    "Xuân Hương", "Quỳnh Anh", "Thảo Nguyên", "Minh Châu", "Tường Vy", "Hạ Vy", "Yến Nhi",
  ];

  const customers: {
    id: string;
    ownerId: string;
    departmentId: string | null;
    territoryId: string;
    channelId: string;
    stage: string;
    tierBias: number;
  }[] = [];
  let customerSeq = 0;

  for (const sales of salesUsers) {
    // Mỗi NVKD phụ trách 8 đại lý, 3 KH "Đang", 3 KH "Sẽ".
    const spec: { stage: "DEALER" | "PROSPECT" | "LEAD"; count: number }[] = [
      { stage: "DEALER", count: 8 },
      { stage: "PROSPECT", count: 3 },
      { stage: "LEAD", count: 3 },
    ];

    for (const { stage, count } of spec) {
      for (let i = 0; i < count; i++) {
        customerSeq += 1;
        const channel = pick(channelRows);
        const name = `${pick(orgPrefix)} ${orgName[customerSeq % orgName.length]}`;
        // tierBias quyết định quy mô mua hàng, để phân bố hạng A–E trông thật.
        const tierBias = rnd();

        // Trải ngày tạo trong 14 tháng qua, chỉ vài khách rơi vào tháng hiện tại.
        // Nếu tất cả cùng ngày hôm nay, chỉ số "KH mở mới trong kỳ" sẽ bằng toàn
        // bộ danh sách và không nói lên điều gì khi demo.
        const createdDaysAgo = between(3, 420);
        const createdAt = new Date(Date.now() - createdDaysAgo * 86_400_000);
        // Khoảng 2/3 khách "Đang" xuất phát từ khách tiềm năng Marketing đã được chốt (để demo lọc "Đã chốt").
        const fromLead = stage === "PROSPECT" && rnd() > 0.35;
        const leadState = pick(["NOT_CONTACTED", "NOT_CONTACTED", "CONTACTED", "CANCELLED"]);

        const [row] = await db
          .insert(t.customers)
          .values({
            tenantId,
            createdAt,
            code: `KH${String(customerSeq).padStart(4, "0")}`,
            orgName: name,
            contactName: `Chị ${pick(["Lan", "Hương", "Trang", "Nga", "Yến", "Thảo", "Vy", "Hà"])}`,
            channelId: channel.id,
            territoryId: sales.provinceId,
            // Địa chỉ tách riêng số nhà/đường – phường – tỉnh (tỉnh nằm ở khu vực), đúng yêu cầu mục 5.
            address: `${between(1, 200)} đường ${pick(["Lê Lợi", "Trần Hưng Đạo", "Nguyễn Huệ", "Hai Bà Trưng", "Lý Thường Kiệt"])}`,
            // Phường phải thuộc đúng tỉnh của khách — trộn ngẫu nhiên từng ra "Phường Bến Nghé, Hà Nội", sai địa lý thấy ngay khi demo.
            ward: pick(WARDS_BY_PROVINCE[sales.provinceName] ?? ["Phường Trung tâm"]),
            phone: `09${between(10000000, 99999999)}`,
            // KH "Sẽ"/"Đang" thường chưa đăng ký kinh doanh hoặc chưa cung cấp mã số
            // thuế — chỉ đại lý chính thức mới có, giống thực tế mua bán qua hoá đơn.
            taxCode: stage === "DEALER" ? `0${between(100000000, 999999999)}` : null,
            ownerId: sales.id,
            stage,
            contactPosition: pick(["Chủ cơ sở", "Giám đốc", "Quản lý"]),
            ...(stage === "LEAD"
              ? {
                  leadNeed: pick(["Tư vấn Filler & Botox", "Tìm nhà cung cấp Meso", "Cần thiết bị laser", "Tư vấn dòng chăm sóc da"]),
                  leadSource: pick(["Facebook", "Hội thảo", "Giới thiệu", "Zalo OA", "Website"]),
                  leadStatus: leadState,
                }
              : {}),
            ...(fromLead
              ? {
                  leadNeed: pick(["Tư vấn Filler & Botox", "Tìm nhà cung cấp Meso", "Cần thiết bị laser"]),
                  leadSource: pick(["Facebook", "Hội thảo", "Giới thiệu", "Zalo OA", "Website"]),
                  leadStatus: "CONVERTED",
                }
              : {}),
            ...(stage === "DEALER" && rnd() > 0.6
              ? {
                  receiverName: `Anh ${pick(["Tuấn", "Hải", "Long", "Nam"])} (kho)`,
                  receiverPhone: `09${between(10000000, 99999999)}`,
                  receiverAddress: `Chành xe ${pick(["Hà Sơn Bình", "Thành Bưởi", "Phương Trang"])} — ${sales.provinceName}`,
                }
              : {}),
            dealerSince: stage === "DEALER" ? `${Number(currentMonth.slice(0, 4)) - 1}-${String(between(1, 12)).padStart(2, "0")}-15` : null,
            isKey: stage === "DEALER" && tierBias > 0.85,
          })
          .returning();

        if (fromLead) {
          // Lịch sử: vào hệ thống là khách tiềm năng, vài ngày sau được chốt thành khách "Đang" kèm báo cáo.
          await db.insert(t.customerStageHistory).values({ tenantId, customerId: row.id, toStage: "LEAD", changedBy: sales.id, note: "Khởi tạo dữ liệu ban đầu", changedAt: createdAt });
          await db.insert(t.customerStageHistory).values({
            tenantId,
            customerId: row.id,
            fromStage: "LEAD",
            toStage: stage,
            changedBy: sales.id,
            note: "Chốt khách tiềm năng — khách đồng ý tham dự chương trình của công ty",
            changedAt: new Date(createdAt.getTime() + between(3, 14) * 86_400_000),
          });
          await db.insert(t.customerNotes).values({
            tenantId,
            customerId: row.id,
            content: "[Đã chốt] Khách đồng ý tham dự chương trình của công ty, hẹn chốt đơn đầu tiên.",
            recordedBy: sales.id,
            recordedAt: new Date(createdAt.getTime() + between(3, 14) * 86_400_000),
          });
        } else {
          await db.insert(t.customerStageHistory).values({
            tenantId,
            customerId: row.id,
            toStage: stage,
            changedBy: sales.id,
            note: "Khởi tạo dữ liệu ban đầu",
          });
        }

        // Báo cáo cho khách tiềm năng đã liên hệ / đã huỷ.
        if (stage === "LEAD" && (leadState === "CONTACTED" || leadState === "CANCELLED")) {
          await db.insert(t.customerNotes).values({
            tenantId,
            customerId: row.id,
            content:
              leadState === "CONTACTED"
                ? "[Đã liên hệ] Đã gọi điện, khách quan tâm dòng Filler, hẹn gửi báo giá."
                : "[Đã huỷ] Khách đã chọn nhà cung cấp khác, chưa có nhu cầu trong 6 tháng tới.",
            recordedBy: sales.id,
            recordedAt: new Date(createdAt.getTime() + between(1, 10) * 86_400_000),
          });
        }

        // Người liên quan — đại lý chính thức có thêm người phụ trách mua hàng
        // bên cạnh người liên hệ chính, giống một phòng khám/spa thật.
        if (stage === "DEALER") {
          await db.insert(t.customerContacts).values({
            tenantId,
            customerId: row.id,
            fullName: `${pick(["Nguyễn", "Trần", "Lê", "Phạm", "Hoàng", "Vũ"])} Thị ${pick(["Lan", "Hương", "Trang", "Nga", "Yến", "Thảo"])}`,
            position: pick(["Quản lý mua hàng", "Kế toán", "Chủ cơ sở"]),
            phone: `09${between(10000000, 99999999)}`,
          });
        }

        // Thông tin trao đổi — chỉ ghi cho một phần khách, đúng thực tế là không
        // phải lần gặp nào NVKD cũng note lại.
        if (rnd() > 0.55) {
          const noteText = pick([
            "Đã gọi trao đổi về đơn hàng gần nhất, khách phản hồi tốt.",
            "Ghé thăm trực tiếp, giới thiệu sản phẩm mới trong danh mục.",
            "Khách hỏi về chính sách chiết khấu cho đơn số lượng lớn.",
            "Hẹn quay lại tháng sau để chốt đơn tiếp theo.",
            "Khách quan tâm chương trình khuyến mãi sản phẩm trọng tâm.",
          ]);
          await db.insert(t.customerNotes).values({
            tenantId,
            customerId: row.id,
            content: noteText,
            recordedBy: sales.id,
            recordedAt: new Date(Date.now() - between(1, 60) * 86_400_000),
          });
        }

        // Toạ độ quanh trung tâm tỉnh để check-in có cái để đối chiếu.
        const base = PROVINCE_COORDS[sales.provinceName] ?? { lat: 21.0278, lng: 105.8342 };
        await db.insert(t.customerLocations).values({
          customerId: row.id,
          tenantId,
          lat: base.lat + (rnd() - 0.5) * 0.08,
          lng: base.lng + (rnd() - 0.5) * 0.08,
        });

        customers.push({
          id: row.id,
          ownerId: sales.id,
          departmentId: sales.departmentId,
          territoryId: sales.provinceId,
          channelId: channel.id,
          stage,
          tierBias,
        });
      }
    }
  }

  /* --- Khách tiềm năng mới từ Marketing, CHƯA giao cho NVKD ----------------- */
  // Chỉ Ban giám đốc thấy cho tới khi được giao (thao tác "Giao khách" ở Khách hàng → Khách tiềm năng).
  const mktLeads = [
    { contact: "Chị Ngọc Anh", need: "Tư vấn Filler & Botox", source: "Facebook", ward: "Phường Bến Thành", province: "TP. Hồ Chí Minh" },
    { contact: "Anh Đức Minh", need: "Cần thiết bị laser", source: "Website", ward: "Phường Cầu Giấy", province: "Hà Nội" },
    { contact: "Chị Thu Hằng", need: "Tìm nhà cung cấp Meso", source: "Zalo OA", ward: "Phường Hải Châu", province: "Đà Nẵng" },
    { contact: "Chị Bích Ngân", need: "Tư vấn dòng chăm sóc da", source: "Hội thảo", ward: "Phường Thủ Dầu Một", province: "Bình Dương" },
    { contact: "Anh Quốc Bảo", need: "Tư vấn Filler & Botox", source: "Giới thiệu", ward: "Phường Lê Chân", province: "Hải Phòng" },
    { contact: "Chị Phương Thảo", need: "Tìm nhà cung cấp Meso", source: "Facebook", ward: "Phường Tân Bình", province: "TP. Hồ Chí Minh" },
  ];
  for (const lead of mktLeads) {
    customerSeq += 1;
    const owner = salesUsers.find((u) => u.provinceName === lead.province) ?? salesUsers[0];
    await db.insert(t.customers).values({
      tenantId,
      code: `KH${String(customerSeq).padStart(4, "0")}`,
      orgName: lead.contact,
      contactName: lead.contact,
      phone: `09${between(10000000, 99999999)}`,
      channelId: pick(channelRows).id,
      territoryId: owner.provinceId,
      ward: lead.ward,
      ownerId: null,
      stage: "LEAD",
      leadNeed: lead.need,
      leadSource: lead.source,
      leadStatus: "NOT_CONTACTED",
      createdAt: new Date(Date.now() - between(0, 6) * 86_400_000),
    });
  }

  /* --- Kịch bản cảnh báo thị trường ------------------------------------------
     Một đại lý ở Quảng Ninh chỉ lấy hàng tới cách đây 4 tháng rồi ngừng (tierBias < 0.12 nên vòng lập đơn bên dưới bỏ qua 4 tháng gần nhất):
     Quảng Ninh từng có doanh thu nhưng nay không còn → cảnh báo "thị trường không có doanh thu" có dữ liệu thật để demo. */
  {
    const quangNinh = provinces.find((p) => p.name === "Quảng Ninh");
    const owner = salesUsers.find((u) => u.provinceName === "Hà Nội");
    if (quangNinh && owner) {
      customerSeq += 1;
      const [row] = await db
        .insert(t.customers)
        .values({
          tenantId,
          code: `KH${String(customerSeq).padStart(4, "0")}`,
          orgName: "Phòng khám Hạ Long",
          contactName: "Chị Hải Yến",
          phone: "0912345678",
          channelId: channelRows[0].id,
          territoryId: quangNinh.id,
          ward: "Phường Bãi Cháy",
          ownerId: owner.id,
          stage: "DEALER",
          dealerSince: shiftMonth(currentMonth, 14) + "-10",
          createdAt: new Date(Date.now() - 420 * 86_400_000),
        })
        .returning();
      customers.push({ id: row.id, ownerId: owner.id, departmentId: owner.departmentId, territoryId: quangNinh.id, channelId: channelRows[0].id, stage: "DEALER", tierBias: 0.1 });
    }
  }

  /* --- Đơn hàng 12 tháng -------------------------------------------------- */
  const dealers = customers.filter((c) => c.stage === "DEALER");
  const months = Array.from({ length: 12 }, (_, i) => shiftMonth(currentMonth, 11 - i));
  const todayIso = today();

  const daNangId = provinces.find((p) => p.name === "Đà Nẵng")?.id;
  // Kịch bản cảnh báo: một sản phẩm không có dòng hàng nào trong 3 tháng gần nhất (đã từng bán trước đó) → "sản phẩm không có doanh thu".
  const stalledProduct = products.find((p) => p.code === "SP009");
  let orderSeq = 0;
  const orderValues: (typeof t.orders.$inferInsert)[] = [];
  const lineValues: (typeof t.orderLines.$inferInsert)[] = [];

  for (const month of months) {
    const { start, end } = periodRange(month);
    const lastDay = Number(end.slice(8, 10));

    for (const dealer of dealers) {
      // Một số đại lý cố tình ngừng lấy hàng ở các tháng gần đây, để cảnh báo
      // "2/3/6 tháng không lấy hàng" có dữ liệu thật khi demo.
      const monthsAgo = months.length - 1 - months.indexOf(month);
      if (dealer.tierBias < 0.12 && monthsAgo < 4) continue;
      if (dealer.tierBias < 0.2 && monthsAgo < 2) continue;

      // Đại lý lớn đặt nhiều đơn hơn trong tháng.
      const baseOrderCount = dealer.tierBias > 0.8 ? between(2, 4) : dealer.tierBias > 0.5 ? between(1, 2) : between(0, 1);
      // Kịch bản cảnh báo: Đà Nẵng sụt mạnh ở tháng đã đóng sổ gần nhất (nửa số đại lý không đặt, nửa còn lại tối đa 1 đơn) → "thị trường tụt giảm".
      const orderCount = daNangId && dealer.territoryId === daNangId && monthsAgo === 1 ? (dealers.indexOf(dealer) % 2 === 0 ? 0 : Math.min(baseOrderCount, 1)) : baseOrderCount;

      for (let o = 0; o < orderCount; o++) {
        const day = between(1, lastDay);
        const orderDate = `${month}-${String(day).padStart(2, "0")}`;
        if (orderDate > todayIso) continue;

        orderSeq += 1;
        const orderId = crypto.randomUUID();

        // Đại lý lớn dùng nhiều SKU hơn — trực tiếp tạo ra phân hạng SKU A–E.
        const skuCount = dealer.tierBias > 0.8 ? between(8, 16) : dealer.tierBias > 0.5 ? between(4, 9) : between(1, 5);
        const chosen = shuffle([...products]).slice(0, skuCount);
        if (stalledProduct && monthsAgo < 3) {
          const at = chosen.findIndex((p) => p.id === stalledProduct.id);
          if (at >= 0) {
            const replacement = products.find((p) => p.id !== stalledProduct.id && !chosen.includes(p));
            if (replacement) chosen[at] = replacement;
            else chosen.splice(at, 1);
          }
        }

        let total = 0;
        let promoTotal = 0;

        for (const product of chosen) {
          const qtySold = between(1, dealer.tierBias > 0.8 ? 12 : 5);
          // Khuyến mãi kiểu mua 10 tặng 1.
          const qtyPromo = qtySold >= 10 ? Math.floor(qtySold / 10) : rnd() > 0.85 ? 1 : 0;
          const amount = qtySold * product.unitPrice;
          total += amount;
          promoTotal += qtyPromo * product.unitPrice;

          lineValues.push({
            tenantId,
            orderId,
            productId: product.id,
            qtySold,
            qtyPromo,
            unitPrice: product.unitPrice,
            amount,
            programId: programByProduct.get(product.id) ?? null,
            orderDate,
          });
        }

        // Hạn thanh toán 30 ngày kể từ ngày đặt — công nợ 30 ngày là thông lệ
        // của mô hình phân phối qua phòng khám và spa.
        const dueDate = new Date(Date.parse(`${orderDate}T00:00:00Z`) + 30 * 86_400_000)
          .toISOString()
          .slice(0, 10);

        // Một phần đơn xuất hoá đơn VAT và gửi qua đơn vị vận chuyển — phần còn
        // lại tự đến lấy tại kho, giống thực tế không phải đơn nào cũng cần cả hai.
        const vatRequired = rnd() > 0.6;
        const shipped = rnd() > 0.5;

        orderValues.push({
          id: orderId,
          tenantId,
          code: `DH${month.replace("-", "")}${String(orderSeq).padStart(4, "0")}`,
          customerId: dealer.id,
          orderDate,
          dueDate,
          salesUserId: dealer.ownerId,
          departmentId: dealer.departmentId,
          territoryId: dealer.territoryId,
          channelId: dealer.channelId,
          status: "CONFIRMED",
          totalAmount: total,
          promoValue: promoTotal,
          createdBy: dealer.ownerId,
          vatRequired,
          receiverName: shipped
            ? `${pick(["Anh", "Chị"])} ${pick(["Tuấn", "Hùng", "Minh", "Hạnh", "Loan", "Đức"])}`
            : null,
          receiverPhone: shipped ? `09${between(10000000, 99999999)}` : null,
          receiverAddress: shipped ? "Giao tại địa chỉ đại lý đã đăng ký" : null,
          carrierName: shipped ? pick(["Giao Hàng Nhanh", "Viettel Post", "GHTK", "J&T Express"]) : null,
          carrierPhone: shipped ? `19${between(1000, 9999)}` : null,
          surchargeAmount: shipped && rnd() > 0.7 ? between(3, 15) * 10_000 : 0,
        });
      }
    }
    void start;
  }

  await insertInChunks(t.orders, orderValues);
  await insertInChunks(t.orderLines, lineValues);

  /* --- Thanh toán: dựng công nợ có thật để demo -------------------------
     Đơn cũ phần lớn đã thu đủ; đơn gần đây thu một phần hoặc chưa thu, tạo ra
     công nợ trong hạn và quá hạn — đủ cả ba trạng thái cho màn hình demo. */
  const paymentValues: (typeof t.orderPayments.$inferInsert)[] = [];
  const todayIsoForPayment = today();

  for (const order of orderValues) {
    const orderDate = String(order.orderDate);
    const total = Number(order.totalAmount);
    const ageDays = Math.round(
      (Date.parse(`${todayIsoForPayment}T00:00:00Z`) - Date.parse(`${orderDate}T00:00:00Z`)) /
        86_400_000,
    );

    // Đơn quá 60 ngày coi như đã thu đủ; càng gần hiện tại càng nhiều khả năng còn nợ.
    const settled = ageDays > 60 ? rnd() > 0.05 : rnd() > 0.45;
    const partial = !settled && rnd() > 0.4;

    if (!settled && !partial) continue;

    const paidRatio = settled ? 1 : 0.3 + rnd() * 0.4;
    const amount = Math.round((total * paidRatio) / 1000) * 1000;
    if (amount <= 0) continue;

    const paidOn = new Date(
      Date.parse(`${orderDate}T00:00:00Z`) + between(1, Math.max(1, Math.min(ageDays, 45))) * 86_400_000,
    )
      .toISOString()
      .slice(0, 10);

    paymentValues.push({
      tenantId,
      orderId: order.id!,
      paidOn: paidOn > todayIsoForPayment ? todayIsoForPayment : paidOn,
      amount: Math.min(amount, total),
      method: pick(["Chuyển khoản", "Tiền mặt"]),
      recordedBy: order.salesUserId ?? null,
    });
  }

  await insertInChunks(t.orderPayments, paymentValues);

  const totalRevenue = orderValues.reduce((sum, o) => sum + Number(o.totalAmount), 0);
  const totalPaid = paymentValues.reduce((sum, p) => sum + Number(p.amount), 0);

  console.log(
    `  ✓ ${orderValues.length} đơn hàng · ${lineValues.length} dòng hàng · ` +
      `${paymentValues.length} phiếu thu (còn nợ ${Math.round((totalRevenue - totalPaid) / 1_000_000)} tr)`,
  );

  /* --- Chỉ tiêu kế hoạch ---------------------------------------------------
     Chỉ tiêu được đặt bám theo doanh thu thực tế đã sinh ra (dao động 90–110%),
     nên khi demo, cột "% kế hoạch" cho ra những con số đáng tin — có người vượt,
     có người hụt — thay vì tất cả cùng vượt 300% vì chỉ tiêu đặt tuỳ ý.
     Tháng hiện tại chưa kết thúc nên lấy tháng liền trước làm gốc. */
  const revenueByMonthUser = new Map<string, number>();
  for (const order of orderValues) {
    const month = String(order.orderDate).slice(0, 7);
    const userKey = `${month}|${order.salesUserId}`;
    revenueByMonthUser.set(userKey, (revenueByMonthUser.get(userKey) ?? 0) + Number(order.totalAmount));
  }

  const roundToMillion = (value: number) => Math.round(value / 1_000_000) * 1_000_000;

  const targetValues: (typeof t.targets.$inferInsert)[] = [];
  const lastTwoClosedMonths = [shiftMonth(currentMonth, 2), shiftMonth(currentMonth, 1)]; // shiftMonth: số dương = lùi về quá khứ
  for (const month of months.slice(-4)) {
    const isCurrentMonth = month === currentMonth;
    const baseMonth = isCurrentMonth ? shiftMonth(currentMonth, 1) : month;

    // Chỉ tiêu công ty = tổng chỉ tiêu các NVKD (điền ngay sau vòng lặp NVKD bên dưới) để mọi tầng — công ty, khu vực, phòng ban,
    // nhân viên — cộng khớp nhau trong báo cáo, thay vì mỗi tầng một con số ngẫu nhiên.
    const companyRow: typeof targetValues[number] = {
      tenantId,
      periodType: "MONTH",
      periodKey: month,
      scopeType: "COMPANY",
      scopeId: null,
      revenueTarget: 0,
      orderTarget: between(70, 95),
      newCustomerTarget: 8,
    };
    targetValues.push(companyRow);

    const userTarget = new Map<string, number>();
    for (const sales of salesUsers) {
      const userBase = revenueByMonthUser.get(`${baseMonth}|${sales.id}`) ?? 300_000_000;
      // Một NVKD cố ý đặt chỉ tiêu quá cao hai tháng liền trước tháng hiện tại → có sẵn cảnh báo "không đạt chỉ tiêu" thật để demo.
      const revenueTarget = roundToMillion(userBase * (sales.id === salesUsers[1].id && lastTwoClosedMonths.includes(month) ? 2.2 : 0.9 + rnd() * 0.25));
      userTarget.set(sales.id, revenueTarget);
      targetValues.push({
        tenantId,
        periodType: "MONTH",
        periodKey: month,
        scopeType: "USER",
        scopeId: sales.id,
        revenueTarget,
        orderTarget: between(12, 20),
        newCustomerTarget: 2,
      });
    }
    companyRow.revenueTarget = [...userTarget.values()].reduce((sum, v) => sum + v, 0);

    // Chỉ tiêu theo khu vực (đủ 3 miền) và theo team/phòng ban = tổng chỉ tiêu các NVKD trong vùng — để báo cáo
    // "thực hiện so với kế hoạch" theo khu vực / phòng ban có số để so thay vì toàn dấu "—".
    for (const region of regions) {
      const regionTarget = salesUsers
        .filter((sales) => provinces.find((p) => p.id === sales.provinceId)?.regionCode === region.code)
        .reduce((sum, sales) => sum + (userTarget.get(sales.id) ?? 0), 0);
      const orderTarget = between(35, 55);
      targetValues.push({
        tenantId,
        periodType: "MONTH",
        periodKey: month,
        scopeType: "TERRITORY",
        scopeId: region.id,
        revenueTarget: regionTarget,
        orderTarget,
        newCustomerTarget: 4,
      });
      const teamId = teamByRegion.get(region.code);
      if (teamId) {
        targetValues.push({
          tenantId,
          periodType: "MONTH",
          periodKey: month,
          scopeType: "DEPARTMENT",
          scopeId: teamId,
          revenueTarget: regionTarget,
          orderTarget,
          newCustomerTarget: 4,
        });
      }
    }
  }
  await db.insert(t.targets).values(targetValues);

  /* --- Lý do đại lý không lấy hàng trong tháng hiện tại -------------------- */
  const boughtThisMonth = new Set(
    orderValues
      .filter((order) => String(order.orderDate).startsWith(currentMonth))
      .map((order) => order.customerId),
  );

  const inactiveThisMonth = dealers.filter((dealer) => !boughtThisMonth.has(dealer.id));

  if (inactiveThisMonth.length > 0) {
    // Ghi lý do cho khoảng hai phần ba số đại lý im lặng — phần còn lại để trống
    // đúng như thực tế: luôn có vài trường hợp NVKD chưa kịp ghi.
    await db.insert(t.dealerInactiveNotes).values(
      inactiveThisMonth
        .filter(() => rnd() > 0.33)
        .map((dealer) => ({
          tenantId,
          customerId: dealer.id,
          periodKey: currentMonth,
          reason: pick([
            "Khách còn tồn kho nhiều, hẹn lấy hàng đầu tháng sau",
            "Đang so sánh giá với nhà cung cấp khác",
            "Cơ sở tạm nghỉ sửa chữa, dự kiến mở lại cuối tháng",
            "Chủ cơ sở đi nước ngoài, chưa duyệt đơn",
            "Còn công nợ kỳ trước, chờ thanh toán xong mới lấy tiếp",
            "Đổi người phụ trách mua hàng, đang bàn giao",
          ]),
          recordedBy: dealer.ownerId,
        })),
    );
  }

  /* --- Chỉ tiêu giao riêng cho vài đại lý lớn ----------------------------- */
  const keyDealers = dealers.filter((dealer) => dealer.tierBias > 0.85).slice(0, 4);
  if (keyDealers.length > 0) {
    await db.insert(t.targets).values(
      keyDealers.map((dealer) => ({
        tenantId,
        periodType: "MONTH" as const,
        periodKey: currentMonth,
        scopeType: "CUSTOMER" as const,
        scopeId: dealer.id,
        revenueTarget: between(80, 250) * 1_000_000,
        orderTarget: between(2, 5),
        newCustomerTarget: 0,
      })),
    );
  }

  /* --- Kế hoạch tuần, báo cáo ngày, check-in ------------------------------ */
  const thisMonday = weekStart(todayIso);
  const weeks = [shiftDays(thisMonday, -7), thisMonday];

  for (const sales of salesUsers) {
    const myCustomers = customers.filter((c) => c.ownerId === sales.id);

    for (const monday of weeks) {
      // Tuần trước đã chốt tổng kết, tuần hiện tại chưa — để màn hình demo có cả
      // hai trạng thái "đã chốt" và "chưa chốt".
      const isPastWeek = monday < thisMonday;

      const [plan] = await db
        .insert(t.weeklyPlans)
        .values({
          tenantId,
          userId: sales.id,
          weekStart: monday,
          objective: "Phủ tuyến trọng điểm, đẩy SP trọng tâm và mở mới 2 khách hàng",
          summary: isPastWeek
            ? "Phủ được phần lớn tuyến đã đăng ký. Nhóm Meso bán tốt, filler chậm hơn dự kiến. Hai điểm chưa gặp được chủ cơ sở."
            : null,
          nextWeekObjective: isPastWeek
            ? "Quay lại các điểm chưa gặp được chủ, đẩy SP trọng tâm cho nhóm phòng khám."
            : null,
          submittedAt: new Date(),
        })
        .returning();

      for (const [dayIndex, day] of weekDays(monday).slice(0, 6).entries()) {
        const visitCount = between(2, 4);
        for (let v = 0; v < visitCount; v++) {
          const customer = pick(myCustomers);
          // Các buổi sáng của chính ngày hôm nay cũng coi như đã đi, để màn hình
          // demo của tuần hiện tại không hiện 0% hoàn thành.
          const isPast = day < todayIso || (day === todayIso && v < 2);
          const status = isPast ? (rnd() > 0.18 ? "DONE" : "MISSED") : "PLANNED";

          const [item] = await db
            .insert(t.planItems)
            .values({
              tenantId,
              planId: plan.id,
              planDate: day,
              content: pick([
                "Thăm và chăm sóc đại lý, kiểm tra tồn kho",
                "Giới thiệu sản phẩm mới, lấy đơn",
                "Đào tạo kỹ thuật sử dụng sản phẩm",
                "Thu hồi công nợ và chốt đơn tháng",
                "Khảo sát khách hàng tiềm năng",
              ]),
              objective: pick(["Chốt đơn tối thiểu 15tr", "Mở mới 1 KH", "Tăng 2 SKU", "Thu công nợ"]),
              timeSlot: `${8 + v * 2}:00-${10 + v * 2}:00`,
              customerId: customer.id,
              territoryId: sales.provinceId,
              status: status as "PLANNED" | "DONE" | "MISSED",
              missedReason:
                status === "MISSED"
                  ? pick([
                      "Chủ cơ sở đi vắng, hẹn lại tuần sau",
                      "Khách bận tiếp đoàn, không sắp xếp được",
                      "Đường ngập, không tới được điểm bán",
                      "Khách đổi lịch sang đầu tuần sau",
                    ])
                  : null,
              sortOrder: v,
            })
            .returning();

          if (status === "DONE") {
            const base = PROVINCE_COORDS[sales.provinceName] ?? { lat: 21.0278, lng: 105.8342 };
            // Đa số check-in trong bán kính; một số ít lệch xa để minh hoạ cờ "ngoài phạm vi".
            const drift = rnd() > 0.9 ? 0.02 : 0.0008;
            const distance = Math.round(drift * 111_000);
            await db.insert(t.checkIns).values({
              tenantId,
              userId: sales.id,
              customerId: customer.id,
              planItemId: item.id,
              checkedAt: new Date(`${day}T0${between(1, 9)}:${between(10, 59)}:00Z`),
              lat: base.lat + drift,
              lng: base.lng + drift,
              accuracyMeters: between(5, 30),
              distanceMeters: distance,
              outOfRange: distance > Number(process.env.CHECKIN_RADIUS_METERS ?? 300),
              note: "Đã gặp chủ cơ sở, trao đổi chương trình tháng",
            });
          }
        }

        if (day < todayIso) {
          await db.insert(t.dailyReports).values({
            tenantId,
            userId: sales.id,
            reportDate: day,
            completionPct: between(60, 100),
            routeCompliant: rnd() > 0.15,
            content: "Đi tuyến theo kế hoạch, gặp và làm việc với các cơ sở trong khu vực.",
            results: `Chốt ${between(1, 3)} đơn, giới thiệu sản phẩm trọng tâm cho ${between(2, 4)} khách.`,
            proposals: pick([
              "Đề xuất tăng chiết khấu cho đơn trên 50tr",
              "Cần thêm tài liệu đào tạo sản phẩm mới",
              "Đề nghị hỗ trợ hàng mẫu cho khách tiềm năng",
              "",
            ]),
            pending: dayIndex % 3 === 0 ? "Còn 1 khách chưa gặp được chủ, hẹn lại tuần sau." : "",
            nextSteps: "Tiếp tục phủ tuyến còn lại và bám đơn đã chào.",
            submittedAt: new Date(`${day}T11:00:00Z`),
          });
        }
      }
    }
  }

  /* --- Chuyên đề / lớp học ------------------------------------------------ */
  await db.insert(t.events).values(
    months.slice(-4).map((month, i) => ({
      tenantId,
      name: `Chuyên đề ứng dụng Mesotherapy trong điều trị — đợt ${i + 1}`,
      eventDate: `${month}-${String(between(10, 25)).padStart(2, "0")}`,
      kind: (["Chuyên đề", "Lớp học", "Hội thảo", "Lớp sản phẩm"] as const)[i % 4],
      territoryId: pick(provinces).id,
      ownerId: pick(salesUsers).id,
      invitedCount: between(40, 90),
      attendedCount: between(25, 60),
      resultOrderCount: between(5, 18),
      resultRevenue: between(80, 350) * 1_000_000,
      note: "Kết hợp giới thiệu sản phẩm trọng tâm quý",
    })),
  );

  /* --- Nhân viên chuyên môn (phụ trách đào tạo) ---------------------------
     Quyền hạn như NVKD (role SALES), chỉ khác chức vụ hiển thị và KPI — không
     bán hàng trực tiếp nên không có trong `salesUsers`/không được giao khách,
     chỉ có các lớp đã dạy (bảng `events`) để lên hồ sơ nhân viên. Xem xác nhận
     của khách trong trao-doi-khach/cau-hoi-bo-sung-2026-09-16.md. */
  const hanoiProvince = provinces.find((p) => p.name === "Hà Nội")!;
  const specialistData = [
    { email: "chuyenmon.hanoi@minhanh.vn", name: "Trịnh Bảo Châu", province: "Hà Nội" },
    { email: "chuyenmon.danang@minhanh.vn", name: "Đinh Thị Ngọc Ánh", province: "Đà Nẵng" },
    { email: "chuyenmon.hcm1@minhanh.vn", name: "Hoàng Minh Tuấn", province: "TP. Hồ Chí Minh" },
    { email: "chuyenmon.hcm2@minhanh.vn", name: "Lâm Thảo Vy", province: "Bình Dương" },
  ];
  const specialists: (typeof t.users.$inferSelect)[] = [];
  for (const sp of specialistData) {
    const province = provinces.find((p) => p.name === sp.province)!;
    const [user] = await db
      .insert(t.users)
      .values({
        tenantId,
        email: sp.email,
        passwordHash,
        fullName: sp.name,
        phone: `09${between(10000000, 99999999)}`,
        role: "SALES",
        jobTitle: "Nhân viên chuyên môn",
        territoryId: province.id,
        departmentId: specialistTeamByRegion.get(province.regionCode) ?? deptSpecialist.id,
      })
      .returning();
    specialists.push(user);
  }
  const specialist = specialists[0];

  // Trưởng phòng chuyên môn (thấy cả phòng) và quản lý chuyên môn khu vực Miền Nam (chỉ thấy team Miền Nam).
  const [specialistHead] = await db
    .insert(t.users)
    .values({
      tenantId,
      email: "truongphong.chuyenmon@minhanh.vn",
      passwordHash,
      fullName: "Lương Thanh Hằng",
      phone: `09${between(10000000, 99999999)}`,
      role: "MANAGER",
      jobTitle: "Trưởng phòng chuyên môn",
      territoryId: hanoiProvince.id,
      departmentId: deptSpecialist.id,
    })
    .returning();
  await db.insert(t.userTerritories).values(regions.map((r) => ({ tenantId, userId: specialistHead.id, territoryId: r.id })));
  const southRegion = regions.find((r) => r.code === "MN")!;
  const [specialistManagerSouth] = await db
    .insert(t.users)
    .values({
      tenantId,
      email: "quanly.chuyenmon.nam@minhanh.vn",
      passwordHash,
      fullName: "Tạ Quang Vinh",
      phone: `09${between(10000000, 99999999)}`,
      role: "MANAGER",
      jobTitle: "Quản lý chuyên môn khu vực Miền Nam",
      territoryId: southRegion.id,
      departmentId: specialistTeamByRegion.get("MN") ?? deptSpecialist.id,
    })
    .returning();
  await db.insert(t.userTerritories).values({ tenantId, userId: specialistManagerSouth.id, territoryId: southRegion.id });
  // Trưởng bộ phận: dùng cho chuỗi duyệt phiếu đề xuất (mở lớp chuyển giao → trưởng phòng chuyên môn).
  await db.update(t.departments).set({ headUserId: specialistHead.id }).where(eq(t.departments.id, deptSpecialist.id));
  await db.update(t.departments).set({ headUserId: specialistManagerSouth.id }).where(eq(t.departments.id, specialistTeamByRegion.get("MN") ?? deptSpecialist.id));

  await db.insert(t.events).values([
    {
      tenantId,
      name: "Lớp đào tạo sản phẩm Serum Vitamin C",
      eventDate: shiftDays(todayIso, -12),
      kind: "Lớp sản phẩm",
      territoryId: hanoiProvince.id,
      ownerId: specialist.id,
      invitedCount: 14,
      attendedCount: 12,
      resultOrderCount: 2,
      resultRevenue: 38_500_000,
      note: "Diễn ra tại Thẩm mỹ viện Bảo Ngọc, bác sĩ chăm chú lắng nghe và hỏi nhiều câu hỏi kỹ thuật — khả năng lên đơn cao trong tuần tới.",
    },
    {
      tenantId,
      name: "Hướng dẫn quy trình tiêm Filler an toàn",
      eventDate: shiftDays(todayIso, -26),
      kind: "Lớp quy trình",
      territoryId: hanoiProvince.id,
      ownerId: specialist.id,
      invitedCount: 8,
      attendedCount: 8,
      resultOrderCount: 0,
      resultRevenue: 0,
      note: "Tại Clinic Kim Anh — nhân sự mới, thao tác còn chưa tự tin, cần thêm 1 buổi thực hành nữa mới chốt được đơn.",
    },
    {
      tenantId,
      name: "Chuyển giao lô Botox mới nhận",
      eventDate: shiftDays(todayIso, -40),
      kind: "Chuyển giao tại cơ sở khách",
      territoryId: hanoiProvince.id,
      ownerId: specialist.id,
      invitedCount: 5,
      attendedCount: 3,
      resultOrderCount: 0,
      resultRevenue: 0,
      note: "Tại TMV Hồng Ngọc, chủ cơ sở khá hời hợt do bận tiếp khách trong giờ hẹn — cần quay lại đào tạo lại vào đợt sau.",
    },
    {
      tenantId,
      name: "Giới thiệu công nghệ Meso tại hội nghị da liễu khu vực",
      eventDate: shiftDays(todayIso, -55),
      kind: "Báo cáo Hội nghị/Bệnh viện",
      territoryId: hanoiProvince.id,
      ownerId: specialist.id,
      invitedCount: 60,
      attendedCount: 41,
      resultOrderCount: 3,
      resultRevenue: 156_000_000,
      note: "Phản hồi tích cực từ nhiều phòng khám tham dự, đã có 2 đơn vị hẹn lịch demo sản phẩm sau hội nghị.",
    },
    {
      tenantId,
      name: "Lớp quy trình chăm sóc da sau điều trị",
      eventDate: shiftDays(todayIso, -70),
      kind: "Lớp quy trình",
      territoryId: hanoiProvince.id,
      ownerId: specialist.id,
      invitedCount: 10,
      attendedCount: 9,
      resultOrderCount: 1,
      resultRevenue: 22_400_000,
      note: "Tại Spa Diệu Linh, nhân viên tiếp thu nhanh, phòng khám đặt thêm đơn ngay sau buổi học.",
    },
  ]);

  // Lớp của các nhân viên chuyên môn miền Trung / miền Nam — để quản lý chuyên môn khu vực có số liệu trong kỳ.
  const danang = provinces.find((p) => p.name === "Đà Nẵng")!;
  const hcm = provinces.find((p) => p.name === "TP. Hồ Chí Minh")!;
  const binhDuong = provinces.find((p) => p.name === "Bình Dương")!;
  await db.insert(t.events).values([
    { tenantId, name: "Lớp quy trình laser trẻ hoá da", eventDate: shiftDays(todayIso, -5), kind: "Lớp quy trình", territoryId: danang.id, ownerId: specialists[1].id, invitedCount: 12, attendedCount: 10, resultOrderCount: 2, resultRevenue: 41_000_000, note: "Tại Clinic Biển Xanh, học viên thực hành tốt, hẹn lấy máy demo tuần sau." },
    { tenantId, name: "Chuyển giao dòng Peel mới cho đại lý miền Trung", eventDate: shiftDays(todayIso, -33), kind: "Chuyển giao tại cơ sở khách", territoryId: danang.id, ownerId: specialists[1].id, invitedCount: 6, attendedCount: 5, resultOrderCount: 1, resultRevenue: 12_800_000, note: "Chủ cơ sở đề nghị bổ sung tài liệu tiếng Việt." },
    { tenantId, name: "Lớp đào tạo sản phẩm Serum Peptide", eventDate: shiftDays(todayIso, -3), kind: "Lớp sản phẩm", territoryId: hcm.id, ownerId: specialists[2].id, invitedCount: 20, attendedCount: 17, resultOrderCount: 4, resultRevenue: 96_500_000, note: "Lớp đông, nhiều câu hỏi về liều lượng; 4 đại lý chốt đơn ngay trong tuần." },
    { tenantId, name: "Hướng dẫn quy trình chăm sóc sau Meso", eventDate: shiftDays(todayIso, -20), kind: "Lớp quy trình", territoryId: hcm.id, ownerId: specialists[2].id, invitedCount: 9, attendedCount: 6, resultOrderCount: 0, resultRevenue: 0, note: "Vắng 3 người do trùng lịch hội nghị — cần tổ chức lại buổi bù." },
    { tenantId, name: "Báo cáo chuyên đề tại hội nghị thẩm mỹ Bình Dương", eventDate: shiftDays(todayIso, -9), kind: "Báo cáo Hội nghị/Bệnh viện", territoryId: binhDuong.id, ownerId: specialists[3].id, invitedCount: 45, attendedCount: 33, resultOrderCount: 2, resultRevenue: 58_000_000, note: "Nhiều phòng khám mới quan tâm, đã xin danh thiếp và hẹn demo." },
  ]);

  // Thêm lớp rải đều các tuần gần đây, đủ các loại lớp — để báo cáo chuyển giao theo ngày / tuần / tháng / quý / năm có số liệu mà xem.
  const extraKinds = ["Lớp sản phẩm", "Lớp quy trình", "Chuyển giao tại cơ sở khách", "Báo cáo Hội nghị/Bệnh viện"] as const;
  const extraDays = [0, -1, -2, -7, -8, -14, -16, -21, -30, -45, -60, -80, -110, -150];
  const extraProvince = [hanoiProvince, danang, hcm, binhDuong];
  await db.insert(t.events).values(
    extraDays.map((offset, i) => {
      const owner = specialists[i % specialists.length];
      const invited = between(8, 40);
      return {
        tenantId,
        name: `${extraKinds[i % extraKinds.length]} — ${["Serum Vitamin C", "Peel da chuyên sâu", "Quy trình Meso an toàn", "Laser trẻ hoá", "Botox nâng cơ", "Chăm sóc sau điều trị"][i % 6]}`,
        eventDate: shiftDays(todayIso, offset),
        kind: extraKinds[i % extraKinds.length],
        territoryId: extraProvince[i % extraProvince.length].id,
        ownerId: owner.id,
        invitedCount: invited,
        attendedCount: Math.max(1, Math.round(invited * (0.55 + rnd() * 0.4))),
        resultOrderCount: between(0, 4),
        resultRevenue: between(0, 90) * 1_000_000,
        note: pick(["Học viên tiếp thu tốt, hẹn lịch thực hành.", "Cần bổ sung tài liệu tiếng Việt.", "Nhiều câu hỏi về liều lượng, sẽ gửi phác đồ.", "Phản hồi tích cực, đề nghị mở thêm lớp."]),
      };
    }),
  );

  /* --- Sự kiện mời khách có chỉ tiêu ---------------------------------------
     Khác với `events` (chuyên đề/lớp học thường, không chỉ tiêu) — loại này
     hiếm (khách xác nhận trung bình 1 tháng 1 sự kiện toàn công ty), có chỉ
     tiêu TGĐ giao theo khu vực rồi GĐKV giao lại cho từng NVKD, và theo dõi
     CME/chứng nhận tham dự trên từng khách mời. Xem xác nhận của khách trong
     trao-doi-khach/cau-hoi-bo-sung-2026-09-16.md. */
  const [inviteEvent] = await db
    .insert(t.inviteEvents)
    .values({
      tenantId,
      name: "Hội nghị da liễu & thẩm mỹ toàn quốc Quý 4/2026",
      eventDate: shiftDays(todayIso, 20),
      note: "Ba khu vực cùng tham gia mời khách, tổ chức chung 1 địa điểm tại Hà Nội.",
      createdBy: director.id,
    })
    .returning();

  const regionQuota = [
    { code: "MB", quota: 40 },
    { code: "MT", quota: 25 },
    { code: "MN", quota: 35 },
  ];
  for (const rq of regionQuota) {
    const region = regions.find((r) => r.code === rq.code)!;
    await db.insert(t.inviteEventTerritoryQuotas).values({
      tenantId,
      eventId: inviteEvent.id,
      territoryId: region.id,
      quota: rq.quota,
    });
  }

  const staffQuota = [
    { email: "nv.hanoi@minhanh.vn", quota: 20 },
    { email: "nv.haiphong@minhanh.vn", quota: 20 },
    { email: "nv.danang@minhanh.vn", quota: 25 },
    { email: "nv.hcm1@minhanh.vn", quota: 20 },
    { email: "nv.hcm2@minhanh.vn", quota: 15 },
  ];
  const regionIdByCode = new Map(regions.map((r) => [r.code, r.id]));
  const provinceRegionCode = new Map(provinces.map((p) => [p.id, p.regionCode]));
  for (const sq of staffQuota) {
    const sales = salesUsers.find((s) => s.email === sq.email)!;
    const regionId = regionIdByCode.get(provinceRegionCode.get(sales.provinceId)!)!;
    await db.insert(t.inviteEventStaffQuotas).values({
      tenantId,
      eventId: inviteEvent.id,
      territoryId: regionId,
      userId: sales.id,
      quota: sq.quota,
    });
  }

  // Tầng phòng ban / team: chỉ tiêu mỗi team = tổng chỉ tiêu nhân viên trong team + dự phòng 10, để demo chuỗi giao Miền → Team → Nhân viên.
  const staffDeptTotals = new Map<string, number>();
  for (const sq of staffQuota) {
    const sales = salesUsers.find((s) => s.email === sq.email)!;
    const [row] = await db.select({ departmentId: t.users.departmentId }).from(t.users).where(eq(t.users.id, sales.id)).limit(1);
    if (row?.departmentId) staffDeptTotals.set(row.departmentId, (staffDeptTotals.get(row.departmentId) ?? 0) + sq.quota);
  }
  for (const [departmentId, total] of staffDeptTotals) {
    await db.insert(t.inviteEventDeptQuotas).values({ tenantId, eventId: inviteEvent.id, departmentId, quota: total + 10 });
  }

  const guestSeed = [
    { email: "nv.hanoi@minhanh.vn", name: "BS. Lê Thị Hạnh", type: "Bác sĩ", cme: true, cert: false, attended: null },
    { email: "nv.hanoi@minhanh.vn", name: "Chị Ngọc — Chủ Spa Diệu Linh", type: "Chủ spa", cme: false, cert: true, attended: null },
    { email: "nv.hanoi@minhanh.vn", name: "BS. Trần Văn Nam", type: "Bác sĩ", cme: true, cert: true, attended: null },
    { email: "nv.haiphong@minhanh.vn", name: "Anh Quang — TMV Hoàng Gia", type: "Chủ TMV", cme: false, cert: false, attended: null },
    { email: "nv.haiphong@minhanh.vn", name: "BS. Đỗ Thị Lan", type: "Bác sĩ", cme: true, cert: false, attended: null },
    { email: "nv.danang@minhanh.vn", name: "BS. Phan Anh Tuấn", type: "Bác sĩ", cme: true, cert: false, attended: true },
    { email: "nv.danang@minhanh.vn", name: "Chị Thuý — Spa Sài Gòn", type: "Chủ spa", cme: false, cert: false, attended: true },
    { email: "nv.danang@minhanh.vn", name: "BS. Ngô Thị Bích", type: "Bác sĩ", cme: true, cert: true, attended: false },
    { email: "nv.hcm1@minhanh.vn", name: "BS. Vũ Đình Khoa", type: "Bác sĩ", cme: true, cert: false, attended: null },
    { email: "nv.hcm1@minhanh.vn", name: "Chị Hoa — Phòng khám Quỳnh Anh", type: "Chủ phòng khám", cme: false, cert: true, attended: null },
    { email: "nv.hcm2@minhanh.vn", name: "BS. Nguyễn Thành Long", type: "Bác sĩ", cme: true, cert: true, attended: null },
  ];
  for (const g of guestSeed) {
    const sales = salesUsers.find((s) => s.email === g.email)!;
    const regionId = regionIdByCode.get(provinceRegionCode.get(sales.provinceId)!)!;
    await db.insert(t.inviteEventGuests).values({
      tenantId,
      eventId: inviteEvent.id,
      territoryId: regionId,
      invitedBy: sales.id,
      guestName: g.name,
      guestType: g.type,
      invitedDate: shiftDays(todayIso, -between(1, 10)),
      attended: g.attended,
      wantsCme: g.cme,
      wantsCertificate: g.cert,
    });
  }

  /* --- Khoá học chuyển giao (chương trình chuyển giao) --------------------
     Đại lý phải học quy trình sử dụng trước khi được lấy một số sản phẩm —
     xem xác nhận của khách trong trao-doi-khach/cau-hoi-bo-sung-2026-09-16.md
     câu 1. Chỉ là danh mục + nhật ký ai học khoá nào, không có logic miễn phí
     tự động theo giá trị đơn hàng (khách xác nhận đây là quyết định linh hoạt
     của NVKD, không phải luật cứng của hệ thống). */
  const courseRows = await db
    .insert(t.trainingCourses)
    .values([
      {
        tenantId,
        name: "Kỹ thuật tiêm Filler cơ bản",
        description: "Bắt buộc trước khi lấy các sản phẩm nhóm Filler (FL001–FL003).",
        schedule: "1 buổi · 4 giờ",
        conditions: "Miễn phí khi đơn từ 5 triệu hoặc NVKD hỗ trợ; nếu không thì thu phí",
        format: "Trực tiếp tại cơ sở khách",
        hasCertificate: true,
      },
      {
        tenantId,
        name: "Kỹ thuật tiêm Filler nâng cao & Botox",
        description: "Bắt buộc trước khi lấy Filler dòng cao cấp và Botox (FL004–FL005).",
        schedule: "2 buổi · 6 giờ",
        conditions: "Đã hoàn thành khoá Filler cơ bản; thu phí trừ khi đơn từ 10 triệu",
        format: "Trực tiếp tại trung tâm đào tạo",
        hasCertificate: true,
      },
      {
        tenantId,
        name: "Chăm sóc da & Mesotherapy",
        description: "Bắt buộc trước khi lấy các sản phẩm nhóm Meso (MS001–MS005).",
        schedule: "1 buổi · 3 giờ",
        conditions: "Miễn phí khi đơn từ 5 triệu",
        format: "Trực tuyến hoặc trực tiếp",
        hasCertificate: true,
      },
      {
        tenantId,
        name: "Vận hành thiết bị công nghệ cao (Laser, điện di, RF)",
        description: "Bắt buộc trước khi lấy thiết bị nhóm DV001–DV005.",
      },
      {
        tenantId,
        name: "Chăm sóc da cơ bản tại nhà",
        description: "Khoá phổ thông cho dòng sản phẩm chăm sóc da SP001–SP010, không bắt buộc.",
        schedule: "2 giờ",
        conditions: "Miễn phí",
        format: "Trực tuyến",
        hasCertificate: false,
      },
    ])
    .returning();

  const courseCompletionValues: (typeof t.customerCourseCompletions.$inferInsert)[] = [];
  for (const dealer of dealers) {
    // Không phải đại lý nào cũng đã hoàn thành khoá học — đúng thực tế là còn
    // một số đại lý mua hàng nhóm phổ thông (SP), chưa cần học khoá bắt buộc.
    if (rnd() > 0.7) continue;
    const courseCount = dealer.tierBias > 0.6 ? between(1, 3) : 1;
    const chosenCourses = shuffle([...courseRows]).slice(0, courseCount);
    for (const course of chosenCourses) {
      const daysAgo = between(15, 300);
      // Đa số học miễn phí (thường do đủ điều kiện đơn hàng, hoặc NVKD bù học phí),
      // một phần nhỏ đóng phí — đúng như khách mô tả, không phải luật cứng.
      const isFree = rnd() > 0.2;
      courseCompletionValues.push({
        tenantId,
        customerId: dealer.id,
        courseId: course.id,
        completedDate: shiftDays(todayIso, -daysAgo),
        isFree,
        note: isFree ? "Đủ điều kiện miễn phí theo đơn hàng đã lấy" : null,
        recordedBy: dealer.ownerId,
      });
    }
  }
  await insertInChunks(t.customerCourseCompletions, courseCompletionValues);
  console.log(`  ✓ ${courseRows.length} khoá học · ${courseCompletionValues.length} lượt hoàn thành`);

  /* --- Thông báo trang chủ ------------------------------------------------ */
  await db.insert(t.announcements).values([
    {
      tenantId,
      title: "Chào mừng tới CRM Mediworld",
      body: "Toàn bộ đơn hàng, khách hàng và báo cáo bán hàng nay quản lý tập trung tại một nơi.",
      scopeType: "COMPANY",
      isPinned: true,
      createdBy: director.id,
    },
    {
      tenantId,
      title: "Hội nghị khách hàng Mediworld 2026",
      body: "Toàn bộ NVKD sắp xếp mời khách; xem chỉ tiêu mời khách theo khu vực tại mục Mời khách sự kiện.",
      scopeType: "COMPANY",
      eventDate: shiftDays(todayIso, 12),
      expiresAt: shiftDays(todayIso, 12),
      createdBy: director.id,
    },
    {
      tenantId,
      title: "Cập nhật chính sách chiết khấu Quý 4/2026",
      body: "Xem chi tiết bậc chiết khấu mới theo từng nhóm sản phẩm tại mục Danh mục.",
      scopeType: "DEPARTMENT",
      scopeId: deptSales.id,
      createdBy: director.id,
    },
    {
      tenantId,
      title: "Nhắc lịch chốt báo cáo tuần",
      body: "NVKD hoàn tất báo cáo kết quả tuần trước 18:00 thứ Bảy hàng tuần.",
      scopeType: "TERRITORY",
      scopeId: regions[0].id,
      createdBy: director.id,
    },
  ]);

  /* --- Hồ sơ cá nhân: ngày sinh, giới tính ------------------------------ */
  const allUsers = await db.select({ id: t.users.id, fullName: t.users.fullName }).from(t.users).where(eq(t.users.tenantId, tenantId));
  for (const person of allUsers) {
    const year = between(1978, 1999);
    await db
      .update(t.users)
      .set({
        birthDate: `${year}-${String(between(1, 12)).padStart(2, "0")}-${String(between(1, 28)).padStart(2, "0")}`,
        gender: /Thị|Thu|Mỹ|Hồng|Lan|Châu/.test(person.fullName) ? "F" : "M",
      })
      .where(eq(t.users.id, person.id));
  }

  /* --- Việc cần làm (danh sách công việc) --------------------------------- */
  await db.insert(t.tasks).values([
    {
      tenantId,
      assigneeId: salesUsers[0].id,
      createdBy: managers[0].id,
      title: "Gọi 5 đại lý chưa lấy hàng tháng này để hỏi lý do",
      startDate: todayIso,
      dueDate: shiftDays(todayIso, 4),
      status: "NOT_STARTED",
    },
    {
      tenantId,
      assigneeId: salesUsers[0].id,
      createdBy: salesUsers[0].id,
      title: "Chuẩn bị hồ sơ mở code cho khách 'Đang'",
      startDate: shiftDays(todayIso, -3),
      dueDate: shiftDays(todayIso, -1),
      status: "WAITING",
    },
    {
      tenantId,
      assigneeId: salesUsers[0].id,
      createdBy: salesUsers[0].id,
      title: "Gửi bảng giá quý 4 cho nhóm khách spa",
      startDate: shiftDays(todayIso, -6),
      dueDate: shiftDays(todayIso, -2),
      status: "DONE",
      completedAt: new Date(),
    },
  ]);

  /* --- Chạy các bộ máy tự động ------------------------------------------- */
  console.log("\nĐang chạy các bộ máy tự động…");
  for (const month of months.slice(-6)) {
    await recomputeClassifications(tenantId, month);
    await recomputeKpi(tenantId, month);
  }
  // Quét cảnh báo trên tháng đã đóng sổ — xem chú thích trong runAlertScan.
  const alertCount = await runAlertScan(tenantId, shiftMonth(currentMonth, 1));
  const forecast = [];
  for (let i = 1; i <= FORECAST_HORIZON_MONTHS; i++) forecast.push(...(await recomputeForecast(tenantId, shiftMonth(currentMonth, -i), shiftMonth(currentMonth, 1))));

  console.log(`  ✓ Phân hạng & KPI 6 tháng gần nhất`);
  console.log(`  ✓ ${alertCount} cảnh báo · ${forecast.length} dòng dự báo`);


  /* --- Phiếu đề xuất mẫu (duyệt lần lượt: Trưởng bộ phận → TGĐ) -------------- */
  const salesByEmail = (email: string) => salesUsers.find((u) => u.email === email)!;
  const managerByEmail = (email: string) => managers.find((u) => u.email === email)!;
  const [filler] = await db.select({ id: t.trainingCourses.id, name: t.trainingCourses.name }).from(t.trainingCourses).where(eq(t.trainingCourses.tenantId, tenantId)).limit(1);
  type SeedProposal = {
    by: { id: string };
    type: "TRAINING" | "PROMOTION" | "GIFT" | "OTHER";
    kind: string;
    title: string;
    content: string;
    values: Record<string, string>;
    daysAgo: number;
    steps: { user: { id: string }; label: string; decision: "PENDING" | "APPROVED" | "REJECTED"; note?: string }[];
  };
  const seedProposals: SeedProposal[] = [
    {
      by: salesByEmail("nv.hcm1@minhanh.vn"),
      type: "TRAINING",
      kind: "Đề xuất mở lớp chuyển giao",
      title: "Mở lớp Filler cơ bản cho khách khu vực TP.HCM",
      content: "Có 8 khách mới chốt Filler nhưng chưa học quy trình. Đề xuất mở 1 lớp tại showroom để đủ điều kiện lấy hàng nhóm Filler.",
      values: { course: filler?.name ?? "Khoá mới (mô tả trong nội dung)", date: `${currentMonth}-28`, place: "Trực tiếp tại showroom TP.HCM", attendees: "12", cost: "6000000" },
      daysAgo: 1,
      steps: [
        { user: managerByEmail("quanly.nam@minhanh.vn"), label: "Trưởng bộ phận", decision: "PENDING" },
        { user: director, label: "TGĐ", decision: "PENDING" },
      ],
    },
    {
      by: salesByEmail("nv.hcm2@minhanh.vn"),
      type: "PROMOTION",
      kind: "Đề xuất khuyến mãi",
      title: "Mua 10 tặng 1 dòng Meso cho đại lý hạng B",
      content: "Đại lý hạng B tháng này chững lại, đề xuất khuyến mãi để kéo sản lượng Meso trước cuối quý.",
      values: { target: "Nhóm Meso (MS001–MS005)", offer: "Mua 10 hộp tặng 1 hộp cùng loại", audience: "Đại lý hạng B miền Nam", from: `${currentMonth}-25`, to: `${currentMonth}-30`, cost: "18000000" },
      daysAgo: 3,
      steps: [
        { user: managerByEmail("quanly.nam@minhanh.vn"), label: "Trưởng bộ phận", decision: "APPROVED", note: "Đồng ý, khu vực đang thấp hơn kế hoạch." },
        { user: director, label: "TGĐ", decision: "PENDING" },
      ],
    },
    {
      by: salesByEmail("nv.hanoi@minhanh.vn"),
      type: "GIFT",
      kind: "Đề xuất quà tặng",
      title: "Quà tri ân đại lý Key nhân dịp 1 năm hợp tác",
      content: "Đại lý Key đạt hạng A liên tục 12 tháng, đề xuất tặng bộ quà tri ân.",
      values: { gift: "Bộ quà tri ân (hộp quà + voucher đào tạo)", cost: "2500000" },
      daysAgo: 12,
      steps: [
        { user: managerByEmail("quanly.bac@minhanh.vn"), label: "Trưởng bộ phận", decision: "APPROVED" },
        { user: director, label: "TGĐ", decision: "APPROVED", note: "Duyệt." },
      ],
    },
    {
      by: salesByEmail("nv.hcm1@minhanh.vn"),
      type: "OTHER",
      kind: "Đề xuất khác",
      title: "Xin thêm 2 tháng công nợ cho khách Clinic Đông Á",
      content: "Khách gặp khó khăn tài chính do sửa chữa cơ sở, xin gia hạn thanh toán thêm 2 tháng.",
      values: {},
      daysAgo: 20,
      steps: [
        { user: managerByEmail("quanly.nam@minhanh.vn"), label: "Trưởng bộ phận", decision: "APPROVED" },
        { user: director, label: "TGĐ", decision: "REJECTED", note: "Khách đã quá hạn 2 lần, không gia hạn thêm." },
      ],
    },
  ];
  for (const item of seedProposals) {
    const created = new Date(Date.now() - item.daysAgo * 86_400_000);
    const decisions = item.steps.map((step) => step.decision);
    const status = decisions.includes("REJECTED") ? "REJECTED" : decisions.every((d) => d === "APPROVED") ? "APPROVED" : "PENDING";
    const [proposal] = await db
      .insert(t.proposals)
      .values({
        tenantId,
        title: item.title,
        kind: item.kind,
        content: item.content,
        details: { type: item.type, values: item.values },
        createdBy: item.by.id,
        status,
        decidedAt: status === "PENDING" ? null : new Date(created.getTime() + 2 * 86_400_000),
        createdAt: created,
      })
      .returning();
    await db.insert(t.proposalApprovers).values(
      item.steps.map((step, index) => ({
        tenantId,
        proposalId: proposal.id,
        approverId: step.user.id,
        sortOrder: index,
        stepLabel: step.label,
        decision: step.decision,
        note: step.note ?? null,
        decidedAt: step.decision === "PENDING" ? null : new Date(created.getTime() + (index + 1) * 86_400_000),
      })),
    );
  }

  /* --- Văn bản công ty -------------------------------------------------------
     Tệp PDF tối giản sinh ngay tại đây (chữ không dấu vì phông chuẩn của PDF không có tiếng Việt) và ghi vào thư mục tải lên với TÊN CỐ ĐỊNH,
     nên chạy lại seed chỉ ghi đè chứ không để lại tệp mồ côi. */
  const simplePdf = (title: string, lines: string[]): Buffer => {
    const esc = (text: string) => text.replace(/[\\()]/g, (m) => `\\${m}`);
    const stream = [`BT /F1 16 Tf 60 780 Td (${esc(title)}) Tj ET`, ...lines.map((line, index) => `BT /F1 11 Tf 60 ${745 - index * 18} Td (${esc(line)}) Tj ET`)].join("\n");
    const objects = [
      "<< /Type /Catalog /Pages 2 0 R >>",
      "<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
      "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 5 0 R /Resources << /Font << /F1 4 0 R >> >> >>",
      "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
      `<< /Length ${Buffer.byteLength(stream)} >>\nstream\n${stream}\nendstream`,
    ];
    let pdf = "%PDF-1.4\n";
    const offsets: number[] = [];
    objects.forEach((body, index) => {
      offsets.push(Buffer.byteLength(pdf));
      pdf += `${index + 1} 0 obj\n${body}\nendobj\n`;
    });
    const xref = Buffer.byteLength(pdf);
    pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n${offsets.map((o) => `${String(o).padStart(10, "0")} 00000 n \n`).join("")}`;
    pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
    return Buffer.from(pdf);
  };
  const sampleDocuments = [
    { slug: "quyet-dinh-bo-nhiem", category: "VAN_BAN", title: "Quyết định bổ nhiệm Trưởng phòng chuyên môn", description: "Quyết định số 12/2026/QĐ-TGĐ, hiệu lực từ 01/09/2026.", body: ["QUYET DINH SO 12/2026/QD-TGD", "Ve viec bo nhiem Truong phong chuyen mon", "Hieu luc tu ngay 01/09/2026.", "(Ban demo)"], daysAgo: 25 },
    { slug: "thong-bao-nghi-le", category: "THONG_BAO", title: "Thông báo lịch nghỉ lễ Quốc khánh 2026", description: "Toàn công ty nghỉ từ 31/08 đến hết 02/09; đơn hàng gấp liên hệ trực ca.", body: ["THONG BAO LICH NGHI LE QUOC KHANH 2026", "Toan cong ty nghi tu 31/08 den het 02/09.", "(Ban demo)"], daysAgo: 30 },
    { slug: "quy-che-ban-hang", category: "QUY_DINH", title: "Quy chế bán hàng và chiết khấu 2026", description: "Bậc chiết khấu theo giá trị đơn, chính sách mua nhiều tặng nhiều, quy trình duyệt đơn.", body: ["QUY CHE BAN HANG VA CHIET KHAU 2026", "Don tu 5 trieu: 15% khuyen mai; tu 10 trieu: 20%.", "Duyet don: Giam doc khu vuc hoac Ban giam doc.", "(Ban demo)"], daysAgo: 12 },
    { slug: "noi-quy-lao-dong", category: "QUY_DINH", title: "Nội quy lao động công ty", description: "Giờ làm việc, chế độ nghỉ phép, quy định báo cáo ngày và check-in tại điểm bán.", body: ["NOI QUY LAO DONG", "Gio lam viec 8h00 - 17h30, thu Hai den thu Bay.", "(Ban demo)"], daysAgo: 60 },
    { slug: "catalogue-meso-filler", category: "TAI_LIEU", title: "Catalogue sản phẩm Meso – Filler 2026", description: "Bảng giá lẻ/đại lý (đã gồm VAT), dung tích và chỉ định của từng dòng.", body: ["CATALOGUE SAN PHAM MESO - FILLER 2026", "Gia da bao gom VAT.", "(Ban demo)"], daysAgo: 8 },
    { slug: "huong-dan-su-dung-crm", category: "TAI_LIEU", title: "Hướng dẫn sử dụng CRM cho nhân viên kinh doanh", description: "Check-in, báo cáo ngày, tạo đơn hàng và theo dõi công nợ.", body: ["HUONG DAN SU DUNG CRM CHO NVKD", "1. Check-in tai diem ban  2. Bao cao ngay  3. Tao don hang", "(Ban demo)"], daysAgo: 4 },
  ];
  for (const doc of sampleDocuments) {
    const content = simplePdf(doc.title.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/đ/g, "d").replace(/Đ/g, "D"), doc.body);
    const relativePath = `van-ban/seed-${doc.slug}.pdf`;
    await storeFixedFile(relativePath, content);
    await db.insert(t.companyDocuments).values({
      tenantId,
      title: doc.title,
      category: doc.category,
      description: doc.description,
      fileName: `${doc.slug}.pdf`,
      mimeType: "application/pdf",
      fileSize: content.length,
      filePath: relativePath,
      uploadedBy: director.id,
      uploadedAt: new Date(Date.now() - doc.daysAgo * 86_400_000),
    });
  }

  console.log("\n──────────────────────────────────────────────");
  console.log("Dữ liệu mẫu đã sẵn sàng. Tài khoản đăng nhập:\n");
  console.log(`  Ban giám đốc    ${director.email}`);
  console.log(`  Giám đốc KV     ${regionalDirector.email}`);
  for (const m of managers) console.log(`  Quản lý vùng    ${m.email}`);
  console.log(`  NVKD            ${salesUsers[0].email}`);
  console.log(`  Trưởng phòng CM ${specialistHead.email}`);
  console.log(`  Quản lý CM Nam  ${specialistManagerSouth.email}`);
  console.log(`  NV chuyên môn   ${specialist.email}`);
  console.log(`\n  Mật khẩu chung: ${PASSWORD}`);
  console.log("──────────────────────────────────────────────\n");
}

/* -------------------------------------------------------------------------- */

/** Phường/xã có thật của từng tỉnh, để địa chỉ demo không ghép nhầm phường miền Nam với tỉnh miền Bắc. */
const WARDS_BY_PROVINCE: Record<string, string[]> = {
  "Hà Nội": ["Phường Trung Hòa", "Phường Thanh Xuân", "Phường Cầu Giấy", "Phường Hoàn Kiếm", "Phường Ba Đình", "Phường Đống Đa", "Phường Hai Bà Trưng", "Phường Long Biên"],
  "Hải Phòng": ["Phường Lê Chân", "Phường Ngô Quyền", "Phường Hồng Bàng", "Phường Hải An", "Phường Kiến An", "Phường Dương Kinh"],
  "Quảng Ninh": ["Phường Hạ Long", "Phường Bãi Cháy", "Phường Hồng Gai", "Phường Cẩm Phả", "Phường Uông Bí"],
  "Bắc Ninh": ["Phường Kinh Bắc", "Phường Võ Cường", "Phường Từ Sơn", "Phường Đình Bảng"],
  "Đà Nẵng": ["Phường Hải Châu", "Phường Thanh Khê", "Phường Sơn Trà", "Phường Ngũ Hành Sơn", "Phường Cẩm Lệ", "Phường Liên Chiểu"],
  Huế: ["Phường Thuận Hóa", "Phường Phú Xuân", "Phường Vỹ Dạ", "Phường An Cựu"],
  "Nghệ An": ["Phường Vinh Phú", "Phường Trường Vinh", "Phường Thành Vinh", "Phường Cửa Lò"],
  "Khánh Hoà": ["Phường Nha Trang", "Phường Tây Nha Trang", "Phường Bắc Nha Trang", "Phường Cam Ranh"],
  "TP. Hồ Chí Minh": ["Phường Bến Nghé", "Phường Tân Định", "Phường Bến Thành", "Phường Tân Bình", "Phường Phú Nhuận", "Phường Thảo Điền", "Phường Bình Thạnh", "Phường Gò Vấp"],
  "Cần Thơ": ["Phường Ninh Kiều", "Phường Cái Khế", "Phường Bình Thủy", "Phường Cái Răng"],
  "Bình Dương": ["Phường Thủ Dầu Một", "Phường Phú Lợi", "Phường Dĩ An", "Phường Thuận An", "Phường Lái Thiêu", "Phường Bến Cát"],
  "Đồng Nai": ["Phường Biên Hòa", "Phường Trấn Biên", "Phường Long Bình", "Phường Tam Hiệp", "Xã Long Thành"],
};

const PROVINCE_COORDS: Record<string, { lat: number; lng: number }> = {
  "Hà Nội": { lat: 21.0278, lng: 105.8342 },
  "Hải Phòng": { lat: 20.8449, lng: 106.6881 },
  "Quảng Ninh": { lat: 20.9599, lng: 107.0448 },
  "Bắc Ninh": { lat: 21.1861, lng: 106.0763 },
  "Đà Nẵng": { lat: 16.0544, lng: 108.2022 },
  Huế: { lat: 16.4637, lng: 107.5909 },
  "Nghệ An": { lat: 18.6796, lng: 105.6813 },
  "Khánh Hoà": { lat: 12.2388, lng: 109.1967 },
  "TP. Hồ Chí Minh": { lat: 10.8231, lng: 106.6297 },
  "Cần Thơ": { lat: 10.0452, lng: 105.7469 },
  "Bình Dương": { lat: 11.0686, lng: 106.6505 },
  "Đồng Nai": { lat: 10.9453, lng: 106.8133 },
};

function shuffle<T>(list: T[]): T[] {
  for (let i = list.length - 1; i > 0; i--) {
    const j = Math.floor(rnd() * (i + 1));
    [list[i], list[j]] = [list[j], list[i]];
  }
  return list;
}

function shiftDays(isoDate: string, days: number): string {
  const date = new Date(`${isoDate}T00:00:00Z`);
  date.setUTCDate(date.getUTCDate() + days);
  return date.toISOString().slice(0, 10);
}

async function insertInChunks<T extends { $inferInsert: object }>(
  table: T,
  values: unknown[],
  size = 400,
) {
  for (let i = 0; i < values.length; i += size) {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    await db.insert(table as any).values(values.slice(i, i + size) as any);
  }
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
