// GreenhouseMap.jsx — data-driven greenhouse floorplan from selected map payload

function toPercent(value) {
  const v = Number(value);
  if (!Number.isFinite(v)) return "0%";
  return `${Math.max(0, Math.min(1, v)) * 100}%`;
}

function normalizeId(value) {
  if (value === null || value === undefined) return null;
  return String(value);
}

function statusToTri(state) {
  const v = String(state || "").toLowerCase();
  if (v === "critical" || v === "crit" || v === "error" || v === "failed") return "cri";
  if (v === "offline" || v === "off") return "off";
  if (v === "warning" || v === "warn" || v === "alert") return "att";
  if (v === "active" || v === "act") return "act";
  if (v === "ok" || v === "normal" || v === "online") return "thr";
  return "thr";
}

const STATUS_META = [
  { key: "thr", label: "Норма", color: "#7FB539", zoneFill: "rgba(127,181,57,.14)" },
  { key: "act", label: "Активный", color: "#E8B84A", zoneFill: "rgba(232,184,74,.16)" },
  { key: "att", label: "Внимание", color: "#FF7A00", zoneFill: "rgba(255,122,0,.16)" },
  { key: "cri", label: "Авария", color: "#E5342B", zoneFill: "rgba(229,52,43,.14)" },
  { key: "off", label: "Офлайн", color: "#C2CCD8", zoneFill: "rgba(194,204,216,.32)" },
];

const STATUS_META_BY_KEY = STATUS_META.reduce((acc, item) => {
  acc[item.key] = item;
  return acc;
}, {});

function statusMetaFromKey(key) {
  return STATUS_META_BY_KEY[key] || STATUS_META_BY_KEY.thr;
}

const MAP_EMPTY_PAYLOAD = { greenhouse_id: null, blueprint_url: null, zones: [], positions: [], device_placements: [] };
const MAP_REFRESH_MS = 60 * 1000;
const MAP_STALE_AFTER_MS = 20 * 60 * 1000;

function safeDate(value) {
  if (!value || typeof value !== "string") return null;
  const isoLike = value.includes("T") || value.includes("-");
  if (!isoLike) return null;
  const d = new Date(value);
  return Number.isNaN(d.getTime()) ? null : d;
}

function formatClock(value) {
  const d = safeDate(value);
  if (!d) return "—";
  return d.toLocaleTimeString("ru-RU", { hour12: false });
}

function formatAge(value) {
  const d = safeDate(value);
  if (!d) return "n/a";
  const diffSec = Math.max(0, Math.floor((Date.now() - d.getTime()) / 1000));
  if (diffSec < 60) return `${diffSec} сек`;
  const min = Math.floor(diffSec / 60);
  if (min < 60) return `${min} мин`;
  const hr = Math.floor(min / 60);
  return `${hr} ч`;
}

function isStaleTimestamp(value) {
  const d = safeDate(value);
  if (!d) return false;
  return (Date.now() - d.getTime()) > MAP_STALE_AFTER_MS;
}

function pickLatestMapTimestamp(mapPayload, hives, lastAttemptAt) {
  const candidates = [];
  (mapPayload?.zones || []).forEach((zone) => {
    if (zone && typeof zone.updated_at === "string") candidates.push(zone.updated_at);
  });
  (hives || []).forEach((hive) => {
    if (!hive || typeof hive !== "object") return;
    if (typeof hive.updated_at === "string") candidates.push(hive.updated_at);
    if (typeof hive.last_updated_at === "string") candidates.push(hive.last_updated_at);
  });
  if (typeof lastAttemptAt === "string") candidates.push(lastAttemptAt);
  const withDates = candidates
    .map((item) => ({ value: item, date: safeDate(item) }))
    .filter((item) => !!item.date)
    .sort((a, b) => b.date.getTime() - a.date.getTime());
  return withDates.length ? withDates[0].value : null;
}

function buildFallbackPositions(zones) {
  if (!Array.isArray(zones) || zones.length === 0) return [];
  const cols = Math.max(1, Math.ceil(Math.sqrt(zones.length)));
  const rows = Math.max(1, Math.ceil(zones.length / cols));
  const cellWidth = 1 / cols;
  const cellHeight = 1 / rows;
  return zones.map((zone, index) => {
    const col = index % cols;
    const row = Math.floor(index / cols);
    return {
      zone_id: zone.id,
      x: Number((col * cellWidth).toFixed(6)),
      y: Number((row * cellHeight).toFixed(6)),
      width: Number(cellWidth.toFixed(6)),
      height: Number(cellHeight.toFixed(6)),
    };
  });
}

