/* =========================================================
   CONNIE — Hint engine (S3c)

   Client-side "proactive hints" derived purely from the /web/jarvis
   snapshot (see panels-jarvis.jsx) plus the client clock. No sound, no TTS,
   no orb changes — a single dedicated banner, top-right, that shows at
   most one hint at a time.

   window.ConnieHints = {
     deriveHints,    // ({jarvis, now}) -> [{key, prio, level, text, action?}]
     pickHint,       // ({hints, state, appState, loadedAt, now}) -> hint|null
     loadHintState,  // () -> {lastShownAt, dedupe} (localStorage, keys/timestamps only)
     saveHintState,  // (state) -> void
     HintBanner,     // React component
   }

   Rules (design §4, connie-jarvis-s3-design.md):
     H1 meeting-in-Kuerze  (prio1/warn): next event 0 < t <= 30 Min.
     H2 overdue tasks      (prio2/info): N tasks due < today, max 1x/Tag.
     H3 deal follow-up     (prio3/info): deal next_activity_date >= 5 Tage
                                          in der Vergangenheit, max 1x/Tag/Deal.
     H4 (approvals) is Backlog — not built (Entscheidung §8.1).

   Anti-Nerv state lives in localStorage under "connie.hints" and holds ONLY
   rule-keys + epoch-ms timestamps — never hint text, never the meeting
   title / deal name / task title that produced the hint (SR-JV-6: those
   strings are attacker-influenced and must stay JSX text nodes only, never
   persisted or used to build hrefs).

   Everything except loadHintState/saveHintState is a pure function.
   ========================================================= */

// No hooks needed here: HintBanner is a pure presentational component and
// the derivation re-runs on every jarvisData poll (app.jsx), so there is no
// local ticking state to manage in this file.

// ---------------------------------------------------------------------------
// Anti-Nerv tuning (design §4)
// ---------------------------------------------------------------------------
const _STORAGE_KEY = "connie.hints";
const _DEDUPE_MS_DEFAULT = 4 * 60 * 60 * 1000;   // 4h general dedupe (H1 + any future rule)
const _DEDUPE_MS_DAILY = 24 * 60 * 60 * 1000;    // "max 1x/Tag" for H2 and H3 (per deal)
const _COOLDOWN_MS = 15 * 60 * 1000;             // global cooldown between hints
const _QUIET_AFTER_LOAD_MS = 10 * 1000;          // no hints in the first 10s after app load
const _STALE_BLACKOUT_MIN = 60;                  // age_min > 60 -> no hints at all
const _H1_THRESHOLD_MIN = 30;
const _H3_THRESHOLD_DAYS = 5;

function _dedupeWindowMs(key) {
  // H2 ("h2") and H3 ("h3:<dealId>") are explicitly "max 1x/Tag" in the design;
  // everything else (H1, future rules) falls back to the general 4h dedupe.
  if (key === "h2" || key.indexOf("h3:") === 0) return _DEDUPE_MS_DAILY;
  return _DEDUPE_MS_DEFAULT;
}

// ---------------------------------------------------------------------------
// localStorage (only impure functions in this file)
// ---------------------------------------------------------------------------
function loadHintState() {
  try {
    const raw = window.localStorage.getItem(_STORAGE_KEY);
    if (!raw) return { lastShownAt: 0, dedupe: {} };
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object") return { lastShownAt: 0, dedupe: {} };
    const dedupe = (parsed.dedupe && typeof parsed.dedupe === "object" && !Array.isArray(parsed.dedupe))
      ? parsed.dedupe
      : {};
    const lastShownAt = typeof parsed.lastShownAt === "number" ? parsed.lastShownAt : 0;
    return { lastShownAt, dedupe };
  } catch (_) {
    // Corrupt JSON, storage disabled (private mode), quota errors, ... -> silent default.
    return { lastShownAt: 0, dedupe: {} };
  }
}

