// app.jsx — shell: sidebar, topbar, role toggle, router
(function () {
  const { useState, useEffect, useRef } = React;
  const h = React.createElement;
  const {
    LEVELS,
    LEVEL_LABEL,
    LEVEL_DESC,
    migrateRole,
    can,
    approveActionForRisk,
    denyReason,
  } = window.EmplixPermissions;
  const {
    Onboarding,
    loadOnboarding,
    saveOnboarding,
  } = window.EmplixOnboarding;
  const {
    SCREEN_KEY,
    parseHash,
    initialRoute,
    writeHash,
    fallbackCompanyUsers,
    companyTeam,
  } = window.EmplixHashRouter;
  const { Sidebar, Topbar, CommandPalette } = window;

  function App() {
    const bootOfficeRef = useRef(null);
    if (!bootOfficeRef.current) bootOfficeRef.current = window.bootOffice();
    const [, setLocaleRevision] = useState(0);
    const [active, setActive] = useState(() => initialRoute().screen);
    // current drill-down arg (#<screen>/<arg>); null when none. Produced ONCE here (C14).
    const [pageArg, setPageArg] = useState(() => initialRoute().arg);
    // migrate legacy exec/fde -> owner/manager on read; default owner.
    const [role, setRole] = useState(() => migrateRole(localStorage.getItem(window.OFFICE_ROLE_KEY) || localStorage.getItem("emplix-role")));
    const [env, setEnv] = useState(() => (bootOfficeRef.current.settings && bootOfficeRef.current.settings.defaultEnvironment) || "Demo");
    const [client, setClient] = useState(() => (bootOfficeRef.current.companyProfile && bootOfficeRef.current.companyProfile.workspace) || "SME Demo Workspace");
    const [palette, setPalette] = useState(false);
    const [woIntake, setWoIntake] = useState(0);
    const [loading, setLoading] = useState(true);
    const [toast, setToast] = useState(null);
    const toastTimerRef = useRef(null);
    const [navOpen, setNavOpen] = useState(false);
    // Unified office model — single mutable source of truth (merge). Mutators below
    // edit arrays/objects in place, then bump a shallow top-level copy to re-render
    // and persist (mirrors the office app's refresh()->persist() contract).
    const [office, setOffice] = useState(() => bootOfficeRef.current);
    const officeRef = useRef(office); officeRef.current = office;
    const [day, setDay] = useState("day");            // "day" | "monthend"
    const [highlight, setHighlight] = useState(null); // onboarding/tour focus id
    // A7: getting-started overlay state, hydrated from the guarded key.
    const [onboard, setOnboard] = useState(() => loadOnboarding() || { done: {}, profile: { industry: null, size: null }, dismissed: false });
    // auto-open on first run (never seen + nothing dismissed/completed)
    const [showOnboard, setShowOnboard] = useState(() => {
      const saved = loadOnboarding();
      return !saved;
    });

    // mirror active screen to the hash (source of truth) + restore-fallback key
    useEffect(() => { writeHash(active, pageArg); localStorage.setItem(SCREEN_KEY, active); }, [active, pageArg]);
    // Dynamic labels may be formatted during render. Re-render the full surface on
    // locale changes so those labels cannot retain the language used at mount.
    useEffect(() => window.I18N && window.I18N.onChange
      ? window.I18N.onChange(() => setLocaleRevision((value) => value + 1))
      : undefined, []);
    // Back/Forward (and external hash edits) navigate screens
    useEffect(() => {
      function onHash() { const r = parseHash(); if (r) { setActive(r.screen); setPageArg(r.arg); } }
      window.addEventListener("hashchange", onHash);
      return () => window.removeEventListener("hashchange", onHash);
    }, []);
    useEffect(() => { localStorage.setItem(window.OFFICE_ROLE_KEY, role); }, [role]);
    useEffect(() => { saveOnboarding(onboard); }, [onboard]);
    function setStep(id, val) { setOnboard((o) => ({ ...o, done: { ...o.done, [id]: val } })); }
    function setProfile(patch) { setOnboard((o) => ({ ...o, profile: { ...o.profile, ...patch } })); }
    function closeOnboard() { setOnboard((o) => ({ ...o, dismissed: true })); setShowOnboard(false); }
    // topbar workspace/environment mirror the persisted workspace defaults.
    useEffect(() => { const t = setTimeout(() => setLoading(false), 600); return () => clearTimeout(t); }, []);
    useEffect(() => { setLoading(true); const t = setTimeout(() => setLoading(false), 360); return () => clearTimeout(t); }, [active]);
    // ⌘K / Ctrl+K opens search palette
    useEffect(() => {
      function onKey(e) {
        if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setPalette((p) => !p); }
        if (e.key === "Escape") setPalette(false);
      }
      window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
    }, []);

    function showToast(msg) {
      if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
      setToast({ id: officeNewId("toast-"), msg: String(msg || "") });
      toastTimerRef.current = setTimeout(() => {
        setToast(null);
        toastTimerRef.current = null;
      }, 2600);
    }
    function setClientPersist(next) {
      if (next === client) return;
      persistWorkspaceSettings({ workspaceLabel: next }, "Workspace selector");
    }
    function setEnvPersist(next) {
      if (next === env) return;
      persistWorkspaceSettings({ defaultEnvironment: next }, "Environment selector");
    }
	    // navigate + auto-close the mobile drawer. Single nav entry point (C14):
	    // openPage(screen, arg) carries an optional drill-down arg into the hash.
	    function closeNav() {
	      if (navOpen) {
	        const sidebar = document.querySelector('[data-nav-sidebar="true"]');
	        const menuButton = document.querySelector('[data-nav-action="open"]');
	        if (sidebar && sidebar.contains(document.activeElement) && menuButton && menuButton.focus) menuButton.focus();
	      }
	      setNavOpen(false);
	    }
	    function go(id) { openPage(id, null); }
	    function openPage(screen, arg) { setActive(screen); setPageArg(arg || null); closeNav(); }
	    function hasOwn(obj, key) { return Object.prototype.hasOwnProperty.call(obj || {}, key); }
	    function clonePlain(value) {
	      if (!value || typeof value !== "object") return value;
	      try { return JSON.parse(JSON.stringify(value)); } catch (e) { return Object.assign({}, value); }
	    }
	    function captureConfigRoleState(r) {
	      return {
	        hasTitle: hasOwn(r, "title"), title: r.title,
	        hasTagline: hasOwn(r, "tagline"), tagline: r.tagline,
	        hasDepartment: hasOwn(r, "department"), department: r.department,
	        hasReportsTo: hasOwn(r, "reportsTo"), reportsTo: r.reportsTo,
	        hasMission: hasOwn(r, "mission"), mission: r.mission,
	        hasConfigDraft: hasOwn(r, "configDraft"), configDraft: clonePlain(r.configDraft),
	        hasPublishedProfile: hasOwn(r, "publishedProfile"), publishedProfile: clonePlain(r.publishedProfile),
	        hasPublishedPolicies: hasOwn(r, "publishedPolicies"), publishedPolicies: clonePlain(r.publishedPolicies),
	        hasPublishedAutonomy: hasOwn(r, "publishedAutonomy"), publishedAutonomy: r.publishedAutonomy,
	        hasPublishedAutonomyLabel: hasOwn(r, "publishedAutonomyLabel"), publishedAutonomyLabel: r.publishedAutonomyLabel,
	        hasLastConfigVersion: hasOwn(r, "lastConfigVersion"), lastConfigVersion: r.lastConfigVersion,
	      };
	    }
	    function restoreKey(target, state, key) {
	      const cap = key.charAt(0).toUpperCase() + key.slice(1);
	      if (state && state["has" + cap]) target[key] = clonePlain(state[key]);
	      else delete target[key];
	    }
	    function restoreConfigRoleState(r, state) {
	      ["title", "tagline", "department", "reportsTo", "mission", "configDraft", "publishedProfile", "publishedPolicies", "publishedAutonomy", "publishedAutonomyLabel", "lastConfigVersion"].forEach((key) => restoreKey(r, state, key));
	    }
	    function syncConfigRollbackRelease(roleId, fromVersion, toVersion, auditId, snapshot, rollbackOf) {
	      const key = window.CONFIG_STORE_KEY || "emplix-config-v1";
	      try {
	        const saved = JSON.parse(localStorage.getItem(key) || "{\"version\":1,\"state\":{}}");
	        const state = saved.state || {};
	        state.releaseByRole = Object.assign({}, state.releaseByRole || {});
	        state.releaseByRole[roleId] = Object.assign({}, state.releaseByRole[roleId] || {}, {
	          publishedVersion: toVersion,
	          draftVersion: toVersion,
	          draftSavedAt: "rolled back just now",
	          publishedAt: "rolled back just now",
	          status: "published",
	          testState: "passed",
	          testProgress: 100,
	          passRate: (state.releaseByRole[roleId] && state.releaseByRole[roleId].passRate) || 96.4,
	          failedCases: 0,
	          testedAt: "rolled back just now",
	          auditId,
	          rollbackAuditId: auditId,
	          rollbackOf,
	          rolledBackFrom: fromVersion,
	          rolledBackFromVersion: fromVersion,
	          rolledBackAt: "just now",
	          snapshot: clonePlain(snapshot),
	          rollbackState: null,
	          maxVersion: Math.max((state.releaseByRole[roleId] && state.releaseByRole[roleId].maxVersion) || 0, fromVersion || 0),
	        });
	        ["roleDrafts", "policiesByRole", "autoByRole"].forEach((bucket) => {
	          if (state[bucket] && Object.prototype.hasOwnProperty.call(state[bucket], roleId)) {
	            state[bucket] = Object.assign({}, state[bucket]);
	            delete state[bucket][roleId];
	          }
	        });
	        localStorage.setItem(key, JSON.stringify({ version: saved.version || 1, state }));
	      } catch (e) { /* config store sync is best-effort; office state remains authoritative */ }
	    }
	    function ensureNotifications(o) {
	      const cur = o.notifications || {};
      o.notifications = {
        version: 1,
        read: Object.assign({}, cur.read || {}),
        dismissed: Object.assign({}, cur.dismissed || {}),
      };
      return o.notifications;
    }
    function markNotificationRead(ids) {
      if (!can(role, "manageNotifications")) { showToast(denyReason(role, "manageNotifications")); return; }
      const list = Array.isArray(ids) ? ids : [ids];
      const clean = list.filter(Boolean);
      if (!clean.length) return;
      const o = officeRef.current;
      const n = ensureNotifications(o);
      clean.forEach((id) => { n.read[id] = "read"; });
      refreshOffice();
    }
    function dismissNotification(id) {
      if (!can(role, "manageNotifications")) { showToast(denyReason(role, "manageNotifications")); return; }
      if (!id) return;
      const o = officeRef.current;
      const n = ensureNotifications(o);
      n.read[id] = "read";
      n.dismissed[id] = "dismissed";
      refreshOffice();
    }
    const team = companyTeam(office);
    function setTeam(next) {
      if (!can(role, "manageCompanyUsers")) { showToast(denyReason(role, "manageCompanyUsers")); return; }
      const o = officeRef.current;
      const existing = o.companyUsers || fallbackCompanyUsers();
      o.companyUsers = next.map((m, i) => Object.assign({}, existing.find((x) => x.id === m.id) || existing[i] || {}, m));
      refreshOffice();
    }
    // acting human identity (for the topbar badge + audit approver, C5/C6)
    const actingMember = team.find((m) => m.level === role && m.status === "Active") || team.find((m) => m.level === role);
    const actingName = (actingMember && actingMember.name) || (role === "owner" ? "Business Owner" : LEVEL_LABEL[role]);
    const runtimeFactories = window.EmplixContextFactories;
    const runtime = runtimeFactories.createRuntimeHelpers({
      can,
      denyReason,
      role,
      env,
      setEnv,
      client,
      setClient,
      setOffice,
      officeRef,
      showToast,
      actingName,
    });
    const {
      refreshOffice,
      officeNewId,
      approverStamp,
      workspaceSettings,
      persistWorkspaceSettings,
      appendWorkOrderHistory,
      findWorkOrderMutable,
      nextWorkOrderId,
      downloadText,
      csvFrom,
      copyTextExport,
    } = runtime;
		    function roiProjectionRoleId(id) {
		      return ({ admin: "clerk", operations: "clerk", "customer-support": "cs", support: "cs", sales: "sales", accounting: "acct", finance: "acct", communications: "social", marketing: "social" })[id] || "clerk";
		    }
		    function roiProjectionById(o, id) {
		      const expansion = (window.deriveDashboardData ? window.deriveDashboardData(o).expansion : []) || [];
		      return expansion.find((p) => p.id === id) || ((o.expansionRequests || []).find((r) => r.projectionId === id) || null);
		    }
		    function normalizedRoiProjectionId(projection) {
		      return window.roiProjectionId ? window.roiProjectionId(projection || {}) : String((projection && projection.id) || (projection && projection.projectionId) || (projection && projection.name) || "projection").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
		    }
		    function ensureExpansionRequest(o, projection, status) {
		      const id = normalizedRoiProjectionId(projection);
		      if (!id) return null;
		      const list = o.expansionRequests || [];
		      const existing = list.find((r) => r.projectionId === id) || null;
		      const request = Object.assign({}, existing || {}, {
		        id: (existing && existing.id) || officeNewId("roi-"),
		        projectionId: id,
		        name: (projection && projection.name) || (existing && existing.name) || id,
		        lift: (projection && projection.lift) || (existing && existing.lift) || "",
		        confidence: (projection && (projection.conf || projection.confidence)) || (existing && existing.confidence) || "",
		        note: (projection && projection.note) || (existing && existing.note) || "",
		        color: (projection && projection.color) || (existing && existing.color) || "#2f7fe0",
		        status: (existing && existing.workOrderId && status === "reviewing") ? "work_order_open" : (status || (existing && existing.status) || "reviewing"),
		        openedBy: actingName,
		        openedByLevel: role,
		        openedAt: "剛剛",
		      });
		      const prev = list.filter((r) => r.projectionId !== id);
		      o.expansionRequests = [request].concat(prev).slice(0, 12);
		      return request;
		    }
		    function openExpansionProjection(projection) {
		      const id = normalizedRoiProjectionId(projection || {});
		      if (!id) { openPage("roi"); return null; }
		      if (!can(role, "openRoiProjection")) {
		        openPage("roi", id);
		        showToast("Value estimate opened. No review was saved.");
		        return null;
		      }
		      const o = officeRef.current;
		      const request = ensureExpansionRequest(o, projection, "reviewing");
		      refreshOffice();
		      openPage("roi", id);
		      showToast("Value estimate opened: " + ((request && request.name) || id));
		      return request;
		    }
		    function createRoiExpansionWorkOrder(projectionId) {
		      if (!can(role, "workOrder")) { showToast(denyReason(role, "workOrder")); return null; }
		      const o = officeRef.current;
		      const id = normalizedRoiProjectionId({ id: projectionId });
		      if (!id) return null;
		      const projection = roiProjectionById(o, id) || { id, name: id };
		      const request = ensureExpansionRequest(o, projection, "reviewing");
		      if (!request) return null;
		      const existing = request.workOrderId && findWorkOrderMutable(o, request.workOrderId);
		      if (existing) {
		        setHighlight(request.workOrderId);
		        openPage("workorder", request.workOrderId);
		        showToast("Opened existing value task");
		        return request.workOrderId;
		      }
		      if (!o.workOrders) o.workOrders = {};
		      if (!o.workOrders.Intake) o.workOrders.Intake = [];
		      const workOrderId = nextWorkOrderId(o);
		      const roleId = roiProjectionRoleId(id);
		      const title = "Evaluate expansion: " + (request.name || id);
		      const wo = {
		        id: workOrderId,
		        title,
		        emp: roleId,
		        sla: request.confidence === "High" ? "2d" : "1w",
		        conf: request.confidence === "High" ? 82 : 68,
		        sources: ["Value", "History", "Task Library"],
		        step: "Validate projected lift, assumptions and staffing sequence",
		        risk: "Medium",
		        future: "Next: confirm the value estimate, choose Task Library items to try, then send any spending decision to My Desk.",
		        roi: { projectionId: id, requestId: request.id, lift: request.lift || "", confidence: request.confidence || "" },
		        history: [],
		      };
		      appendWorkOrderHistory(wo, { type: "created", title: "Created from Value", note: request.id + " · " + (request.lift || "projection lift") + " · " + (request.confidence || "review") + " confidence" });
		      o.workOrders.Intake.unshift(wo);
		      request.workOrderId = workOrderId;
		      request.workOrderCreatedAt = "剛剛";
		      request.workOrderCreatedBy = actingName;
		      request.status = "work_order_open";
		      const ap = approverStamp();
		      o.auditLog.unshift(Object.assign({ id: officeNewId("al-"), time: "剛剛", action: "建立任務", roleId, tool: "Value", title: workOrderId + " · " + title, note: "Follow-up task created from value estimate " + request.name + " · request " + request.id }, ap ? { approver: ap } : {}));
		      setHighlight(workOrderId);
		      refreshOffice();
		      openPage("workorder", workOrderId);
		      showToast("Value task created: " + workOrderId);
		      return workOrderId;
		    }
    // RBAC-aware ctx: screens gate call sites via ctx.can(action); denied controls
    // render disabled-with-reason via ctx.denyReason(action). Approver identity (C6)
    // reflects the acting human; viewer never appears as an approver.
    const ctx = {
      // ---- nav / state (router-owned values consumed, not redeclared) ----
      role, env, client, woIntake, showToast, setActive: go,
      settings: workspaceSettings(office),
      downloadText,
      exportCsv: (filename, rows, columns, label, detail) => downloadText(filename, csvFrom(rows || [], columns || []), "text/csv;charset=utf-8", label || "CSV export", detail || ((rows || []).length + " rows")),
      exportJson: (filename, value, label, detail) => downloadText(filename, JSON.stringify(value, null, 2) + "\n", "application/json;charset=utf-8", label || "JSON export", detail),
      copyTextExport,
	      openPage, openExpansionProjection, createRoiExpansionWorkOrder, pageArg, highlight, setHighlight, day, setDay,
      markNotificationRead,
      dismissNotification,
      office, // always the current live state (incl. roleById, correct after hire/promote)
      can: (action) => can(role, action),
      denyReason: (action) => denyReason(role, action),
      approveActionForRisk,
      actingLevel: role, actingLevelLabel: LEVEL_LABEL[role], actingName,
      approver: role === "viewer" ? null : actingName + " · " + LEVEL_LABEL[role],
      team, setTeam, LEVELS, LEVEL_LABEL, LEVEL_DESC,
    };

    const actionDeps = Object.assign({
      LEVEL_LABEL,
      can,
      denyReason,
      role,
      env,
      setClient,
      officeRef,
      setHighlight,
      showToast,
      openPage,
      clonePlain,
      captureConfigRoleState,
      restoreConfigRoleState,
      syncConfigRollbackRelease,
      actingName,
    }, runtime);

    Object.assign(
      ctx,
      runtimeFactories.createApprovalAndWorkforceActions(actionDeps, ctx),
      runtimeFactories.createConnectorAndCompanyActions(actionDeps, ctx),
      runtimeFactories.createFilesAndConfigActions(actionDeps, ctx),
      runtimeFactories.createAgentActions(actionDeps, ctx),
      runtimeFactories.createWorkOrderAndBriefActions(actionDeps, ctx),
    );

    const Screen = window.Screens[active] || window.Screens.command;

    return h("div", { style: { display: "flex", minHeight: "100vh", position: "relative", zIndex: 1 } },
      h(Sidebar, { active, setActive: go, role, open: navOpen,
        pending: (office.inbox || []).length,
        hiredCount: (office.roles || []).filter((r) => r.hired).length }),
      navOpen && h("div", { className: "nav-backdrop", "data-nav-backdrop": "true", onClick: closeNav }),
      h("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column" } },
        h(Topbar, { role, setRole, env, setEnv: setEnvPersist, client, setClient: setClientPersist, setActive: go, ctx, onSearch: () => setPalette(true),
          onBurger: () => setNavOpen((o) => !o), onOpenGuide: () => setShowOnboard(true),
          onNewWO: () => { if (!can(role, "workOrder")) { showToast(denyReason(role, "workOrder")); return; } go("workorders"); setWoIntake((n) => n + 1); } }),
        h("main", { className: "app-main", style: { padding: "26px 30px 60px", maxWidth: 1480, width: "100%", margin: "0 auto" } },
          loading ? h(LoadingState) : h("div", { className: "screen-enter", key: active }, h(Screen, { ctx })))),
      showOnboard && h(Onboarding, { role, done: onboard.done, profile: onboard.profile, office: office, openPage: openPage, startDay0: ctx.startDay0,
        onStep: setStep, onProfile: setProfile, onClose: closeOnboard, setActive: go }),
      palette && h(CommandPalette, { ctx, setActive: go, onClose: () => setPalette(false) }),
      toast && h("div", { key: toast.id, role: "status", "aria-live": "polite", "aria-atomic": "true", style: { position: "fixed", bottom: 24, left: "50%", transform: "translateX(-50%)", zIndex: 100,
        background: "var(--text)", color: "var(--bg-2)", padding: "11px 18px", borderRadius: 11, fontSize: 13, fontWeight: 600, boxShadow: "var(--shadow-lg)",
        display: "flex", alignItems: "center", gap: 9, animation: "fadeUp .3s" } },
        h(window.Icon, { name: "checkCircle", size: 16, color: "var(--green)" }), toast.msg));
  }

  function LoadingState() {
    return h("div", null,
      h("div", { className: "skel", style: { height: 30, width: 280, marginBottom: 24 } }),
      h("div", { style: { display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 16, marginBottom: 18 } },
        Array.from({ length: 4 }).map((_, i) => h("div", { key: i, className: "skel", style: { height: 118 } }))),
      h("div", { style: { display: "grid", gridTemplateColumns: "1.6fr 1fr", gap: 16 } },
        h("div", { className: "skel", style: { height: 300 } }), h("div", { className: "skel", style: { height: 300 } })));
  }

  window.EmplixApp = App;
})();
