/**
 * 車線看板警示：Rule1（4F 同店跨線 weekday）、Rule2（非 4F 同店跨線發車時分）。
 * 規則細節見 `README-ROUTE-BOARD-WARNINGS.md`。
 */
import { formatDepartureTime, normalizeStoreId } from "@/app/utils/formatUtil";

const LANE_KEY_SEP = "|";

/** 與 RouteBoard `encodeLaneId` 同演算法，避免從巨型元件 re-export。 */
export function encodeLaneKeyForWarning(
  truckLanceCode: string,
  laneRemark: string | null | undefined,
): string {
  const code = String(truckLanceCode || "").trim();
  const rem =
    laneRemark != null && String(laneRemark).trim() !== ""
      ? String(laneRemark).trim()
      : "";
  return `${encodeURIComponent(code)}${LANE_KEY_SEP}${encodeURIComponent(rem)}`;
}

const WEEKDAY_LOWER = new Set([
  "mon",
  "tue",
  "wed",
  "thu",
  "fri",
  "sat",
  "sun",
]);

const CANON_WEEKDAY: Record<string, string> = {
  mon: "Mon",
  tue: "Tue",
  wed: "Wed",
  thu: "Thu",
  fri: "Fri",
  sat: "Sat",
  sun: "Sun",
};

/** 第二段 `_` 為 weekday；normalize 成 Mon..Sun（DB 慣例大小寫）。無法辨識回傳 null。 */
export function parseWeekdayFromTruckLanceCode(
  truckLanceCode: string,
): string | null {
  const parts = String(truckLanceCode || "").split("_");
  if (parts.length < 2) return null;
  const token = String(parts[1] ?? "").trim();
  if (!token) return null;
  const key3 = token.slice(0, 3).toLowerCase();
  if (!WEEKDAY_LOWER.has(key3)) return null;
  return CANON_WEEKDAY[key3] ?? null;
}

/** Rule2：發車時間比到 HH:mm；無效 / 空 / "-" → null（不參與 Rule2）。 */
export function departureTimeToHHmmKey(
  dep: string | number[] | null | undefined,
): string | null {
  const fmt = formatDepartureTime(dep as any);
  if (!fmt || fmt === "-") return null;
  const m = fmt.match(/^(\d{1,2}):(\d{2})(?::\d{2})?/);
  if (!m) return null;
  return `${m[1].padStart(2, "0")}:${m[2].padStart(2, "0")}`;
}

export type TruckLaneWarningInputRow = {
  truckRowId: number;
  truckLanceCode: string;
  /** 車線桶 remark（與 RouteBoard lane.remark 一致） */
  laneRemark: string | null;
  /** truck 列 store（已 normalize 與否皆可，內部會 normalize） */
  storeId: string;
  departureTime: string | number[] | null | undefined;
  shopEntityId: number | null;
  shopCode: string;
  shopDisplayName: string;
  deleted?: boolean;
};

export type TruckLaneWarningRule = "RULE_1_WEEKDAY" | "RULE_2_DEPARTURE";

export type TruckLaneWarningLaneRef = {
  laneKey: string;
  truckLanceCode: string;
  laneRemark: string | null;
  storeId: string;
  departureTimeDisplay: string | null;
  weekday: string | null;
  truckRowId: number;
};

export type TruckLaneWarning = {
  rule: TruckLaneWarningRule;
  shopEntityId: number | null;
  shopCode: string;
  shopDisplayName: string;
  /** Rule1: Mon..Sun；Rule2: HH:mm */
  triggerValue: string;
  lanes: TruckLaneWarningLaneRef[];
};

export type TruckLaneWarningResult = {
  warnings: TruckLaneWarning[];
  /** 4F 列但 TruckLanceCode 無法解析 weekday（不進 Rule1） */
  weekdayParseFailures: Array<{ truckRowId: number; truckLanceCode: string }>;
};

/**
 * 店鋪主鍵：優先 master `shopEntityId`；若本列無 id，但同 `shopCode + store` 的其他列有 id，則併入該 id（避免 join 不一致導致同店被拆成 `id:` 與 `fallback:` 兩桶）。
 * 若仍無法對應 master，則 fallback `shopCode（小寫） + storeId`。
 */
export function buildMasterShopIdByCodeStore(
  rows: readonly {
    shopEntityId: number | null;
    shopCode: string;
    storeId: string;
  }[],
): Map<string, number> {
  const m = new Map<string, number>();
  for (const row of rows) {
    const id = row.shopEntityId;
    if (id == null || !Number.isFinite(id) || id <= 0) continue;
    const code = String(row.shopCode || "").trim().toLowerCase();
    if (!code) continue;
    const store = normalizeStoreId(row.storeId);
    const ck = `${code}|${store}`;
    if (!m.has(ck)) m.set(ck, id);
  }
  return m;
}