function saveHintState(state) {
  try {
    const lastShownAt = (state && typeof state.lastShownAt === "number") ? state.lastShownAt : 0;
    const dedupe = (state && state.dedupe && typeof state.dedupe === "object") ? state.dedupe : {};
    window.localStorage.setItem(_STORAGE_KEY, JSON.stringify({ lastShownAt, dedupe }));
  } catch (_) {
    // Storage unavailable — hints just won't be deduped across reloads. No crash.
  }
}

// ---------------------------------------------------------------------------
// deriveHints — pure. Turns a /web/jarvis snapshot into candidate hints.
// Staleness-Gate lives here because it is entirely data-driven (age_min /
// stale_fields come from the snapshot itself, no persisted state needed).
// ---------------------------------------------------------------------------
function deriveHints({ jarvis, now }) {
  const _now = now instanceof Date ? now : new Date();
  if (!jarvis || jarvis.available === false) return [];

  // Total blackout when the pre-cache snapshot itself is too old to trust.
  if (typeof jarvis.age_min === "number" && jarvis.age_min > _STALE_BLACKOUT_MIN) return [];

  const staleFields = Array.isArray(jarvis.stale_fields) ? jarvis.stale_fields : [];
  const fields = jarvis.fields || {};
  const logic = (typeof window !== "undefined" && window.JarvisCardLogic) || {};
  const out = [];

  // --- H1: next meeting in 0 < t <= 30 Min -----------------------------
  const calendarStale = staleFields.indexOf("calendar_today") !== -1
    || staleFields.indexOf("calendar_tomorrow") !== -1;
  if (!calendarStale && typeof logic.nextEvent === "function") {
    const calToday = fields.calendar_today;
    const calTom = fields.calendar_tomorrow;
    const todayItems = (calToday && calToday.items) || [];
    const tomItems = (calTom && calTom.items) || [];
    const next = logic.nextEvent(todayItems, tomItems, _now);
    if (next && next.isToday) {
      const diffMin = (next.startsAt.getTime() - _now.getTime()) / 60000;
      if (diffMin > 0 && diffMin <= _H1_THRESHOLD_MIN) {
        const roundedMin = Math.max(1, Math.round(diffMin));
        const title = (next.item && typeof next.item.title === "string" && next.item.title) || "Termin";
        out.push({
          key: "h1",
          prio: 1,
          level: "warn",
          text: `Meeting "${title}" in ${roundedMin} Min - Prep bereit?`,
          action: "prep_meeting",
        });
      }
    }
  }

  // --- H2: overdue tasks, max 1x/Tag -----------------------------------
  const tasksStale = staleFields.indexOf("tasks_open") !== -1;
  if (!tasksStale && typeof logic.overdueTasks === "function") {
    const tasksField = fields.tasks_open;
    const items = (tasksField && tasksField.items) || [];
    const todayMidnight = new Date(_now);
    todayMidnight.setHours(0, 0, 0, 0);
    const overdue = logic.overdueTasks(items, todayMidnight);
    if (overdue.length > 0) {
      out.push({
        key: "h2",
        prio: 2,
        level: "info",
        text: `${overdue.length} überfällige Aufgabe${overdue.length === 1 ? "" : "n"}`,
      });
    }
  }

  // --- H3: deal follow-up >= 5 Tage in der Vergangenheit, max 1x/Tag/Deal
  const dealsStale = staleFields.indexOf("hubspot_active_deals_top5") !== -1;
  if (!dealsStale && typeof logic.parseDue === "function") {
    const dealsField = fields.hubspot_active_deals_top5;
    const items = (dealsField && dealsField.items) || [];
    const todayMidnight = new Date(_now);
    todayMidnight.setHours(0, 0, 0, 0);
    let oldest = null; // pick the single most-overdue deal (one hint slot anyway)
    for (const item of items) {
      if (!item || typeof item !== "object") continue;
      // CRM era (#40): the date slot is "closedate" (Zieltermin); HubSpot-era
      // snapshots may still carry "next_activity_date" - accept both.
      const due = logic.parseDue(item.closedate || item.next_activity_date || "");
      if (!due) continue;
      const diffDays = Math.round((todayMidnight.getTime() - due.getTime()) / 86400000);
      if (diffDays >= _H3_THRESHOLD_DAYS) {
        if (!oldest || diffDays > oldest.diffDays) oldest = { item, diffDays };
      }
    }
    if (oldest) {
      // QS-JV-4: the key is persisted to localStorage on dismiss - it must
      // never carry content (deal NAME). Id-less deals share the fallback key.
      const dealId = oldest.item.id || "unknown";
      const dealName = (typeof oldest.item.name === "string" && oldest.item.name) || "Deal";
      out.push({
        key: `h3:${dealId}`,
        prio: 3,
        level: "info",
        text: `Deal "${dealName}": Zieltermin seit ${oldest.diffDays} Tagen überfällig`,
      });
    }
  }

  return out;
}

