// server.js
// Node 18+
// Требуется: npm i ws
//
// MAIN:   http://localhost:65100/
// STREAM: http://localhost:65100/stream
//
// ЛОГИКА:
// - WS state (ws://HOST:4317) определяет режим: show / blank / theme / desktop
// - active = (mode === "show")
// - Если active=false ИЛИ live-item.type !== 1 (не текст) => показываем PNG-заставку (cover + затемнение)
// - Для /stream: если active=false ИЛИ live-item.type !== 1 => просто "пусто" (ничего не показываем)
// - Если active=true и type==1 => основной экран: bible/song как раньше, stream: ваша плашка (stream.js)

const http = require("http");
const fs = require("fs");
const path = require("path");
const WS = require("ws");

const { renderSong } = require("./song");
const { renderBible, getBibleBgRoute } = require("./bible");

// ================== CONFIG ==================
const PORT = Number(process.env.PORT || 65100);

const OPENLP_HOST = process.env.OPENLP_HOST || "192.168.50.39";
const HTTP_PORT = Number(process.env.OPENLP_HTTP_PORT || 4316);
const WS_PORT = Number(process.env.OPENLP_WS_PORT || 4317);

const HTTP_BASE = `http://${OPENLP_HOST}:${HTTP_PORT}/api/v2`;

const OPENLP_SERVICE_ITEMS =
  process.env.OPENLP_SERVICE_ITEMS || "/service/items";
const OPENLP_LIVE_ITEM =
  process.env.OPENLP_LIVE_ITEM || "/controller/live-item";

const POLL_MS = Number(process.env.OPENLP_POLL_MS || 200);

// PNG в корне проекта (рядом с server.js) для заставки, когда "не show" ИЛИ "не текст"
const FALLBACK_BG_FILE = process.env.FALLBACK_BG_FILE || "inactive_bg.png";
const FALLBACK_DARKEN = Number(process.env.FALLBACK_DARKEN || 0.45); // 0..1
// ===========================================

function mimeByExt(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  if (ext === ".png") return "image/png";
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
  if (ext === ".webp") return "image/webp";
  return "application/octet-stream";
}

