/**
 * Dự báo dự trù sản phẩm tháng tới (yêu cầu cuối mục 6).
 *
 * Phương pháp: bình quân trượt 3 tháng nhân hệ số mùa vụ của tháng đích, trong
 * đó hệ số mùa vụ = sản lượng tháng đó năm ngoái / bình quân tháng năm ngoái.
 * Cố ý chọn cách tính đơn giản, giải thích được cho khách hàng — người dùng
 * luôn chỉnh tay được trước khi chốt (`adjustedQty`).
 */
import { and, eq, gte, inArray, isNull, lte, notInArray, sql } from "drizzle-orm";
import { db } from "@/db";
import { orders, orderLines, productGroups, products, forecasts } from "@/db/schema";
import { periodRange, lastMonths, shiftMonth, lastCompletedMonth } from "./period";

export const FORECAST_WINDOW_MONTHS = 3;
/** Số tháng tới được dự báo mỗi đêm (tháng tới, 2 tháng nữa, 3 tháng nữa) — đủ cho một chu kỳ đặt hàng. */
export const FORECAST_HORIZON_MONTHS = 3;

/** Chỉ áp hệ số mùa vụ khi năm ngoái có đủ dữ liệu, tránh khuếch đại nhiễu. */
const MIN_MONTHS_FOR_SEASONALITY = 6;

export type ForecastRow = {
  productId: string;
  code: string;
  name: string;
  forecastQty: number;
  averageQty: number;
  seasonalFactor: number;
  confidence: number;
};

/**
 * Tính dự báo cho `targetPeriod` (ví dụ '2026-10') dựa trên dữ liệu tới hết THÁNG ĐÃ ĐÓNG SỔ gần nhất (`basedOn`),
 * rồi ghi vào bảng `forecasts`.
 *
 * `basedOn` mặc định là tháng đã đóng sổ gần nhất. Trước đây lấy "tháng liền trước tháng đích" — khi đích là tháng sau thì đó chính là
 * tháng ĐANG CHẠY DỞ, nên cửa sổ 3 tháng chứa một tháng mới có vài ngày số liệu và dự báo bị thấp đi một khoảng lớn giữa tháng.
 * Chạy lại KHÔNG xoá số lượng người dùng đã chỉnh tay (`adjustedQty`) — trước đây job hằng đêm xoá hết rồi ghi lại nên mọi lần chỉnh tay mất sạch.
 */