function buildHivePlacements(positions, hivesByZone) {
  const result = [];
  (positions || []).forEach((pos) => {
    const zoneId = normalizeId(pos.zone_id || pos.id);
    const hives = (zoneId && hivesByZone.get(zoneId)) || [];
    if (!hives.length) return;
    const width = Number(pos.width) || 0;
    const height = Number(pos.height) || 0;
    const x0 = Number(pos.x) || 0;
    const y0 = Number(pos.y) || 0;
    if (width <= 0 || height <= 0) return;
    const cols = Math.max(1, Math.ceil(Math.sqrt(hives.length)));
    const rows = Math.max(1, Math.ceil(hives.length / cols));
    const spacingX = width / cols;
    const spacingY = height / rows;

    hives.forEach((hive, index) => {
      const col = index % cols;
      const row = Math.floor(index / cols);
      const x = x0 + spacingX * (col + 0.5);
      const y = y0 + spacingY * (row + 0.5);
      result.push({
        id: normalizeId(hive.id),
        num: normalizeId(hive.id),
        zoneId,
        triState: statusToTri(hive.state || hive.status),
        x,
        y,
      });
    });
  });
  return result;
}

function buildDevicePlacements(rawPlacements) {
  return (rawPlacements || [])
    .map((item) => {
      const id = normalizeId(item.device_id || item.id);
      if (!id) return null;
      return {
        id,
        zoneId: normalizeId(item.zone_id || item.zoneId),
        x: Number(item.x) || 0,
        y: Number(item.y) || 0,
        kind: item.kind || item.type || "sensor",
        triState: statusToTri(item.status || item.state),
      };
    })
    .filter(Boolean);
}

function Zone({ zone, rowCount, isSelected, onClick }) {
  const hiveCount = Number(zone.hive_count || 0);
  const deviceCount = Number(zone.device_count || 0);
  const label = zone.name || zone.row || zone.id || "Зона";
  const statusMeta = statusMetaFromKey(zone.triState);
  return (
    <div
      className="gh-zone"
      data-state={zone.triState}
      onClick={() => onClick && onClick(zone)}
      role={onClick ? "button" : undefined}
      tabIndex={onClick ? 0 : undefined}
      onKeyDown={(e) => {
        if (!onClick) return;
        if (e.key === "Enter" || e.key === " ") {
          e.preventDefault();
          onClick(zone);
        }
      }}
      style={{
        left: toPercent(zone.x),
        top: toPercent(zone.y),
        width: toPercent(zone.width),
        height: toPercent(zone.height),
        borderColor: isSelected ? "#007BFB" : statusMeta.color,
        boxShadow: isSelected ? "inset 0 0 0 1px rgba(0,123,251,.2)" : `inset 0 0 0 1px ${statusMeta.color}22`,
        background: isSelected ? undefined : statusMeta.zoneFill,
      }}
    >
      <span className="gh-zone-lbl">
        <span className={"gh-zone-state tri tri-" + zone.triState}></span>{label}
      </span>
      <span className="gh-zone-meta">{hiveCount} ульев · {deviceCount} датч.</span>
      <div className="gh-rows">
        {Array.from({ length: rowCount }).map((_, i) => <div key={i} className="gh-row"></div>)}
      </div>
    </div>
  );
}