function codeStoreKey(row: {
  shopCode: string;
  storeId: string;
}): string {
  const code = String(row.shopCode || "").trim().toLowerCase();
  const store = normalizeStoreId(row.storeId);
  return `${code}|${store}`;
}

export function shopIdentityKeyFromRow(
  row: {
    shopEntityId: number | null;
    shopCode: string;
    storeId: string;
  },
  masterByCodeStore?: ReadonlyMap<string, number>,
): string {
  if (
    row.shopEntityId != null &&
    Number.isFinite(row.shopEntityId) &&
    row.shopEntityId > 0
  ) {
    return `id:${row.shopEntityId}`;
  }
  if (masterByCodeStore != null) {
    const ck = codeStoreKey(row);
    const inferred = masterByCodeStore.get(ck);
    if (inferred != null && inferred > 0) return `id:${inferred}`;
  }
  const code = String(row.shopCode || "").trim().toLowerCase();
  const store = normalizeStoreId(row.storeId);
  return `fallback:${code}|store:${store}`;
}

function rowToLaneRef(row: TruckLaneWarningInputRow): TruckLaneWarningLaneRef {
  const laneKey = encodeLaneKeyForWarning(row.truckLanceCode, row.laneRemark);
  const storeNorm = normalizeStoreId(row.storeId);
  const depDisp = departureTimeToHHmmKey(row.departureTime);
  const weekday =
    storeNorm === "4F"
      ? parseWeekdayFromTruckLanceCode(row.truckLanceCode)
      : null;
  return {
    laneKey,
    truckLanceCode: String(row.truckLanceCode || "").trim(),
    laneRemark: row.laneRemark,
    storeId: storeNorm,
    departureTimeDisplay: depDisp,
    weekday,
    truckRowId: row.truckRowId,
  };
}

function pickLaneRef(
  byLane: Map<string, TruckLaneWarningLaneRef>,
  laneKey: string,
): TruckLaneWarningLaneRef {
  const hit = byLane.get(laneKey);
  if (hit) return hit;
  return {
    laneKey,
    truckLanceCode: "",
    laneRemark: null,
    storeId: "",
    departureTimeDisplay: null,
    weekday: null,
    truckRowId: 0,
  };
}