export async function recomputeForecast(
  tenantId: string,
  targetPeriod: string,
  basedOn: string = lastCompletedMonth(),
): Promise<ForecastRow[]> {
  const lastClosedMonth = basedOn;
  const windowMonths = lastMonths(lastClosedMonth, FORECAST_WINDOW_MONTHS);

  const qtyByProductMonth = await db
    .select({
      productId: orderLines.productId,
      month: sql<string>`to_char(${orderLines.orderDate}, 'YYYY-MM')`.as("month"),
      qty: sql<number>`coalesce(sum(${orderLines.qtySold}), 0)::int`.as("qty"),
    })
    .from(orderLines)
    .innerJoin(orders, eq(orderLines.orderId, orders.id))
    .where(
      and(
        eq(orders.tenantId, tenantId),
        eq(orders.status, "CONFIRMED"),
        // Lấy 24 tháng để đủ dữ liệu tính mùa vụ năm trước.
        gte(orderLines.orderDate, periodRange(shiftMonth(lastClosedMonth, 23)).start),
        lte(orderLines.orderDate, periodRange(lastClosedMonth).end),
      ),
    )
    .groupBy(orderLines.productId, sql`to_char(${orderLines.orderDate}, 'YYYY-MM')`);

  const byProduct = new Map<string, Map<string, number>>();
  for (const row of qtyByProductMonth) {
    const map = byProduct.get(row.productId) ?? new Map<string, number>();
    map.set(row.month, Number(row.qty));
    byProduct.set(row.productId, map);
  }

  const activeProducts = await db
    .select({ id: products.id, code: products.code, name: products.name })
    .from(products)
    .where(and(eq(products.tenantId, tenantId), eq(products.isActive, true)));

  const results: ForecastRow[] = [];

  for (const product of activeProducts) {
    const months = byProduct.get(product.id);
    if (!months || months.size === 0) continue;

    const windowQty = windowMonths.map((m) => months.get(m) ?? 0);
    const averageQty = windowQty.reduce((a, b) => a + b, 0) / FORECAST_WINDOW_MONTHS;
    if (averageQty === 0) continue;

    const { factor, confidence } = seasonality(months, targetPeriod, windowQty);
    const forecastQty = Math.max(0, Math.round(averageQty * factor));

    results.push({
      productId: product.id,
      code: product.code,
      name: product.name,
      forecastQty,
      averageQty: Math.round(averageQty * 10) / 10,
      seasonalFactor: Math.round(factor * 100) / 100,
      confidence,
    });
  }

  const now = new Date();
  if (results.length > 0) {
    await db
      .insert(forecasts)
      .values(
        results.map((r) => ({
          tenantId,
          periodKey: targetPeriod,
          productId: r.productId,
          forecastQty: r.forecastQty,
          method: `MOVING_AVG_${FORECAST_WINDOW_MONTHS}M`,
          confidence: r.confidence,
          computedAt: now,
        })),
      )
      // Cập nhật số máy tính, GIỮ NGUYÊN adjustedQty và ghi chú của người dùng.
      .onConflictDoUpdate({
        target: [forecasts.tenantId, forecasts.periodKey, forecasts.productId],
        set: { forecastQty: sql`excluded.forecast_qty`, method: sql`excluded.method`, confidence: sql`excluded.confidence`, computedAt: sql`excluded.computed_at` },
      });
  }

  // Sản phẩm không còn được dự báo (ngừng bán, tắt hoạt động): bỏ dòng cũ, trừ dòng đã có số chỉnh tay.
  const keep = results.map((r) => r.productId);
  await db
    .delete(forecasts)
    .where(
      and(
        eq(forecasts.tenantId, tenantId),
        eq(forecasts.periodKey, targetPeriod),
        isNull(forecasts.adjustedQty),
        keep.length > 0 ? notInArray(forecasts.productId, keep) : undefined,
      ),
    );

  return results.sort((a, b) => b.forecastQty - a.forecastQty);
}

/**
 * Hệ số mùa vụ cho tháng đích và mức tin cậy của dự báo.
 * Độ tin cậy thấp khi ít dữ liệu lịch sử hoặc sản lượng biến động mạnh.
 */
export function seasonality(
  months: Map<string, number>,
  targetPeriod: string,
  windowQty: number[],
): { factor: number; confidence: number } {
  const sameMonthLastYear = `${Number(targetPeriod.slice(0, 4)) - 1}-${targetPeriod.slice(5, 7)}`;
  const lastYearMonths = [...months.entries()].filter(([m]) =>
    m.startsWith(String(Number(targetPeriod.slice(0, 4)) - 1)),
  );

  let factor = 1;
  if (lastYearMonths.length >= MIN_MONTHS_FOR_SEASONALITY) {
    /* Bình quân tháng năm ngoái tính trên số tháng sản phẩm CÒN BÁN từ tháng đầu tiên có doanh thu năm đó tới hết năm, tháng không có đơn tính 0.
       Trước đây chia cho số tháng "có dữ liệu" nên sản phẩm bán rời rạc (6 tháng trong 12) bị tính bình quân gấp đôi thực tế → hệ số mùa vụ thấp giả. */
    const firstMonthNumber = Math.min(...lastYearMonths.map(([m]) => Number(m.slice(5, 7))));
    const targetMonthNumber = Number(targetPeriod.slice(5, 7));
    const spanMonths = 13 - firstMonthNumber;
    const lastYearAvg = lastYearMonths.reduce((sum, [, qty]) => sum + qty, 0) / spanMonths;
    // Tháng đích năm ngoái không có đơn nhưng sản phẩm đã bán từ trước đó → coi là 0 (mùa thấp), không bỏ qua hệ số.
    const targetMonthQty = months.get(sameMonthLastYear) ?? (targetMonthNumber >= firstMonthNumber ? 0 : null);
    if (lastYearAvg > 0 && targetMonthQty != null) {
      // Chặn trong [0,5 – 2,0] để một tháng bất thường năm ngoái không kéo lệch dự báo.
      factor = Math.min(2, Math.max(0.5, targetMonthQty / lastYearAvg));
    }
  }

  const mean = windowQty.reduce((a, b) => a + b, 0) / windowQty.length;
  const variance =
    windowQty.reduce((sum, q) => sum + (q - mean) ** 2, 0) / windowQty.length;
  const cv = mean > 0 ? Math.sqrt(variance) / mean : 1;

  const dataScore = Math.min(1, months.size / 12);
  const stabilityScore = Math.max(0, 1 - cv);
  const confidence = Math.round(((dataScore * 0.4 + stabilityScore * 0.6) as number) * 100) / 100;

  return { factor, confidence };
}