function GreenhouseMap({
  selectedHive,
  onSelectHive,
  onDrillZone,
  onDrillHive,
  onDrillDevice,
  greenhouseId = "gh-1",
  selectedGreenhouseId,
  selectedZoneId,
}) {
  const RecommendationsPanel = window.RecommendationsPanel;
  const [mapPayload, setMapPayload] = React.useState(MAP_EMPTY_PAYLOAD);
  const [hives, setHives] = React.useState([]);
  const [mapLifecycle, setMapLifecycle] = React.useState({
    status: "loading", // "loading" | "ready" | "empty" | "stale" | "error"
    message: "",
    lastSuccessAt: null,
    dataTimestamp: null,
  });
  const requestSeqRef = React.useRef(0);
  const lastSnapshotRef = React.useRef(null);
  const resolvedGreenhouseId = selectedGreenhouseId || greenhouseId || "gh-1";

  const greenhouse = Object.values(DATA?.greenhouses || {})
    .flat()
    .find((item) => String(item.id) === String(resolvedGreenhouseId));
  const zone = (mapPayload.zones || []).find((item) => String(item.id) === String(selectedZoneId));

  const loadMapData = React.useCallback(async (opts = {}) => {
    const silent = Boolean(opts.silent);
    const seq = ++requestSeqRef.current;
    if (!resolvedGreenhouseId) {
      setMapPayload(MAP_EMPTY_PAYLOAD);
      setHives([]);
      setMapLifecycle({
        status: "empty",
        message: "",
        lastSuccessAt: null,
        dataTimestamp: null,
      });
      return;
    }

    if (!silent) {
      setMapLifecycle((prev) => Object.assign({}, prev, { status: "loading", message: "" }));
    }

    const [mapResult, hivesResult] = await Promise.allSettled([
      Keeper.api.greenhouseMap(resolvedGreenhouseId),
      Keeper.api.hives(resolvedGreenhouseId),
    ]);
    if (seq !== requestSeqRef.current) return;

    const fallbackHives = (DATA?.hives?.[resolvedGreenhouseId] || []);
    const mapValue = mapResult.status === "fulfilled" && mapResult.value && typeof mapResult.value === "object"
      ? mapResult.value
      : MAP_EMPTY_PAYLOAD;
    const safeHives = hivesResult.status === "fulfilled" && Array.isArray(hivesResult.value)
      ? hivesResult.value
      : fallbackHives;
    const normalizedMap = {
      greenhouse_id: mapValue.greenhouse_id || resolvedGreenhouseId,
      blueprint_url: mapValue.blueprint_url || null,
      zones: Array.isArray(mapValue.zones) ? mapValue.zones : [],
      positions: Array.isArray(mapValue.positions) ? mapValue.positions : [],
      device_placements: Array.isArray(mapValue.device_placements) ? mapValue.device_placements : [],
    };
    const hasRenderableData =
      normalizedMap.zones.length > 0 ||
      normalizedMap.positions.length > 0 ||
      normalizedMap.device_placements.length > 0 ||
      safeHives.length > 0;
    const firstError =
      mapResult.status === "rejected"
        ? mapResult.reason
        : (hivesResult.status === "rejected" ? hivesResult.reason : null);

    if (firstError && lastSnapshotRef.current) {
      setMapPayload(lastSnapshotRef.current.mapPayload);
      setHives(lastSnapshotRef.current.hives);
      setMapLifecycle({
        status: "stale",
        message: "Не удалось обновить карту. Показаны последние загруженные данные.",
        lastSuccessAt: lastSnapshotRef.current.lastSuccessAt || null,
        dataTimestamp: lastSnapshotRef.current.dataTimestamp || null,
      });
      return;
    }

    if (firstError && !hasRenderableData) {
      const fallbackMessage = "Не удалось загрузить карту теплицы. Повторите попытку.";
      const formatted = typeof Keeper.formatApiFailure === "function"
        ? Keeper.formatApiFailure(firstError, { fallbackMessage })
        : fallbackMessage;
      setMapPayload(MAP_EMPTY_PAYLOAD);
      setHives([]);
      setMapLifecycle({
        status: "error",
        message: formatted,
        lastSuccessAt: null,
        dataTimestamp: null,
      });
      return;
    }

    const refreshedAt = new Date().toISOString();
    const latestDataTs = pickLatestMapTimestamp(normalizedMap, safeHives, refreshedAt);
    const staleByAge = hasRenderableData && isStaleTimestamp(latestDataTs);
    const status = hasRenderableData ? (staleByAge || !!firstError ? "stale" : "ready") : "empty";
    const message = staleByAge
      ? `Данные карты могли устареть (${formatAge(latestDataTs)} назад).`
      : (firstError ? "Часть данных недоступна. Показан резервный срез." : "");

    setMapPayload(normalizedMap);
    setHives(safeHives);
    setMapLifecycle({
      status,
      message,
      lastSuccessAt: refreshedAt,
      dataTimestamp: latestDataTs,
    });
    if (status !== "empty") {
      lastSnapshotRef.current = {
        mapPayload: normalizedMap,
        hives: safeHives,
        lastSuccessAt: refreshedAt,
        dataTimestamp: latestDataTs,
      };
    }
  }, [resolvedGreenhouseId]);

  React.useEffect(() => {
    loadMapData();
  }, [loadMapData]);

  React.useEffect(() => {
    if (!resolvedGreenhouseId) return undefined;
    const timer = setInterval(() => {
      loadMapData({ silent: true });
    }, MAP_REFRESH_MS);
    return () => clearInterval(timer);
  }, [resolvedGreenhouseId, loadMapData]);

  const zones = Array.isArray(mapPayload.zones) ? mapPayload.zones : [];
  const positions = (Array.isArray(mapPayload.positions) && mapPayload.positions.length)
    ? mapPayload.positions
    : buildFallbackPositions(zones);
  const zoneById = new Map(zones.map((item) => [normalizeId(item.id), item]));
  const positionedZones = positions
    .map((position) => {
      const zoneId = normalizeId(position.zone_id);
      if (!zoneId) return null;
      const zoneItem = zoneById.get(zoneId) || { id: zoneId, name: zoneId, hive_count: 0, device_count: 0, status: "ok" };
      return {
        id: zoneId,
        x: Number(position.x) || 0,
        y: Number(position.y) || 0,
        width: Number(position.width) || 1,
        height: Number(position.height) || 1,
        name: zoneItem.name || zoneItem.row || zoneItem.row_label || zoneId,
        row: zoneItem.row || zoneItem.row_label || null,
        status: zoneItem.status || "ok",
        triState: statusToTri(zoneItem.status || zoneItem.state || "ok"),
        hive_count: Number(zoneItem.hive_count ?? zoneItem.hives_count ?? 0),
        device_count: Number(zoneItem.device_count ?? zoneItem.devices_count ?? 0),
      };
    })
    .filter(Boolean);

  const hivesByZone = hives.reduce((acc, hive) => {
    const zoneId = normalizeId(hive.zone_id || hive.zoneId);
    if (!zoneId) return acc;
    if (!acc.has(zoneId)) acc.set(zoneId, []);
    acc.get(zoneId).push(hive);
    return acc;
  }, new Map());

  const hivePlacements = buildHivePlacements(positions, hivesByZone);
  const devicePlacements = buildDevicePlacements(mapPayload.device_placements);
  const totalDevices = positionedZones.reduce((sum, item) => sum + (item.device_count || 0), 0);
  const totalHives = hives.length || positionedZones.reduce((sum, item) => sum + (item.hive_count || 0), 0);
  const mapTitle = greenhouse?.name || "Теплица № 1 «Север»";
  const mapSubtitle = zone
    ? `${zone.id || "Зона"} · ${zone.name || zone.row || zone.row_label || "контекст из маршрута"}`
    : `${totalHives} ульев · ${totalDevices} датчиков · ${positionedZones.length} зон`;
  const isLoading = mapLifecycle.status === "loading";
  const isError = mapLifecycle.status === "error";
  const isStale = mapLifecycle.status === "stale";
  const showEmpty = mapLifecycle.status === "empty" || (!isLoading && !isError && positionedZones.length === 0);
  const hasMapVisuals = positionedZones.length > 0 || hivePlacements.length > 0 || devicePlacements.length > 0;

  return (
    <div className="page">
      <div className="page-head">
        <div>
          <div className="page-title">{mapTitle}</div>
          <div className="page-sub">{mapSubtitle}</div>
        </div>
        <div className="actions">
          <button className="btn btn--secondary"><i className="ri-zoom-in-line"></i></button>
          <button className="btn btn--secondary"><i className="ri-filter-line"></i>Фильтр</button>
          <button className="btn btn--dark"><i className="ri-lock-line"></i>Закрыть всё</button>
          <button className="btn btn--primary"><i className="ri-add-line"></i>Правило для зоны</button>
          <button className="btn btn--primary"><i className="ri-add-line"></i>Правило для зоны</button>
        </div>
      </div>

      <div className="tab-bar">
        <button className="tab active">Схема</button>
        <button className="tab">Ульи ({totalHives})</button>
        <button className="tab">Устройства ({totalDevices})</button>
        <button className="tab">Зоны ({positionedZones.length})</button>
        <button className="tab">История</button>
      </div>

      <div className="gh-layout">
        <div>
          {isStale && hasMapVisuals && (
            <div className="banner gh-map-banner">
              <i className="ri-time-line"></i>
              {mapLifecycle.message || "Показаны последние известные данные карты"}
              {(mapLifecycle.dataTimestamp || mapLifecycle.lastSuccessAt) && (
                <span className="gh-map-banner__time">
                  Обновлено: {formatClock(mapLifecycle.dataTimestamp || mapLifecycle.lastSuccessAt)}
                </span>
              )}
            </div>
          )}
          <div className="gh-canvas">
            {mapPayload.blueprint_url && (
              <img
                src={mapPayload.blueprint_url}
                alt="Схема теплицы"
                style={{
                  position: "absolute",
                  inset: 0,
                  width: "100%",
                  height: "100%",
                  objectFit: "cover",
                  opacity: 0.15,
                  pointerEvents: "none",
                  zIndex: 0,
                }}
              />
            )}
            <div className="gh-hull"></div>

            {positionedZones.map((z) => {
              const rowCount = Math.max(6, Math.min(42, Math.round((z.width * 44) + (z.height * 28))));
              return (
                <Zone
                  key={z.id}
                  zone={z}
                  rowCount={rowCount}
                  isSelected={normalizeId(selectedZoneId) === normalizeId(z.id)}
                  onClick={onDrillZone ? (zoneItem) => onDrillZone({
                    greenhouseId: resolvedGreenhouseId,
                    zoneId: zoneItem.id,
                  }) : null}
                />
              );
            })}

            {hivePlacements.map((h) => (
              <div
                key={h.id}
                className={"gh-hive tri tri-" + h.triState + (normalizeId(selectedHive) === normalizeId(h.id) ? " selected" : "")}
                style={{ left: toPercent(h.x), top: toPercent(h.y) }}
                onClick={(e) => {
                  e.stopPropagation();
                  if (onDrillHive) {
                    onDrillHive({
                      greenhouseId: resolvedGreenhouseId,
                      zoneId: h.zoneId,
                      hiveId: h.id,
                    });
                    return;
                  }
                  if (onSelectHive) onSelectHive(h.id);
                }}
                title={`Улей ${h.num}`}
              >
                <span className="num">{h.num}</span>
              </div>
            ))}

            {devicePlacements.map((d) => (
              <div
                key={d.id}
                className={"gh-device tri tri-" + d.triState}
                style={{ left: toPercent(d.x), top: toPercent(d.y) }}
                onClick={(e) => {
                  e.stopPropagation();
                  if (onDrillDevice) {
                    onDrillDevice({
                      greenhouseId: resolvedGreenhouseId,
                      zoneId: d.zoneId,
                      deviceId: d.id,
                    });
                  }
                }}
                title={`Устройство ${d.id}`}
              >
                <i className="ri-radio-button-line"></i>
              </div>
            ))}

            <div className="gh-compass"><span>С</span></div>

            <div className="gh-scale">
              <div className="bar"><i></i><i></i><i></i><i></i></div>
              <span>0 ────────── 20 м</span>
            </div>

            {isLoading && (
              <div style={{
                position: "absolute",
                inset: "18px",
                display: "grid",
                placeItems: "center",
                zIndex: 3,
                color: "var(--fg-muted)",
                fontSize: 13,
                textAlign: "center",
                background: "rgba(255,255,255,0.72)",
              }}>
                Загрузка карты…
              </div>
            )}

            {!isLoading && isError && (
              <div style={{
                position: "absolute",
                inset: "18px",
                display: "grid",
                placeItems: "center",
                zIndex: 3,
                color: "#9a1f18",
                fontSize: 13,
                textAlign: "center",
                background: "rgba(252,226,224,0.96)",
                border: "1px solid #F2C1BC",
                borderRadius: 8,
                gap: 10,
              }}>
                <div>{mapLifecycle.message || "Не удалось загрузить карту теплицы"}</div>
                <button className="btn btn--secondary btn--sm" onClick={() => loadMapData()} type="button">
                  Повторить
                </button>
              </div>
            )}

            {!isLoading && !isError && showEmpty && (
              <div style={{
                position: "absolute",
                inset: "18px",
                display: "grid",
                placeItems: "center",
                zIndex: 3,
                color: "var(--fg-muted)",
                fontSize: 13,
                textAlign: "center",
                background: "rgba(255,255,255,0.72)",
              }}>
                Для выбранной теплицы нет данных карты
              </div>
            )}
          </div>

          <div className="gh-map-footer">
            <div className="legend">
              {STATUS_META.map((item) => (
                <span key={item.key}><span className="d" style={{ background: item.color }}></span>{item.label}</span>
              ))}
            </div>
            <div className="gh-map-status-note">
              {isLoading && "Загрузка карты…"}
              {!isLoading && isError && "Ошибка загрузки карты"}
              {!isLoading && !isError && showEmpty && "Карта загружена, но данных для отображения пока нет"}
              {!isLoading && !isError && !showEmpty && isStale && "Показаны устаревшие/резервные данные карты"}
              {!isLoading && !isError && !showEmpty && !isStale && "Легенда статусов общая для зон, устройств и ульев"}
            </div>
          </div>
        </div>
        <aside>
          {/* [TASK_START:T016] */}
          {RecommendationsPanel ? (
            <RecommendationsPanel greenhouseId={resolvedGreenhouseId}/>
          ) : (
            <section className="reco-panel">
              <div className="reco-panel__head"><h3>Recommendations</h3></div>
              <div className="reco-panel__note">Панель рекомендаций недоступна</div>
            </section>
          )}
          {/* [TASK_COMPLETE:T016] */}
        </aside>
      </div>
    </div>
  );
}

window.GreenhouseMap = GreenhouseMap;