export function computeTruckLaneWarnings(
  rows: TruckLaneWarningInputRow[],
): TruckLaneWarningResult {
  const active = (rows || []).filter((r) => r.deleted !== true);
  const masterByCodeStore = buildMasterShopIdByCodeStore(active);
  const shopKOf = (r: TruckLaneWarningInputRow) =>
    shopIdentityKeyFromRow(r, masterByCodeStore);
  const entityIdFromShopKey = (shopK: string): number | null => {
    if (!shopK.startsWith("id:")) return null;
    const n = Number(shopK.slice(3));
    return Number.isFinite(n) && n > 0 ? n : null;
  };
  const weekdayParseFailures: TruckLaneWarningResult["weekdayParseFailures"] =
    [];

  const byLaneRef = new Map<string, TruckLaneWarningLaneRef>();
  for (const row of active) {
    const lk = encodeLaneKeyForWarning(row.truckLanceCode, row.laneRemark);
    if (!byLaneRef.has(lk)) byLaneRef.set(lk, rowToLaneRef(row));
  }

  // Rule 1: 4F, same shop, >=2 distinct lanes, same parsed weekday
  const r1Map = new Map<
    string,
    Map<string, Set<string>>
  >();
  for (const row of active) {
    const store = normalizeStoreId(row.storeId);
    if (store !== "4F") continue;
    const wd = parseWeekdayFromTruckLanceCode(row.truckLanceCode);
    if (!wd) {
      weekdayParseFailures.push({
        truckRowId: row.truckRowId,
        truckLanceCode: String(row.truckLanceCode || ""),
      });
      continue;
    }
    const shopK = shopKOf(row);
    const lk = encodeLaneKeyForWarning(row.truckLanceCode, row.laneRemark);
    let byWd = r1Map.get(shopK);
    if (!byWd) {
      byWd = new Map();
      r1Map.set(shopK, byWd);
    }
    let set = byWd.get(wd);
    if (!set) {
      set = new Set();
      byWd.set(wd, set);
    }
    set.add(lk);
  }

  const warnings: TruckLaneWarning[] = [];
  r1Map.forEach((byWd, shopK) => {
    byWd.forEach((laneKeys, wd) => {
      if (laneKeys.size < 2) return;
      const sampleRow = active.find(
        (r) =>
          shopKOf(r) === shopK &&
          normalizeStoreId(r.storeId) === "4F" &&
          parseWeekdayFromTruckLanceCode(r.truckLanceCode) === wd,
      );
      const laneKeyList: string[] = Array.from(laneKeys);
      warnings.push({
        rule: "RULE_1_WEEKDAY",
        shopEntityId:
          sampleRow?.shopEntityId ?? entityIdFromShopKey(shopK) ?? null,
        shopCode: sampleRow?.shopCode ?? "",
        shopDisplayName: sampleRow?.shopDisplayName ?? "",
        triggerValue: wd,
        lanes: laneKeyList.map((lk) => pickLaneRef(byLaneRef, lk)),
      });
    });
  });

  // Rule 2: non-4F, same shop, >=2 lanes, same HH:mm departure
  const r2Map = new Map<string, Map<string, Set<string>>>();
  for (const row of active) {
    const store = normalizeStoreId(row.storeId);
    if (store === "4F") continue;
    const hhmm = departureTimeToHHmmKey(row.departureTime);
    if (!hhmm) continue;
    const shopK = shopKOf(row);
    const lk = encodeLaneKeyForWarning(row.truckLanceCode, row.laneRemark);
    let byT = r2Map.get(shopK);
    if (!byT) {
      byT = new Map();
      r2Map.set(shopK, byT);
    }
    let set = byT.get(hhmm);
    if (!set) {
      set = new Set();
      byT.set(hhmm, set);
    }
    set.add(lk);
  }

  r2Map.forEach((byT, shopK) => {
    byT.forEach((laneKeys, hhmm) => {
      if (laneKeys.size < 2) return;
      const sampleRow = active.find(
        (r) =>
          shopKOf(r) === shopK &&
          normalizeStoreId(r.storeId) !== "4F" &&
          departureTimeToHHmmKey(r.departureTime) === hhmm,
      );
      const laneKeyList: string[] = Array.from(laneKeys);
      warnings.push({
        rule: "RULE_2_DEPARTURE",
        shopEntityId:
          sampleRow?.shopEntityId ?? entityIdFromShopKey(shopK) ?? null,
        shopCode: sampleRow?.shopCode ?? "",
        shopDisplayName: sampleRow?.shopDisplayName ?? "",
        triggerValue: hhmm,
        lanes: laneKeyList.map((lk) => pickLaneRef(byLaneRef, lk)),
      });
    });
  });

  // 穩定排序：規則、店名、觸發值
  warnings.sort((a, b) => {
    if (a.rule !== b.rule) return a.rule.localeCompare(b.rule);
    const na = a.shopDisplayName || a.shopCode;
    const nb = b.shopDisplayName || b.shopCode;
    const c = na.localeCompare(nb, "zh-Hant");
    if (c !== 0) return c;
    return a.triggerValue.localeCompare(b.triggerValue);
  });

  return { warnings, weekdayParseFailures };
}

/** Stable key for comparing warning sets before/after a hypothetical edit. */
export function truckLaneWarningFingerprint(w: TruckLaneWarning): string {
  const laneKeys = w.lanes
    .map((l) => l.laneKey)
    .sort()
    .join(",");
  const shopKey =
    w.shopEntityId != null && Number.isFinite(w.shopEntityId) && w.shopEntityId > 0
      ? `id:${w.shopEntityId}`
      : `code:${String(w.shopCode || "").trim().toLowerCase()}`;
  return `${w.rule}|${shopKey}|${w.triggerValue}|${laneKeys}`;
}

/** Warnings present after but not before (e.g. only prompt on newly introduced conflicts). */
export function diffNewTruckLaneWarnings(
  before: readonly TruckLaneWarning[],
  after: readonly TruckLaneWarning[],
): TruckLaneWarning[] {
  const beforeKeys = new Set(before.map(truckLaneWarningFingerprint));
  return after.filter((w) => !beforeKeys.has(truckLaneWarningFingerprint(w)));
}

/** 模擬「即將新增」的一筆店鋪列（`tempTruckRowId` 建議用負數）；供 Rule1/2 試算。 */
export function appendSyntheticPendingShopRow(
  baseRows: TruckLaneWarningInputRow[],
  lane: {
    truckLanceCode: string;
    laneRemark: string | null;
    storeId: string | number;
    startTime: string;
  },
  pick: { id: number; name: string; code: string },
  tempTruckRowId: number,
): TruckLaneWarningInputRow[] {
  const code = String(lane.truckLanceCode || "").trim();
  const rem =
    lane.laneRemark != null && String(lane.laneRemark).trim() !== ""
      ? String(lane.laneRemark).trim()
      : null;
  return [
    ...baseRows,
    {
      truckRowId: tempTruckRowId,
      truckLanceCode: code,
      laneRemark: rem,
      storeId: normalizeStoreId(lane.storeId),
      departureTime: lane.startTime,
      shopEntityId: pick.id,
      shopCode: pick.code,
      shopDisplayName: pick.name,
    },
  ];
}