// ---------------------------------------------------------------------------
// pickHint — pure. Applies the Anti-Nerv gates and returns at most one hint.
// ---------------------------------------------------------------------------
function pickHint({ hints, state, appState, loadedAt, now }) {
  const _now = now instanceof Date ? now.getTime() : (typeof now === "number" ? now : Date.now());
  const _loadedAt = loadedAt instanceof Date ? loadedAt.getTime() : loadedAt;

  // Ruhe-Gate: only while the app is idle/ready, and never in the first 10s.
  if (appState !== "ready") return null;
  if (typeof _loadedAt === "number" && (_now - _loadedAt) < _QUIET_AFTER_LOAD_MS) return null;

  const st = state || { lastShownAt: 0, dedupe: {} };
  const dedupe = st.dedupe || {};
  const lastShownAt = typeof st.lastShownAt === "number" ? st.lastShownAt : 0;

  const candidates = (hints || []).filter((h) => {
    if (!h || !h.key || typeof h.prio !== "number") return false;
    const lastForKey = dedupe[h.key];
    if (typeof lastForKey === "number" && (_now - lastForKey) < _dedupeWindowMs(h.key)) return false;
    // Global cooldown — H1 is exempt (design §4: time-critical).
    if (h.prio !== 1 && (_now - lastShownAt) < _COOLDOWN_MS) return false;
    return true;
  });
  if (candidates.length === 0) return null;

  candidates.sort((a, b) => a.prio - b.prio);
  return candidates[0];
}

// ---------------------------------------------------------------------------
// HintBanner — dedicated banner, top-right under the clock. NOT the system
// toast. Colors come from styles.css (.hint-banner--info / --warn) so this
// file needs no color constants of its own. Click = dismiss; the optional
// action button fires onAction(hint.action) then also dismisses.
//
// SR-JV-6 guardrail: hint.text is attacker-influenced (mail subject / deal
// name / meeting title flow through deriveHints). It is rendered as a plain
// JSX text child only — no dangerouslySetInnerHTML, no href/src built from
// hint data.
// ---------------------------------------------------------------------------
const _ACTION_LABELS = { prep_meeting: "Prep" };

function HintBanner({ hint, onDismiss, onAction }) {
  if (!hint) return null;
  const levelClass = hint.level === "warn" ? "hint-banner--warn" : "hint-banner--info";
  const actionLabel = (hint.action && _ACTION_LABELS[hint.action]) || "Los";

  return (
    <div
      className={`hint-banner ${levelClass}`}
      role="status"
      onClick={() => { if (typeof onDismiss === "function") onDismiss(hint); }}
    >
      <span className="hint-banner__text">{hint.text}</span>
      {hint.action && (
        <button
          type="button"
          className="hint-banner__action"
          onClick={(e) => {
            e.stopPropagation();
            if (typeof onAction === "function") onAction(hint.action);
            if (typeof onDismiss === "function") onDismiss(hint);
          }}
        >
          {actionLabel}
        </button>
      )}
    </div>
  );
}

window.ConnieHints = { deriveHints, pickHint, loadHintState, saveHintState, HintBanner };