async function fetchJson(url) {
  const res = await fetch(url, { headers: { Accept: "application/json" } });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

function detectKind(item) {
  if (item?.name === "songs") return "song";
  if (item?.name === "bibles") return "bible";
  return "unknown";
}

function getSelectedSlide(item) {
  const slides = Array.isArray(item?.slides) ? item.slides : [];
  return slides.find((s) => s?.selected) || slides[0] || null;
}

function parseSelectedFromItems(serviceItemsJson) {
  const arr = Array.isArray(serviceItemsJson) ? serviceItemsJson : [];
  const selected = arr.find((x) => x && x.selected === true);
  return {
    hasSelected: Boolean(selected),
    title: selected?.title ?? "",
  };
}

// ============ WS STATE ============
const wsState = {
  mode: "show", // show | blank | theme | desktop
  item: "",
  counter: 0,
  connected: false,
};

function modeFromState(state) {
  // как у тебя в Python
  if (state?.blank) return "blank";
  if (state?.theme) return "theme";
  if (state?.display) return "desktop";
  return "show";
}

function applyWsPayload(obj) {
  const state =
    obj && obj.results && typeof obj.results === "object" ? obj.results : obj;

  const mode = modeFromState(state);
  const item = state?.item ? String(state.item) : "";
  const counter = Number(state?.counter || 0);

  if (
    wsState.mode !== mode ||
    wsState.item !== item ||
    wsState.counter !== counter
  ) {
    wsState.mode = mode;
    wsState.item = item;
    wsState.counter = counter;
    console.log(`[WS] mode=${mode} item=${item} counter=${counter}`);
  }
}

function startWs() {
  const url = `ws://${OPENLP_HOST}:${WS_PORT}`;
  console.log(`[WS] connecting ${url}`);

  const ws = new WS(url, {
    headers: { Origin: `http://${OPENLP_HOST}:${HTTP_PORT}` },
  });

  ws.on("open", () => {
    wsState.connected = true;
    console.log("[WS] connected");
  });

  ws.on("message", (data) => {
    const s = Buffer.isBuffer(data)
      ? data.toString("utf8")
      : String(data || "");
    if (!s) return;
    try {
      applyWsPayload(JSON.parse(s));
    } catch {
      // OpenLP 4317 обычно JSON, но если нет — просто игнор
    }
  });

  ws.on("error", (err) => {
    console.log(`[WS] error: ${err?.message || err}`);
  });

  ws.on("close", () => {
    wsState.connected = false;
    console.log("[WS] closed -> reconnect in 2s");
    setTimeout(startWs, 2000);
  });
}

startWs();

// ============ FALLBACK SCREEN (PNG) ============
function renderFallbackPngScreen() {
  return {
    kind: "fallback",
    meta: {},
    html: `
<style>
  .fallback-root{
    width:3840px;height:2160px;
    background: url("/fallback-bg") center center / cover no-repeat;
    position:relative;
  }
  .fallback-root::before{
    content:"";
    position:absolute; inset:0;
    background: rgba(0,0,0,${FALLBACK_DARKEN});
  }
</style>
<div class="fallback-root"></div>
`,
  };
}

function buildMainScreen(payload) {
  // если не show -> заставка
  if (!payload.active) return renderFallbackPngScreen();
  // если не текст (например, image/video/command) -> заставка
  if (!payload.isText) return renderFallbackPngScreen();

  if (payload.kind === "song") return renderSong(payload);
  if (payload.kind === "bible") return renderBible(payload);

  return renderFallbackPngScreen();
}

// ============ SSE ============
const clients = new Set();
function sseSend(res, event, dataObj) {
  res.write(`event: ${event}\n`);
  res.write(`data: ${JSON.stringify(dataObj)}\n\n`);
}

let lastKey = "";
let lastMainScreen = { kind: "fallback", html: "", meta: {} };
let lastPayload = null;

// ============ Pages ============
function mainPageHtml() {
  return `<!doctype html><html lang="ru"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=3840,height=2160,initial-scale=1" />
<style>html,body{margin:0;width:3840px;height:2160px;overflow:hidden;background:#000;font-family:Roboto,Arial,sans-serif}</style>
</head><body><div id="app"></div>
<script>
const app=document.getElementById("app");
const es=new EventSource("/events");

function fitBible(){
  const root=app.querySelector('[data-kind="bible"]'); if(!root) return;
  const t=app.querySelector("#bibleText"); const w=app.querySelector("#bibleWindow");
  if(!t||!w) return;
  let font=Number(root.dataset.baseFont||240);
  const min=Number(root.dataset.minFont||90);
  const step=Number(root.dataset.step||4);
  t.style.fontSize=font+"px";
  let guard=0;
  while((t.scrollHeight>w.clientHeight||t.scrollWidth>w.clientWidth)&&font>min){
    font-=step; t.style.fontSize=font+"px"; if(++guard>500) break;
  }
}
function fitSong(){
  const root=app.querySelector('[data-kind="song"]'); if(!root) return;
  const t=app.querySelector("#songText"); const w=app.querySelector("#songWindow");
  if(!t||!w) return;
  let font=Number(root.dataset.baseFont||200);
  const min=Number(root.dataset.minFont||70);
  const step=Number(root.dataset.step||3);
  t.style.fontSize=font+"px";
  let guard=0;
  while((t.scrollHeight>w.clientHeight||t.scrollWidth>w.clientWidth)&&font>min){
    font-=step; t.style.fontSize=font+"px"; if(++guard>500) break;
  }
}

es.addEventListener("screen",(ev)=>{
  const s=JSON.parse(ev.data);
  app.innerHTML=s.html||"";
  fitBible(); fitSong();
});
</script></body></html>`;
}

function streamPageHtml() {
  // Страница прозрачная, stream.js решает что показать.
  return `<!doctype html><html lang="ru"><head>
<meta charset="utf-8" /><meta name="viewport" content="width=3840,height=2160,initial-scale=1" />
<style>html,body{margin:0;width:3840px;height:2160px;overflow:hidden;background:transparent}</style>
</head><body><div id="root"></div><script src="/stream.js"></script></body></html>`;
}

// ============ HTTP SERVER ============
const server = http.createServer((req, res) => {
  // bible background route from bible.js
  const bg = getBibleBgRoute(req, res);
  if (bg === true) return;

  // fallback bg (PNG)
  if (req.url === "/fallback-bg") {
    const p = path.join(__dirname, FALLBACK_BG_FILE);
    if (!fs.existsSync(p)) {
      res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
      res.end(`Нет файла ${FALLBACK_BG_FILE} рядом с server.js`);
      return;
    }
    res.writeHead(200, {
      "Content-Type": mimeByExt(p),
      "Cache-Control": "no-cache",
    });
    res.end(fs.readFileSync(p));
    return;
  }

  if (req.url === "/stream.js") {
    const p = path.join(__dirname, "stream.js");
    if (!fs.existsSync(p)) {
      res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
      res.end("Нет файла stream.js рядом с server.js");
      return;
    }
    res.writeHead(200, {
      "Content-Type": "application/javascript; charset=utf-8",
      "Cache-Control": "no-cache",
    });
    res.end(fs.readFileSync(p));
    return;
  }

  if (req.url === "/events") {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "Access-Control-Allow-Origin": "*",
    });
    res.write("\n");
    clients.add(res);

    // отдадим текущее состояние
    sseSend(res, "screen", lastMainScreen);
    if (lastPayload) sseSend(res, "live", lastPayload);

    req.on("close", () => clients.delete(res));
    return;
  }

  if (req.url === "/") {
    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(mainPageHtml());
    return;
  }

  if (req.url === "/stream") {
    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(streamPageHtml());
    return;
  }

  res.writeHead(404);
  res.end("Not found");
});

