/* global React, CR_DATA, CR_UI, CR_API */
const { useState, useEffect, useRef, useCallback } = React;
const { Icons, Btn, formatRelative } = CR_UI;

// BRAND is passed as a prop from app.jsx — no hardcode here

const JOB_KIND_LABEL = {
  find_topics:    "find_topics",
  deep_research:  "deep_research",
  refine_article: "refine_article",
  generate_post:  "generate_post",
  bootstrap_brand:"bootstrap_brand",
};

// tag parse: "OK   line text" → { tag: "ok", text: "line text" }
// tags in stdout: "OK  " / "WARN" / "ERR " / "INFO" / "TOOL"
const TAG_RE = /^(OK  |WARN|ERR |INFO|TOOL)\s/;
const TAG_CLASS = { "INFO": "info", "TOOL": "tool", "OK  ": "ok", "ERR ": "err", "WARN": "warn" };

function parseLine(raw) {
  const m = TAG_RE.exec(raw);
  if (m) {
    const tag = m[1];
    return { tag, tagClass: TAG_CLASS[tag] || "info", text: raw.slice(m[0].length) };
  }
  return { tag: "INFO", tagClass: "info", text: raw };
}

function fmtTime(iso) {
  if (!iso) return "—";
  try {
    return new Date(iso).toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", second: "2-digit" });
  } catch (_) { return "—"; }
}

function fmtDuration(startedAt, finishedAt) {
  if (!startedAt) return "—";
  const end = finishedAt ? new Date(finishedAt) : new Date();
  const sec = Math.round((end - new Date(startedAt)) / 1000);
  if (sec < 60) return `${sec}с`;
  const m = Math.floor(sec / 60), s = sec % 60;
  return `${m}:${String(s).padStart(2, "0")}`;
}

// Live version — uses an externally ticking `nowMs` so it re-renders each second.
function fmtDurationLive(startedAt, nowMs) {
  if (!startedAt) return "—";
  const sec = Math.round((nowMs - Date.parse(startedAt)) / 1000);
  if (sec < 0) return "0с";
  if (sec < 60) return `${sec}с`;
  const m = Math.floor(sec / 60), s = sec % 60;
  return `${m}:${String(s).padStart(2, "0")}`;
}

// Extract "N/M" progress from last log line
function extractProgress(lines) {
  for (let i = lines.length - 1; i >= 0; i--) {
    const m = /(\d+)\/(\d+)/.exec(lines[i]);
    if (m) return { current: Number(m[1]), total: Number(m[2]) };
  }
  return null;
}

// STATUS icon — running→spinner, ok→check, failed/timeout/killed→X, else null
const StatusIcon = ({ status }) => {
  if (status === "running" || status === "queued")
    return <span style={{ color: "var(--status-info-fg)" }}><Icons.Spinner size={13} /></span>;
  if (status === "ok")
    return <span style={{ color: "var(--status-approved-fg)" }}><Icons.Check size={13} /></span>;
  if (status === "failed" || status === "timeout" || status === "killed")
    return <span style={{ color: "var(--status-error-fg)" }}><Icons.X size={13} /></span>;
  return null;
};

// ---- WebSocket manager hook ----
// Opens a WS per running job, fans lines into linesMap[id].
// Reconnects up to 3 times on unexpected close.
//
// Two effects on purpose:
//   1) jobs-driven effect — opens new WSs for running jobs, closes WSs for
//      jobs that flipped to a terminal status. NO cleanup return — otherwise
//      every polling tick (5s) closes-and-reopens every WS, churning sockets
//      and duplicating tail replays from the backend.
//   2) unmount-only effect — closes everything in socketsRef on component
//      teardown. Runs once.
function useJobStreams(jobs, linesMap, setLinesMap) {
  const socketsRef = useRef({}); // id → { ws, retries, dead }

  // (1) jobs-driven effect
  useEffect(() => {
    const runningIds = new Set(
      jobs.filter((j) => j.status === "running" || j.status === "queued").map((j) => j.id)
    );

    // open missing
    runningIds.forEach((id) => {
      if (socketsRef.current[id]) return;
      openSocket(id);
    });

    // close sockets for jobs no longer running (terminal status flipped)
    Object.keys(socketsRef.current).forEach((id) => {
      if (!runningIds.has(id)) {
        const entry = socketsRef.current[id];
        if (entry) {
          entry.dead = true;
          if (entry.ws.readyState === WebSocket.OPEN || entry.ws.readyState === WebSocket.CONNECTING) {
            entry.ws.close();
          }
          delete socketsRef.current[id];
        }
      }
    });

    function openSocket(id) {
      const entry = socketsRef.current[id] || { retries: 0, dead: false };
      socketsRef.current[id] = entry;

      const ws = CR_API.jobLogSocket(id); // includes ?token= when CR_AUTH_TOKEN is set
      entry.ws = ws;

      ws.onmessage = (ev) => {
        try {
          const msg = JSON.parse(ev.data);
          if (msg.type === "line") {
            setLinesMap((prev) => {
              const cur = prev[id] || [];
              return { ...prev, [id]: [...cur, msg.line] };
            });
          }
          // "done" message — polling will pick up status change
        } catch (_) {}
      };

      ws.onclose = () => {
        if (entry.dead) return;
        if (entry.retries < 3) {
          entry.retries += 1;
          setTimeout(() => {
            if (!entry.dead) openSocket(id);
          }, 2000);
        }
      };

      ws.onerror = () => { /* handled in onclose */ };
    }
    // No cleanup return — see comment above.
  }, [jobs]);

  // (2) unmount-only effect
  useEffect(() => {
    return () => {
      Object.keys(socketsRef.current).forEach((id) => {
        const entry = socketsRef.current[id];
        if (entry) {
          entry.dead = true;
          if (entry.ws && (entry.ws.readyState === WebSocket.OPEN || entry.ws.readyState === WebSocket.CONNECTING)) {
            entry.ws.close();
          }
        }
      });
      socketsRef.current = {};
    };
  }, []);
}