/* -------------------------------------------------------------------------- */
/* Bảng dự trù để hiển thị                                                    */
/* -------------------------------------------------------------------------- */

export type ForecastTableRow = {
  id: string;
  productId: string;
  code: string;
  name: string;
  groupName: string;
  role: string;
  /** Số máy tính. */
  forecastQty: number;
  /** Số người dùng chỉnh tay; null = dùng số máy tính. */
  adjustedQty: number | null;
  /** Số dự trù đang có hiệu lực = số chỉnh tay nếu có, không thì số máy tính. */
  planQty: number;
  confidence: number | null;
  /** Sản lượng tháng đã đóng sổ gần nhất, bình quân 3 tháng và cùng tháng năm trước — để người quyết đối chiếu với số dự báo. */
  lastMonthQty: number;
  avgQty: number;
  lastYearQty: number | null;
  computedAt: Date;
};

export async function forecastTable(tenantId: string, targetPeriod: string, basedOn: string = lastCompletedMonth()): Promise<ForecastTableRow[]> {
  const rows = await db
    .select({
      id: forecasts.id,
      productId: forecasts.productId,
      code: products.code,
      name: products.name,
      role: products.role,
      groupName: sql<string>`coalesce(${productGroups.name}, 'Chưa phân nhóm')`,
      forecastQty: forecasts.forecastQty,
      adjustedQty: forecasts.adjustedQty,
      confidence: forecasts.confidence,
      computedAt: forecasts.computedAt,
    })
    .from(forecasts)
    .innerJoin(products, eq(forecasts.productId, products.id))
    .leftJoin(productGroups, eq(products.groupId, productGroups.id))
    .where(and(eq(forecasts.tenantId, tenantId), eq(forecasts.periodKey, targetPeriod)));
  if (rows.length === 0) return [];

  const windowMonths = lastMonths(basedOn, FORECAST_WINDOW_MONTHS);
  const sameMonthLastYear = shiftMonth(targetPeriod, 12);
  const wanted = [...windowMonths, sameMonthLastYear];
  const monthExpr = sql<string>`to_char(${orderLines.orderDate}, 'YYYY-MM')`;
  const history = await db
    .select({ productId: orderLines.productId, month: monthExpr, qty: sql<number>`coalesce(sum(${orderLines.qtySold}), 0)::int` })
    .from(orderLines)
    .innerJoin(orders, eq(orderLines.orderId, orders.id))
    .where(and(eq(orders.tenantId, tenantId), eq(orders.status, "CONFIRMED"), inArray(monthExpr, wanted)))
    .groupBy(orderLines.productId, monthExpr);
  const byProduct = new Map<string, Map<string, number>>();
  for (const h of history) {
    const map = byProduct.get(h.productId) ?? new Map<string, number>();
    map.set(h.month, Number(h.qty));
    byProduct.set(h.productId, map);
  }

  return rows
    .map((r) => {
      const months = byProduct.get(r.productId) ?? new Map<string, number>();
      const windowQty = windowMonths.map((m) => months.get(m) ?? 0);
      return {
        ...r,
        role: r.role as string,
        planQty: r.adjustedQty ?? r.forecastQty,
        lastMonthQty: months.get(basedOn) ?? 0,
        avgQty: Math.round((windowQty.reduce((a, b) => a + b, 0) / FORECAST_WINDOW_MONTHS) * 10) / 10,
        lastYearQty: months.has(sameMonthLastYear) ? (months.get(sameMonthLastYear) ?? 0) : null,
      };
    })
    // Xếp theo số máy tính (không phải số đã chỉnh) để dòng không nhảy vị trí ngay sau khi người dùng lưu số điều chỉnh.
    .sort((a, b) => b.forecastQty - a.forecastQty || a.name.localeCompare(b.name, "vi"));
}