server.listen(PORT, () => {
  console.log(`Main:   http://localhost:${PORT}/`);
  console.log(`Stream: http://localhost:${PORT}/stream`);
  console.log(`HTTP live-item: ${HTTP_BASE}${OPENLP_LIVE_ITEM}`);
  console.log(`HTTP items:     ${HTTP_BASE}${OPENLP_SERVICE_ITEMS}`);
  console.log(`WS state:       ws://${OPENLP_HOST}:${WS_PORT}`);
  console.log(`Fallback BG:    ${FALLBACK_BG_FILE} (route /fallback-bg)`);
});

// ============ POLL LOOP ============
async function tick() {
  // 1) selected title (для логов)
  let selectedTitle = "-";
  try {
    const items = await fetchJson(`${HTTP_BASE}${OPENLP_SERVICE_ITEMS}`);
    const sel = parseSelectedFromItems(items);
    selectedTitle = sel.hasSelected ? sel.title : "-";
  } catch {}

  // 2) live-item
  const liveItem = await fetchJson(`${HTTP_BASE}${OPENLP_LIVE_ITEM}`);

  // live-item.type: 1 = text (как у тебя в Python live_type=1)
  const liveType = typeof liveItem?.type === "number" ? liveItem.type : null;
  const isText = liveType === 1;

  const kind = detectKind(liveItem);
  const slide = getSelectedSlide(liveItem);

  const wsMode = wsState.mode || "show";
  const active = wsMode === "show";

  const payload = {
    // для stream.js
    active, // false => stream скрывается
    kind,
    isText, // false => stream скрывается (изображение/команда)

    // дебаг
    wsMode,
    wsItem: wsState.item,
    selectedTitle,
    liveType,

    // контент
    title: liveItem?.title ?? "",
    footer: Array.isArray(liveItem?.footer) ? liveItem.footer : [],
    slideTag: slide?.tag ?? "",
    html: String(slide?.html ?? liveItem?.html ?? "").trim(),
    text: String(slide?.text ?? liveItem?.text ?? "").trim(),

    ts: new Date().toISOString(),
  };

  // 3) лог при смене режима/типа
  const key = JSON.stringify([
    payload.active,
    payload.wsMode,
    payload.isText,
    payload.kind,
    payload.title,
    payload.slideTag,
  ]);
  if (key !== lastKey) {
    const mainMode =
      payload.active && payload.isText ? payload.kind : "fallback";
    console.log(
      `[${payload.ts}] MAIN=${mainMode} wsMode=${payload.wsMode} liveType=${payload.liveType} selected="${payload.selectedTitle}"`,
    );
    lastKey = key;
  }

  // 4) main screen
  const screen = buildMainScreen(payload);
  lastMainScreen = screen;
  lastPayload = payload;

  // 5) push to clients
  for (const c of clients) {
    sseSend(c, "screen", screen);
    sseSend(c, "live", payload);
  }
}

setInterval(
  () => tick().catch((e) => console.error("poll error:", e.message)),
  POLL_MS,
);
tick().catch((e) => console.error("start error:", e.message));