// Russian phase labels for bootstrap_brand jobs (from .bootstrap/status.json)
const BOOTSTRAP_PHASE_LABEL = {
  downloading: "Скачивание постов из Telegram",
  downloaded:  "Посты скачаны, ожидание Claude…",
  analyzing:   "Claude анализирует канал и пишет конфиг",
  analyzed:    "Готово",
  failed:      "Ошибка",
};

// ---- Main component ----
const JobLog = ({ open, onToggle, brand }) => {
  const [jobs, setJobs] = useState([]);
  const [linesMap, setLinesMap] = useState({}); // id → string[]
  const [activeId, setActiveId] = useState(null);
  const [killing, setKilling] = useState(false);
  const [now, setNow] = useState(() => Date.now());           // ticks each sec for live duration
  const [bootstrapStatus, setBootstrapStatus] = useState(null); // {phase, downloaded, channel, ...}
  const streamRef = useRef(null);

  // 1Hz tick for live duration display when there's at least one running job
  useEffect(() => {
    const hasRunning = jobs.some((j) => j.status === "running" || j.status === "queued");
    if (!hasRunning) return;
    const t = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(t);
  }, [jobs]);

  // Poll bootstrap-status every 3s for live phase/downloaded display
  // Only when there's an active bootstrap_brand job in progress
  useEffect(() => {
    const hasBootstrap = jobs.some(
      (j) => j.kind === "bootstrap_brand" && (j.status === "running" || j.status === "queued"),
    );
    if (!hasBootstrap) {
      setBootstrapStatus(null);
      return;
    }
    let cancelled = false;
    async function load() {
      try {
        const s = await CR_API.telegramStatus(brand);
        if (!cancelled) setBootstrapStatus(s);
      } catch (_) {}
    }
    load();
    const t = setInterval(load, 3000);
    return () => { cancelled = true; clearInterval(t); };
  }, [jobs, brand]);

  // Polling: fetch job list every 5s; restart when brand changes
  useEffect(() => {
    if (!brand) return;
    let cancelled = false;

    async function poll() {
      try {
        const data = await CR_API.listJobs(brand);
        if (!cancelled) {
          setJobs(data);
          // Pre-populate linesMap with tail from existing jobs (for late-join)
          // The WS will stream new lines; existing tail comes from the backend response.
          // JobState has `tail: string[]` field.
          setLinesMap((prev) => {
            const next = { ...prev };
            data.forEach((j) => {
              if (!next[j.id] && j.tail && j.tail.length) {
                next[j.id] = j.tail;
              }
            });
            return next;
          });
        }
      } catch (_) {
        // backend offline — keep whatever we have
      }
    }

    poll();
    const interval = setInterval(poll, 5000);
    return () => { cancelled = true; clearInterval(interval); };
  }, [brand]);

  // Auto-select most-relevant job when list changes.
  // Priority: keep current selection if still in list → first running job →
  // first job in list (which is now the newest, after backend sort fix).
  useEffect(() => {
    if (jobs.length === 0) { setActiveId(null); return; }
    setActiveId((prev) => {
      if (prev && jobs.find((j) => j.id === prev)) return prev;
      const running = jobs.find((j) => j.status === "running" || j.status === "queued");
      if (running) return running.id;
      return jobs[0].id;
    });
  }, [jobs]);

  // Auto-scroll stream pane on new lines
  useEffect(() => {
    if (open && streamRef.current) {
      streamRef.current.scrollTop = streamRef.current.scrollHeight;
    }
  }, [open, activeId, linesMap]);

  // Manage WS connections
  useJobStreams(jobs, linesMap, setLinesMap);

  const active = jobs.find((j) => j.id === activeId);
  const activeLines = (activeId && linesMap[activeId]) || [];
  const runningJobs = jobs.filter((j) => j.status === "running" || j.status === "queued");
  const activeCount = runningJobs.length;

  // Bar: progress hint from last running job
  const barActiveJob = runningJobs[0];
  const barLines = barActiveJob ? (linesMap[barActiveJob.id] || []) : [];
  const barProgress = barActiveJob ? extractProgress(barLines) : null;

  const handleKill = useCallback(async () => {
    if (!active || active.status !== "running") return;
    setKilling(true);
    try {
      await CR_API.killJob(active.id);
      // polling will update status
    } catch (_) {}
    setKilling(false);
  }, [active]);

  return (
    <div className={"joblog " + (open ? "joblog--expanded" : "joblog--collapsed")}>
      <div className="joblog__bar" onClick={onToggle}>
        <span className="joblog__bar-stat">
          <span className={"dot " + (activeCount ? "dot--info dot--pulse" : "")} style={{ color: "var(--status-info-fg)" }}></span>
          <span className={activeCount ? "info" : ""}>{activeCount} активных</span>
        </span>
        <span className="joblog__bar-stat"><span style={{ opacity: 0.5 }}>·</span> {jobs.length} недавних</span>
        {activeCount > 0 && !open && barActiveJob && (
          <span className="joblog__bar-active">
            <Icons.Spinner size={12} />
            <span style={{ fontSize: 12, fontFamily: "JetBrains Mono, monospace" }}>
              {JOB_KIND_LABEL[barActiveJob.kind] || barActiveJob.kind}
              {barProgress ? ` — ${barProgress.current}/${barProgress.total}` : ""}
            </span>
          </span>
        )}
        <div className="joblog__bar-spacer" />
        <span className="joblog__bar-toggle">
          Журнал {open ? <Icons.ChevronDown size={12} /> : <Icons.ChevronUp size={12} />}
        </span>
      </div>

      {open && (
        <div className="joblog__body">
          {jobs.length === 0 ? (
            <div style={{ display: "flex", alignItems: "center", justifyContent: "center", flex: 1, color: "var(--fg-muted)", fontSize: 13 }}>
              Нет недавних запусков
            </div>
          ) : (
            <>
              <div className="joblog__list">
                {jobs.map((j) => {
                  const jLines = linesMap[j.id] || [];
                  const jProgress = (j.status === "running" || j.status === "queued") ? extractProgress(jLines) : null;
                  const isError = j.status === "failed" || j.status === "timeout" || j.status === "killed";
                  return (
                    <div key={j.id}
                      className={"joblog__job " + (activeId === j.id ? "is-active" : "")}
                      onClick={() => setActiveId(j.id)}>
                      <div className="joblog__job-icon"><StatusIcon status={j.status} /></div>
                      <div style={{ flex: 1, minWidth: 0 }}>
                        <div className="joblog__job-title">{JOB_KIND_LABEL[j.kind] || j.kind}</div>
                        <div className="joblog__job-sub">
                          {j.brand} · {(j.status === "running" || j.status === "queued")
                            ? `работает ${fmtDurationLive(j.started_at, now)}`
                            : `${fmtDuration(j.started_at, j.finished_at)} · ${formatRelative(Date.parse(j.started_at))}`}
                        </div>
                        {(j.status === "running" || j.status === "queued") && jProgress && (
                          <div className="joblog__job-tail">
                            ↳ {jProgress.current}/{jProgress.total}
                          </div>
                        )}
                        {j.status === "ok" && j.cost_tokens && (
                          <div className="joblog__job-tail" style={{ color: "var(--status-approved-fg)" }}>
                            ↳ {j.cost_tokens.toLocaleString("ru-RU")} токенов
                          </div>
                        )}
                        {isError && j.error && (
                          <div className="joblog__job-tail" style={{ color: "var(--status-error-fg)" }}>
                            ↳ {j.error}
                          </div>
                        )}
                        {isError && !j.error && j.status === "killed" && (
                          <div className="joblog__job-tail" style={{ color: "var(--status-error-fg)" }}>
                            ↳ прерван вручную
                          </div>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>

              {active && (
                <div className="joblog__detail">
                  <div className="joblog__detail-header">
                    <StatusIcon status={active.status} />
                    <span className="joblog__detail-title">{JOB_KIND_LABEL[active.kind] || active.kind}</span>
                    <span className="joblog__detail-meta">
                      {active.id} · pid {active.pid || "—"}
                      {(active.status === "running" || active.status === "queued") && active.started_at && (
                        <> · работает {fmtDurationLive(active.started_at, now)}</>
                      )}
                      {active.status !== "running" && active.status !== "queued" && active.started_at && (
                        <> · {fmtDuration(active.started_at, active.finished_at)}</>
                      )}
                    </span>
                    <div style={{ flex: 1 }} />
                    {(active.status === "running" || active.status === "queued") ? (
                      <Btn kind="danger" size="sm" icon={<Icons.Stop size={12} />} onClick={handleKill} disabled={killing}>
                        Прервать
                      </Btn>
                    ) : (
                      <Btn kind="ghost" size="sm" icon={<Icons.Refresh size={12} />}>Запустить повтор</Btn>
                    )}
                  </div>

                  {/* Bootstrap-specific live status banner: phase + downloaded count + elapsed */}
                  {active.kind === "bootstrap_brand" && (active.status === "running" || active.status === "queued") && (
                    <div className="joblog__phase-banner">
                      <Icons.Spinner size={12} />
                      <span style={{ marginLeft: 6, fontWeight: 600 }}>
                        {bootstrapStatus
                          ? (BOOTSTRAP_PHASE_LABEL[bootstrapStatus.phase] || bootstrapStatus.phase || "В работе")
                          : "В работе"}
                      </span>
                      {bootstrapStatus?.phase === "downloading" && bootstrapStatus.limit && (
                        <span style={{ marginLeft: 8, color: "var(--fg-muted)" }}>
                          {bootstrapStatus.downloaded || 0}/{bootstrapStatus.limit} постов
                        </span>
                      )}
                      {bootstrapStatus?.phase === "analyzing" && bootstrapStatus.downloaded != null && (
                        <span style={{ marginLeft: 8, color: "var(--fg-muted)" }}>
                          из {bootstrapStatus.downloaded} постов
                        </span>
                      )}
                      {/* Elapsed from heartbeat — shown even when stdout is silent */}
                      {bootstrapStatus?.elapsed_sec > 0 && (() => {
                        const sec = bootstrapStatus.elapsed_sec;
                        const m = Math.floor(sec / 60), s = sec % 60;
                        return (
                          <span style={{ marginLeft: 8, color: "var(--fg-muted)", fontFamily: "JetBrains Mono, monospace", fontSize: 11 }}>
                            {m > 0 ? `${m}м ${s}с` : `${s}с`}
                          </span>
                        );
                      })()}
                    </div>
                  )}

                  <div ref={streamRef} className="joblog__stream">
                    {activeLines.length === 0 && (
                      <div style={{ color: "var(--fg-muted)", fontStyle: "italic" }}>
                        {(active.status === "running" || active.status === "queued") ? (
                          <>
                            Ожидание первого вывода…
                            {active.started_at && (
                              <span style={{ marginLeft: 8, fontSize: 11 }}>
                                ({fmtDurationLive(active.started_at, now)} без stdout)
                              </span>
                            )}
                          </>
                        ) : (
                          "Лог не сохранён или job не оставил stdout."
                        )}
                      </div>
                    )}
                    {activeLines.map((raw, i) => {
                      const { tag, tagClass, text } = parseLine(raw);
                      // Extract timestamp prefix "HH:MM:SS " if present
                      const timeM = /^(\d{2}:\d{2}:\d{2})\s(.*)$/.exec(text);
                      const displayTime = timeM ? timeM[1] : fmtTime(active.started_at);
                      const displayText = timeM ? timeM[2] : text;
                      return (
                        <div key={i} className="joblog__line">
                          <span className="joblog__line-time">{displayTime}</span>
                          <span className={`joblog__line-tag joblog__line-tag--${tagClass}`}>{tag}</span>
                          <span className="joblog__line-text">{displayText}</span>
                        </div>
                      );
                    })}
                    {(active.status === "running" || active.status === "queued") && (
                      <div className="joblog__line">
                        <span className="joblog__line-time" style={{ color: "var(--status-info-fg)" }}>
                          {fmtTime(new Date().toISOString())}
                        </span>
                        <span className="joblog__line-tag joblog__line-tag--info">INFO</span>
                        <span className="joblog__line-text" style={{ color: "var(--status-info-fg)" }}>
                          выполняется… <span style={{ animation: "cr-spin 1s linear infinite", display: "inline-block" }}>▏</span>
                        </span>
                      </div>
                    )}
                  </div>
                </div>
              )}
            </>
          )}
        </div>
      )}
    </div>
  );
};

window.CR_JOBLOG = { JobLog };
