import React, { useState, useEffect, useRef, useMemo, createContext, useContext, useCallback } from 'react';
import { createPortal } from 'react-dom';
import * as shared from '@ttrpg/shared';
import { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, where, orderBy, limit, offset, getStorage, getStorageMode } from './storage';
import { isDevToolsEnabled } from './config/devTools';
import {
  PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown,
  UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle,
  Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon,
  StopCircle as StopCircleIcon, Users2, Dices, ChevronDown, ScrollText,
  Maximize2, Minimize2, PanelTopOpen, PanelTopClose, Moon, Coffee, Clock, ChevronRight, X,
  Undo2, Redo2, Crosshair
} from 'lucide-react';

// ----- UI feedback: toast (transient) + info modal (persistent) -----
const UIFeedbackContext = createContext(null);
const useUIFeedback = () => useContext(UIFeedbackContext);

function ToastStack({ toasts, onDismiss }) {
  if (!toasts.length) return null;
  return (
    <div className="fixed bottom-4 right-4 z-[60] flex flex-col gap-2 max-w-sm">
      {toasts.map(t => (
        <div key={t.id} className="flex items-start gap-2 bg-stone-800 border border-red-600 text-stone-100 px-3 py-2 rounded-md shadow-lg">
          <AlertTriangle size={16} className="text-red-400 flex-shrink-0 mt-0.5" />
          <span className="text-sm flex-grow">{t.message}</span>
          <button onClick={() => onDismiss(t.id)} className="text-stone-400 hover:text-stone-200 flex-shrink-0" aria-label="Dismiss">
            <X size={14} />
          </button>
        </div>
      ))}
    </div>
  );
}

function InfoModal({ message, onClose }) {
  if (!message) return null;
  return (
    <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-[100]">
      <div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
        <div className="flex items-center mb-4">
          <AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" />
          <h2 className="text-xl font-semibold text-yellow-300">Notice</h2>
        </div>
        <p className="text-stone-300 mb-6">{message}</p>
        <div className="flex justify-end">
          <button onClick={onClose} className="px-4 py-2 text-sm font-medium text-stone-900 bg-amber-400 hover:bg-amber-300 rounded-md transition-colors">
            OK
          </button>
        </div>
      </div>
    </div>
  );
}

function UIFeedbackProvider({ children }) {
  const [toasts, setToasts] = useState([]);
  const [info, setInfo] = useState(null);
  const nextId = useRef(1);

  const showToast = useCallback((message) => {
    const id = nextId.current++;
    setToasts(prev => [...prev, { id, message }]);
    setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 6000);
  }, []);

  const showInfo = useCallback((message) => {
    setInfo(message);
  }, []);

  const dismissToast = useCallback((id) => {
    setToasts(prev => prev.filter(t => t.id !== id));
  }, []);

  return (
    <UIFeedbackContext.Provider value={{ showToast, showInfo }}>
      {children}
      <ToastStack toasts={toasts} onDismiss={dismissToast} />
      <InfoModal message={info} onClose={() => setInfo(null)} />
    </UIFeedbackContext.Provider>
  );
}

// Custom CSS for death animation (player view only)
const deathAnimationStyles = `
  @keyframes death-dissolve {
    0% {
      opacity: 1;
      transform: scale(1) translateY(0);
    }
    50% {
      opacity: 0.5;
      transform: scale(0.95) translateY(-5px);
      filter: blur(2px);
    }
    100% {
      opacity: 0;
      transform: scale(0.8) translateY(-10px);
      filter: blur(4px);
    }
  }
  
  .animate-death-dissolve {
    animation: death-dissolve 2s ease-in-out forwards;
  }
`;

// Inject styles
if (typeof document !== 'undefined') {
  const styleElement = document.createElement('style');
  styleElement.innerHTML = deathAnimationStyles;
  document.head.appendChild(styleElement);
}

// ============================================================================
// CONSTANTS
// ============================================================================

const APP_VERSION = 'v0.4';
const {
  DEFAULT_MAX_HP, DEFAULT_INIT_MOD, MONSTER_DEFAULT_INIT_MOD,
  generateId, rollD20, formatInitMod,
  startEncounter, nextTurn, togglePause, endEncounter,
  addParticipant, addParticipants, updateParticipant, removeParticipant,
  toggleParticipantActive: combatToggleActive,
  applyHpChange: combatApplyHpChange,
  deathSave: combatDeathSave,
  stabilizeParticipant,
  reviveParticipant,
  markDead,
  toggleCondition: combatToggleCondition,
  reorderParticipants,
} = shared;
const ROLL_DISPLAY_DURATION = 5000;

const CONDITIONS = [
  { id: 'alchemist_fire',      label: 'Alchemist Fire',      emoji: '🔥' },
  { id: 'bardic_inspiration',  label: 'Bardic Inspiration',  emoji: '🎵' },
  { id: 'blinded',             label: 'Blinded',             emoji: '🙈' },
  { id: 'charmed',             label: 'Charmed',             emoji: '💘' },
  { id: 'deafened',            label: 'Deafened',            emoji: '🔇' },
  { id: 'exhaustion',          label: 'Exhaustion',          emoji: '😴' },
  { id: 'frightened',          label: 'Frightened',          emoji: '😱' },
  { id: 'grappled',            label: 'Grappled',            emoji: '🤜' },
  { id: 'grazed',              label: 'Grazed',              emoji: '🩹' },
  { id: 'incapacitated',       label: 'Incapacitated',       emoji: '💫' },
  { id: 'invisible',           label: 'Invisible',           emoji: '👻' },
  { id: 'paralyzed',           label: 'Paralyzed',           emoji: '⚡' },
  { id: 'petrified',           label: 'Petrified',           emoji: '🗿' },
  { id: 'poisoned',            label: 'Poisoned',            emoji: '🤢' },
  { id: 'prone',               label: 'Prone',               emoji: '⬇️' },
  { id: 'restrained',          label: 'Restrained',          emoji: '🕸️' },
  { id: 'sapped',              label: 'Sapped',              emoji: '🔨' },
  { id: 'shield',              label: 'Shield',              emoji: '🛡️' },
  { id: 'slowed',              label: 'Slowed',              emoji: '🐌' },
  { id: 'stunned',             label: 'Stunned',             emoji: '💥' },
  { id: 'unconscious',         label: 'Unconscious',         emoji: '💤' },
  { id: 'vexed',               label: 'Vexed',               emoji: '🎯' },
];

// ============================================================================
// FIREBASE CONFIGURATION
// ============================================================================

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID
};

const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default';
const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`;

let app;
let db;
let auth;
let storage;

const STORAGE_MODE = getStorageMode();

// Initialize storage backend. firebase mode = real SDK init.
// ws/memory mode = mock auth, no firebase.
const initializeStorage = () => {
  if (STORAGE_MODE === 'firebase') {
    const requiredKeys = ['apiKey', 'authDomain', 'projectId', 'appId'];
    const missingKeys = requiredKeys.filter(key => !firebaseConfig[key]);
    if (missingKeys.length > 0) {
      console.error(`CRITICAL: Missing Firebase config values: ${missingKeys.join(', ')}`);
      return false;
    }
    try {
      app = initializeApp(firebaseConfig);
      db = getFirestore(app);
      auth = getAuth(app);
      storage = getStorage();
      return true;
    } catch (error) {
      console.error("Error initializing Firebase:", error);
      return false;
    }
  }

  // ws / memory mode: stub auth so App's anon-sign-in path works.
  // db stays a truthy sentinel object so legacy `if (!db) return` guards pass;
  // all real reads/writes route through `storage.*`, never the SDK `db`.
  const FAKE_USER = { uid: 'local-user', isAnonymous: true };
  auth = {
    currentUser: FAKE_USER,
  };
  db = { __localStub: true };
  storage = getStorage();
  return true;
};

const isInitialized = initializeStorage();

// ============================================================================
// FIRESTORE PATH HELPERS
// ============================================================================

const getPath = {
  campaigns: () => `${PUBLIC_DATA_PATH}/campaigns`,
  campaign: (id) => `${PUBLIC_DATA_PATH}/campaigns/${id}`,
  encounters: (campaignId) => `${PUBLIC_DATA_PATH}/campaigns/${campaignId}/encounters`,
  encounter: (campaignId, encounterId) => `${PUBLIC_DATA_PATH}/campaigns/${campaignId}/encounters/${encounterId}`,
  activeDisplay: () => `${PUBLIC_DATA_PATH}/activeDisplay/status`,
  logs: () => `${PUBLIC_DATA_PATH}/logs`
};

// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================

// generateId, rollD20, formatInitMod, sortParticipantsByInitiative,
// computeTurnOrderAfterRemoval/Addition: imported from @ttrpg/shared (1-list model).

// Display limit: recent logs only. Download/copy fetch full set on demand.
// Keep small — 500 = ~900KB per snapshot = slow load.
const LOG_PAGE_SIZE = 100;
const LOG_ORDER = orderBy('ts', 'desc');

// Combat actions now live in shared/turn.js and WRITE THEIR OWN LOGS.
// Every mutating func is async, takes ctx {storage, encPath, logPath},
// persists encounter patch + log entry itself. No separate logEvent call.
//
// buildCtx: standard context for combat action calls. Lazy getStorage()
// so handlers work even if module-level `storage` assigned late (test/auth).
const buildCtx = (encounterPath) => ({
  storage: getStorage(),
  encPath: encounterPath,
  logPath: getPath.logs(),
});

// Read undo payload: new lean `undo` (id-based) or legacy (`undo_payload`/`undo`).
// Lean events need expandUndo() at click time (needs current enc doc).
// expandUndo lives in shared/turn.js (re-exported via @ttrpg/shared).
const { expandUndo } = shared;
const getUndo = (entry) => entry.undo_payload || entry.undo || null;


// ============================================================================
// CUSTOM HOOKS
// ============================================================================

function useFirestoreDocument(docPath) {
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!docPath) {
      setData(null);
      setIsLoading(false);
      setError("Document path not provided.");
      return;
    }

    setIsLoading(true);
    setError(null);

    const storage = getStorage();
    const unsubscribe = storage.subscribeDoc(docPath, (doc) => {
      setData(doc);
      setIsLoading(false);
    }, (err) => {
      console.error(`Error fetching document ${docPath}:`, err);
      setError(err.message || "Failed to fetch document.");
      setIsLoading(false);
      setData(null);
    });
    return () => { if (typeof unsubscribe === 'function') unsubscribe(); };
  }, [docPath]);

  return { data, isLoading, error };
}

function useFirestoreCollection(collectionPath, queryConstraints = []) {
  const [data, setData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const queryString = useMemo(() => JSON.stringify(queryConstraints), [queryConstraints]);

  useEffect(() => {
    if (!collectionPath) {
      setData([]);
      setIsLoading(false);
      setError("Collection path not provided.");
      return;
    }

    setIsLoading(true);
    setError(null);

    const storage = getStorage();
    const unsubscribe = storage.subscribeCollection(collectionPath, (items) => {
      setData(items);
      setIsLoading(false);
    }, queryConstraints, (err) => {
      console.error(`[useFirestoreCollection] ERR ${collectionPath}:`, err);
      setError(err.message || "Failed to fetch collection.");
      setIsLoading(false);
      setData([]);
    });
    return () => {
      if (typeof unsubscribe === 'function') unsubscribe();
    };
    // queryString, not array ref
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [collectionPath, queryString]);

  return { data, isLoading, error };
}

// ============================================================================
// REUSABLE COMPONENTS
// ============================================================================

function Modal({ onClose, title, children }) {
  useEffect(() => {
    const handleEsc = (event) => {
      if (event.key === 'Escape') onClose();
    };
    window.addEventListener('keydown', handleEsc);
    return () => window.removeEventListener('keydown', handleEsc);
  }, [onClose]);

  return createPortal(
    <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-[100]">
      <div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
        <div className="flex justify-between items-center mb-4">
          <h2 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide">{title}</h2>
          <button onClick={onClose} className="text-stone-400 hover:text-stone-200">
            <XCircle size={24} />
          </button>
        </div>
        {children}
      </div>
    </div>,
    document.body
  );
}

function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(null);

  // Reset transient state each time modal opens.
  useEffect(() => {
    if (isOpen) { setPending(false); setError(null); }
  }, [isOpen]);

  if (!isOpen) return null;

  const handleConfirm = async () => {
    if (pending) return; // double-click guard
    setPending(true);
    setError(null);
    try {
      await onConfirm();
      onClose();
    } catch (err) {
      console.error('ConfirmationModal action failed:', err);
      setError(err?.message || 'Action failed. Try again.');
      setPending(false);
    }
  };

  const close = () => {
    if (pending) return; // block close mid-action
    onClose();
  };

  return createPortal(
    <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center p-4 z-[100]">
      <div className="bg-stone-900 p-6 rounded-lg shadow-xl w-full max-w-md">
        <div className="flex items-center mb-4">
          <AlertTriangle size={24} className="text-yellow-400 mr-3 flex-shrink-0" />
          <h2 className="text-xl font-semibold text-yellow-300">{title || "Confirm Action"}</h2>
        </div>
        <p className="text-stone-300 mb-6">{message || "Are you sure you want to proceed?"}</p>
        {error && (
          <p className="text-red-400 text-sm mb-4">{error}</p>
        )}
        <div className="flex justify-end space-x-3">
          <button
            onClick={close}
            disabled={pending}
            className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            Cancel
          </button>
          <button
            onClick={handleConfirm}
            disabled={pending}
            className="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {pending ? "Working..." : "Confirm"}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function LoadingSpinner({ message = "Loading..." }) {
  return (
    <div className="min-h-screen bg-stone-950 text-white flex flex-col items-center justify-center p-4">
      <div className="animate-spin rounded-full h-16 w-16 border-t-4 border-amber-500 border-solid"></div>
      <p className="mt-4 text-xl">{message}</p>
    </div>
  );
}

function ErrorDisplay({ message, critical = false }) {
  return (
    <div className={`min-h-screen ${critical ? 'bg-red-900' : 'bg-stone-950'} text-white flex flex-col items-center justify-center p-4`}>
      <h1 className="text-3xl font-bold mb-4">
        {critical ? 'Configuration Error' : 'Error'}
      </h1>
      <p className="text-xl text-center max-w-2xl">{message}</p>
    </div>
  );
}

// ============================================================================
// FORM COMPONENTS
// ============================================================================

function CreateCampaignForm({ onCreate, onCancel }) {
  const [name, setName] = useState('');
  const [backgroundUrl, setBackgroundUrl] = useState('');
  const [ruleset, setRuleset] = useState('5e');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.trim()) {
      onCreate(name, backgroundUrl, ruleset);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="campaignName" className="block text-sm font-medium text-stone-300">
          Campaign Name
        </label>
        <input
          type="text"
          id="campaignName"
          value={name}
          onChange={(e) => setName(e.target.value)}
          className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
          required
        />
      </div>
      <div>
        <label htmlFor="backgroundUrl" className="block text-sm font-medium text-stone-300">
          Player Display Background URL (Optional)
        </label>
        <input
          type="url"
          id="backgroundUrl"
          value={backgroundUrl}
          onChange={(e) => setBackgroundUrl(e.target.value)}
          placeholder="https://example.com/image.jpg"
          className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
        />
      </div>
      <div>
        <label className="block text-sm font-medium text-stone-300 mb-1">Ruleset</label>
        <div className="flex gap-4">
          <label className="flex items-center gap-2 text-sm text-stone-200">
            <input type="radio" value="5e" checked={ruleset === '5e'} onChange={() => setRuleset('5e')} />
            D&D 5e (death saves)
          </label>
          <label className="flex items-center gap-2 text-sm text-stone-200">
            <input type="radio" value="generic" checked={ruleset === 'generic'} onChange={() => setRuleset('generic')} />
            Generic (no death saves, negative HP)
          </label>
        </div>
      </div>

      <div className="flex justify-end space-x-3">
        <button
          type="button"
          onClick={onCancel}
          className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
        >
          Cancel
        </button>
        <button
          type="submit"
          className="px-4 py-2 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors"
        >
          Create
        </button>
      </div>
    </form>
  );
}

function CreateEncounterForm({ onCreate, onCancel, defaultRuleset = '5e' }) {
  const [name, setName] = useState('');
  const [ruleset, setRuleset] = useState(defaultRuleset);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (name.trim()) {
      onCreate(name, ruleset);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <div>
        <label htmlFor="encounterName" className="block text-sm font-medium text-stone-300">
          Encounter Name
        </label>
        <input
          type="text"
          id="encounterName"
          value={name}
          onChange={(e) => setName(e.target.value)}
          className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
          required
        />
      </div>
      <div>
        <label className="block text-sm font-medium text-stone-300 mb-1">Ruleset</label>
        <div className="flex gap-4">
          <label className="flex items-center gap-2 text-sm text-stone-200">
            <input type="radio" value="5e" checked={ruleset === '5e'} onChange={() => setRuleset('5e')} />
            D&D 5e (death saves)
          </label>
          <label className="flex items-center gap-2 text-sm text-stone-200">
            <input type="radio" value="generic" checked={ruleset === 'generic'} onChange={() => setRuleset('generic')} />
            Generic (no death saves, negative HP)
          </label>
        </div>
      </div>
      <div className="flex justify-end space-x-3">
        <button
          type="button"
          onClick={onCancel}
          className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
        >
          Cancel
        </button>
        <button
          type="submit"
          className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors"
        >
          Create
        </button>
      </div>
    </form>
  );
}

function EditParticipantModal({ participant, onClose, onSave }) {
  const [name, setName] = useState(participant.name);
  const [initiative, setInitiative] = useState(participant.initiative);
  const [currentHp, setCurrentHp] = useState(participant.currentHp);
  const [maxHp, setMaxHp] = useState(participant.maxHp);
  const [hpFormula, setHpFormula] = useState(participant.hpFormula || '');
  const [ac, setAc] = useState(participant.ac != null ? participant.ac : '');
  const [asNpc, setAsNpc] = useState(participant.type === 'npc');
  const [tempHp, setTempHp] = useState(participant.tempHp || 0);

  const handleSubmit = (e) => {
    e.preventDefault();
    let finalMaxHp = parseInt(maxHp, 10);
    // formula rolls only if maxHp empty. maxHp wins if both.
    if (isNaN(finalMaxHp) && hpFormula.trim()) {
      const rolled = shared.rollHpFormula(hpFormula);
      if (rolled) finalMaxHp = rolled;
    }
    if (isNaN(finalMaxHp)) finalMaxHp = participant.maxHp;
    let finalCurrentHp = parseInt(currentHp, 10);
    if (isNaN(finalCurrentHp)) finalCurrentHp = finalMaxHp;
    onSave({
      name: name.trim(),
      initiative: parseInt(initiative, 10),
      currentHp: finalCurrentHp,
      maxHp: finalMaxHp,
      hpFormula: (participant.type === 'monster' || participant.type === 'npc') ? (hpFormula.trim() || null) : null,
      ac: ac !== '' ? parseInt(ac, 10) : null,
      tempHp: Math.max(0, parseInt(tempHp, 10) || 0),
      type: participant.type === 'monster' || participant.type === 'npc' ? (asNpc ? 'npc' : 'monster') : participant.type,
    });
  };

  return (
    <Modal onClose={onClose} title={`Edit ${participant.name}`}>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-stone-300">Name</label>
          <input
            type="text"
            value={name}
            onChange={(e) => setName(e.target.value)}
            className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
          />
        </div>
        <div className="flex gap-4">
          <div className="flex-1">
            <label htmlFor="edit-initiative" className="block text-sm font-medium text-stone-300">Initiative</label>
            <input
              type="number"
              id="edit-initiative"
              value={initiative}
              onChange={(e) => setInitiative(e.target.value)}
              className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
            />
          </div>
          <div className="flex-1">
            <label className="block text-sm font-medium text-stone-300">AC</label>
            <input
              type="number"
              value={ac}
              onChange={(e) => setAc(e.target.value)}
              placeholder="optional"
              className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
            />
          </div>
        </div>
        <div className="flex gap-4">
          <div className="flex-1">
            <label className="block text-sm font-medium text-stone-300">Current HP</label>
            <input
              type="number"
              value={currentHp}
              onChange={(e) => setCurrentHp(e.target.value)}
              className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
            />
          </div>
          <div className="flex-1">
            <label className="block text-sm font-medium text-stone-300">Max HP</label>
            <input
              type="number"
              value={maxHp}
              onChange={(e) => setMaxHp(e.target.value)}
              className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
            />
          </div>
        </div>
        <div className="flex gap-4">
          <div className="flex-1">
            <label className="block text-sm font-medium text-stone-300">Temp HP</label>
            <input
              type="number"
              value={tempHp}
              onChange={(e) => setTempHp(e.target.value)}
              min="0"
              placeholder="0"
              className="mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-cyan-600 focus:border-cyan-600 sm:text-sm text-white"
            />
          </div>
          <div className="flex-1" />
        </div>
        {(participant.type === 'monster' || participant.type === 'npc') && (
          <div>
            <label className="block text-sm font-medium text-stone-300 mb-1">HP Formula</label>
            <div className="flex gap-2">
              <input
                type="text"
                value={hpFormula}
                onChange={(e) => setHpFormula(e.target.value)}
                placeholder="e.g. 2d6+9"
                className="flex-1 px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
              />
              <button
                type="button"
                disabled={!shared.rollHpFormula(hpFormula)}
                onClick={() => {
                  const rolled = shared.rollHpFormula(hpFormula);
                  if (rolled) setMaxHp(rolled);
                }}
                className="px-3 py-2 text-sm font-medium text-white bg-violet-700 hover:bg-violet-800 rounded-md transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
                title="Roll formula, set Max HP"
              >
                <Dices size={16} className="inline" /> Reroll
              </button>
            </div>
          </div>
        )}
        {(participant.type === 'monster' || participant.type === 'npc') && (
          <div className="flex items-center">
            <input
              type="checkbox"
              id="editIsNpc"
              checked={asNpc}
              onChange={(e) => setAsNpc(e.target.checked)}
              className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
            />
            <label htmlFor="editIsNpc" className="ml-2 block text-sm text-stone-300">
              Is NPC?
            </label>
          </div>
        )}
        <div className="flex justify-end space-x-3 pt-2">
          <button
            type="button"
            onClick={onClose}
            className="px-4 py-2 text-sm font-medium text-stone-300 bg-stone-700 hover:bg-stone-600 rounded-md transition-colors"
          >
            Cancel
          </button>
          <button
            type="submit"
            className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors"
          >
            <Save size={18} className="mr-1 inline-block" /> Save
          </button>
        </div>
      </form>
    </Modal>
  );
}

// ============================================================================
// CHARACTER MANAGER COMPONENT
// ============================================================================

function CharacterManager({ campaignId, campaignCharacters, syncCharacters }) {
  const { showToast, showInfo } = useUIFeedback();
  const [characterName, setCharacterName] = useState('');
  const [defaultMaxHp, setDefaultMaxHp] = useState(DEFAULT_MAX_HP);
  const [defaultInitMod, setDefaultInitMod] = useState(DEFAULT_INIT_MOD);
  const [defaultAc, setDefaultAc] = useState('');
  const [isCharacterNpc, setIsCharacterNpc] = useState(false);
  const [editingCharacter, setEditingCharacter] = useState(null);
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [itemToDelete, setItemToDelete] = useState(null);
  const [draggedCharId, setDraggedCharId] = useState(null);

  const handleCharDragStart = (e, id) => { setDraggedCharId(id); e.dataTransfer.effectAllowed = 'move'; };
  const handleCharDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; };
  const handleCharDrop = async (e, targetId) => {
    e.preventDefault();
    if (!campaignId || !draggedCharId || draggedCharId === targetId) { setDraggedCharId(null); return; }
    const reordered = [...campaignCharacters];
    const fromIdx = reordered.findIndex(c => c.id === draggedCharId);
    const toIdx = reordered.findIndex(c => c.id === targetId);
    if (fromIdx === -1 || toIdx === -1) { setDraggedCharId(null); return; }
    const [moved] = reordered.splice(fromIdx, 1);
    reordered.splice(toIdx, 0, moved);
    try { await storage.updateDoc(getPath.campaign(campaignId), { players: reordered }); } catch (err) {}
    setDraggedCharId(null);
  };
  const [isOpen, setIsOpen] = useState(() => {
    try { return localStorage.getItem('ttrpg.charactersCollapsed') !== 'true'; }
    catch { return true; }
  });
  useEffect(() => {
    try { localStorage.setItem('ttrpg.charactersCollapsed', String(!isOpen)); }
    catch {}
  }, [isOpen]);

  const handleAddCharacter = async () => {
    if (!db || !characterName.trim() || !campaignId) return;

    const hp = parseInt(defaultMaxHp, 10);
    const initMod = parseInt(defaultInitMod, 10);

    if (isNaN(hp) || hp <= 0) {
      showInfo("Please enter a valid positive number for Default Max HP.");      return;
    }
    if (isNaN(initMod)) {
      showInfo("Please enter a valid number for Default Initiative Modifier.");      return;
    }

    const newCharacter = {
      id: generateId(),
      name: characterName.trim(),
      defaultMaxHp: hp,
      defaultInitMod: initMod,
      defaultAc: defaultAc !== '' ? parseInt(defaultAc, 10) : null,
      isNpc: isCharacterNpc || false,
    };

    try {
      await storage.updateDoc(getPath.campaign(campaignId), {
        players: [...campaignCharacters, newCharacter]
      });
      setCharacterName('');
      setDefaultMaxHp(DEFAULT_MAX_HP);
      setDefaultInitMod(DEFAULT_INIT_MOD);
      setDefaultAc('');
      setIsCharacterNpc(false);
    } catch (err) {
      console.error("Error adding character:", err);
      showToast("Failed to add character. Please try again.");    }
  };

  const handleUpdateCharacter = async (characterId, newName, newDefaultMaxHp, newDefaultInitMod, newDefaultAc, newDefaultCurrentHp, newIsNpc) => {
    if (!db || !newName.trim() || !campaignId) return;

    const hp = parseInt(newDefaultMaxHp, 10);
    const initMod = parseInt(newDefaultInitMod, 10);

    if (isNaN(hp) || hp <= 0) {
      showInfo("Please enter a valid positive number for Default Max HP.");      setEditingCharacter(null);
      return;
    }
    if (isNaN(initMod)) {
      showInfo("Please enter a valid number for Default Initiative Modifier.");      setEditingCharacter(null);
      return;
    }

    const updatedCharacters = campaignCharacters.map(c =>
      c.id === characterId
        ? { ...c, name: newName.trim(), defaultMaxHp: hp, defaultInitMod: initMod, defaultAc: newDefaultAc !== undefined && newDefaultAc !== '' ? parseInt(newDefaultAc, 10) : null, defaultCurrentHp: newDefaultCurrentHp !== undefined && newDefaultCurrentHp !== '' ? parseInt(newDefaultCurrentHp, 10) : null, isNpc: newIsNpc != null ? newIsNpc : (c.isNpc || false) }
        : c
    );

    try {
      await storage.updateDoc(getPath.campaign(campaignId), { players: updatedCharacters });
      setEditingCharacter(null);
    } catch (err) {
      console.error("Error updating character:", err);
      showToast("Failed to update character. Please try again.");    }
  };

  const requestDeleteCharacter = (characterId, charName) => {
    setItemToDelete({ id: characterId, name: charName });
    setShowDeleteConfirm(true);
  };

  const confirmDeleteCharacter = async () => {
    if (!db || !itemToDelete) return;

    const updatedCharacters = campaignCharacters.filter(c => c.id !== itemToDelete.id);

    try {
      await storage.updateDoc(getPath.campaign(campaignId), { players: updatedCharacters });
    } catch (err) {
      console.error("Error deleting character:", err);
      showToast("Failed to delete character. Please try again.");    }

    setShowDeleteConfirm(false);
    setItemToDelete(null);
  };

  return (
    <>
      <div className="p-2 md:p-4 bg-stone-900 rounded-lg shadow">
        <div className="flex justify-between items-center mb-3">
          <button
            onClick={() => setIsOpen(!isOpen)}
            className="flex items-center text-xl font-semibold text-amber-300 font-cinzel tracking-wide hover:text-amber-200 transition-colors"
            aria-expanded={isOpen}
          >
            {isOpen ? <ChevronDown size={20} className="mr-1" /> : <ChevronRight size={20} className="mr-1" />}
            <Users size={24} className="mr-2" /> Campaign Characters
            <span className="text-sm font-normal text-stone-400 ml-1">({campaignCharacters.length})</span>
          </button>
        </div>

        {isOpen && (
          <>
            <label className={`inline-flex items-center gap-2 mb-3 px-3 py-1.5 rounded-lg cursor-pointer text-sm font-medium transition-colors ${syncCharacters ? 'bg-amber-900 border border-amber-500 text-amber-100' : 'bg-stone-800 border border-stone-700 text-stone-400 hover:text-stone-200'}`} title="When enabled, ending an encounter writes each character's current HP, Max HP, and AC back to the campaign roster so they carry into the next encounter."
            >
              <input
                type="checkbox"
                checked={syncCharacters}
                onChange={(e) => storage.updateDoc(getPath.campaign(campaignId), { syncCharacters: e.target.checked })}
                className="h-4 w-4 text-amber-600 border-stone-400 rounded focus:ring-amber-500"
              />
              Sync character changes on encounter end
            </label>
            <form onSubmit={(e) => { e.preventDefault(); handleAddCharacter(); }} className="grid grid-cols-2 sm:grid-cols-12 gap-2 mb-4 items-end">
              <div className="col-span-2 sm:col-span-6">
                <label htmlFor="characterName" className="block text-xs font-medium text-stone-400">
                  Name
                </label>
                <input
                  type="text"
                  id="characterName"
                  value={characterName}
                  onChange={(e) => setCharacterName(e.target.value)}
                  placeholder="Character name"
                  className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="sm:col-span-2">
                <label htmlFor="defaultInitMod" className="block text-xs font-medium text-stone-400">
                  Init Mod
                </label>
                <input
                  type="number"
                  id="defaultInitMod"
                  value={defaultInitMod}
                  onChange={(e) => setDefaultInitMod(e.target.value)}
                  className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="sm:col-span-2">
                <label htmlFor="defaultAc" className="block text-xs font-medium text-stone-400">
                  AC
                </label>
                <input
                  type="number"
                  id="defaultAc"
                  value={defaultAc}
                  onChange={(e) => setDefaultAc(e.target.value)}
                  className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="sm:col-span-2">
                <label htmlFor="defaultMaxHp" className="block text-xs font-medium text-stone-400">
                  HP
                </label>
                <input
                  type="number"
                  id="defaultMaxHp"
                  value={defaultMaxHp}
                  onChange={(e) => setDefaultMaxHp(e.target.value)}
                  className="w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <label className="col-span-2 sm:col-span-12 flex items-center gap-2 text-xs font-medium text-stone-400 cursor-pointer">
                <input
                  type="checkbox"
                  checked={isCharacterNpc}
                  onChange={(e) => setIsCharacterNpc(e.target.checked)}
                  className="w-4 h-4 accent-amber-600"
                />
                Long-lived NPC (death saves like character, red name in combat)
              </label>
              <button
                type="submit"
                className="col-span-2 sm:col-span-12 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center justify-center"
              >
                <PlusCircle size={18} className="mr-1" /> Add Character
              </button>
            </form>

            {campaignCharacters.length === 0 && (
              <p className="text-sm text-stone-400">No characters added yet.</p>
            )}

            <ul className="space-y-2">
              {campaignCharacters.map(character => (
                <li
                  key={character.id}
                  draggable
                  onDragStart={(e) => handleCharDragStart(e, character.id)}
                  onDragOver={handleCharDragOver}
                  onDrop={(e) => handleCharDrop(e, character.id)}
                  onDragEnd={() => setDraggedCharId(null)}
                  className={`flex justify-between items-center p-3 bg-stone-800 rounded-md cursor-grab ${draggedCharId === character.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`}
                >
                  <div className="flex items-center flex-grow">
                    <ChevronsUpDown size={16} className="text-stone-400 flex-shrink-0 mr-2" title="Drag to reorder" />
                  {editingCharacter && editingCharacter.id === character.id ? (
                    <form
                      onSubmit={(e) => {
                        e.preventDefault();
                        handleUpdateCharacter(
                          character.id,
                          editingCharacter.name,
                          editingCharacter.defaultMaxHp,
                          editingCharacter.defaultInitMod,
                          editingCharacter.defaultAc,
                          editingCharacter.defaultCurrentHp,
                          editingCharacter.isNpc
                        );
                      }}
                      className="flex-grow flex flex-wrap gap-2 items-center"
                    >
                      <div className="flex flex-col flex-grow min-w-[100px]">
                        <label className="text-[10px] text-stone-400 mb-0.5">Name</label>
                        <input
                          type="text"
                          value={editingCharacter.name}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, name: e.target.value })}
                          className="w-full px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
                        />
                      </div>
                      <div className="flex flex-col">
                        <label className="text-[10px] text-stone-400 mb-0.5">HP</label>
                        <input
                          type="number"
                          value={editingCharacter.defaultMaxHp}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultMaxHp: e.target.value })}
                          className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
                          title="Default Max HP"
                        />
                      </div>
                      <div className="flex flex-col">
                        <label className="text-[10px] text-stone-400 mb-0.5">Cur HP</label>
                        <input
                          type="number"
                          value={editingCharacter.defaultCurrentHp ?? ''}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultCurrentHp: e.target.value })}
                          className="w-16 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
                          title="Default Current HP"
                        />
                      </div>
                      <div className="flex flex-col">
                        <label className="text-[10px] text-stone-400 mb-0.5">Init</label>
                        <input
                          type="number"
                          value={editingCharacter.defaultInitMod}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultInitMod: e.target.value })}
                          className="w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
                          title="Default Init Mod"
                        />
                      </div>
                      <div className="flex flex-col">
                        <label className="text-[10px] text-stone-400 mb-0.5">AC</label>
                        <input
                          type="number"
                          value={editingCharacter.defaultAc ?? ''}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, defaultAc: e.target.value })}
                          className="w-16 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
                          title="AC"
                        />
                      </div>
                      <label className="flex items-center gap-1 text-[10px] text-stone-400" title="Long-lived NPC">
                        <input
                          type="checkbox"
                          checked={editingCharacter.isNpc || false}
                          onChange={(e) => setEditingCharacter({ ...editingCharacter, isNpc: e.target.checked })}
                          className="w-3 h-3 accent-red-600"
                        />
                        NPC
                      </label>
                      <button
                        type="submit"
                        className="inline-flex items-center gap-1.5 px-3 py-2 rounded-md bg-green-700 hover:bg-green-600 text-white font-semibold text-sm min-h-[40px]"
                        aria-label="Save character"
                      >
                        <Save size={18} /> Save
                      </button>
                      <button
                        type="button"
                        onClick={() => setEditingCharacter(null)}
                        className="p-1 text-stone-400 hover:text-stone-200"
                      >
                        <XCircle size={18} />
                      </button>
                    </form>
                  ) : (
                    <>
                      <div className="flex items-center mr-auto">
                        <span className="text-stone-100 mr-2">{character.name}</span>
                        {character.isNpc && (
                          <span className="px-1.5 py-0.5 rounded bg-red-900 text-red-200 text-xs font-medium border border-red-600" title="Long-lived NPC">NPC</span>
                        )}
                        <div className="flex gap-1">
                          <span className="px-1.5 py-0.5 rounded bg-red-900/60 text-red-200 text-xs font-medium">HP {character.defaultMaxHp || '?'}</span>
                          {character.defaultCurrentHp != null && character.defaultCurrentHp !== character.defaultMaxHp && (
                            <span className="px-1.5 py-0.5 rounded bg-red-950 text-red-300 text-xs font-medium" title="Current HP (carried from last encounter)">Current HP {character.defaultCurrentHp}</span>
                          )}
                          {character.defaultAc != null && (
                            <span className="px-1.5 py-0.5 rounded bg-sky-900/60 text-sky-200 text-xs font-medium">AC {character.defaultAc}</span>
                          )}
                          <span className="px-1.5 py-0.5 rounded bg-amber-900/40 text-amber-200 text-xs font-medium">Init {formatInitMod(character.defaultInitMod)}</span>
                        </div>
                      </div>
                      <div className="flex space-x-2 flex-shrink-0">
                        <button
                          onClick={() => setEditingCharacter({
                            id: character.id,
                            name: character.name,
                            defaultMaxHp: character.defaultMaxHp || DEFAULT_MAX_HP,
                            defaultInitMod: character.defaultInitMod || DEFAULT_INIT_MOD,
                            defaultAc: character.defaultAc ?? '',
                            defaultCurrentHp: character.defaultCurrentHp ?? ''
                          })}
                          className="p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
                          aria-label="Edit character"
                        >
                          <Edit3 size={18} />
                        </button>
                        <button
                          onClick={() => requestDeleteCharacter(character.id, character.name)}
                          className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
                          aria-label="Delete character"
                        >
                          <Trash2 size={18} />
                        </button>
                      </div>
                    </>
                  )}
                  </div>
                </li>
              ))}
            </ul>
          </>
        )}
      </div>

      <ConfirmationModal
        isOpen={showDeleteConfirm}
        onClose={() => setShowDeleteConfirm(false)}
        onConfirm={confirmDeleteCharacter}
        title="Delete Character?"
        message={`Are you sure you want to remove "${itemToDelete?.name}" from this campaign?`}
      />
    </>
  );
}

// ============================================================================
// PARTICIPANT MANAGER COMPONENT
// ============================================================================

function ParticipantManager({ encounter, encounterPath, campaignCharacters, campaignId }) {
  const { showToast, showInfo } = useUIFeedback();
  const [participantName, setParticipantName] = useState('');
  const [participantType, setParticipantType] = useState('monster');
  const [selectedCharacterId, setSelectedCharacterId] = useState('');
  const [monsterInitMod, setMonsterInitMod] = useState(MONSTER_DEFAULT_INIT_MOD);
  const [maxHp, setMaxHp] = useState('');
  const [hpFormula, setHpFormula] = useState('');
  const [manualInitiative, setManualInitiative] = useState('');
  const [monsterAc, setMonsterAc] = useState('');
  const [asNpc, setAsNpc] = useState(false);
  const combatActive = encounter.isStarted && !encounter.isPaused;
  const [addSectionOpen, setAddSectionOpen] = useState(!combatActive);
  const [editingParticipant, setEditingParticipant] = useState(null);
  const [hpChangeValues, setHpChangeValues] = useState({});
  const [draggedItemId, setDraggedItemId] = useState(null);
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [itemToDelete, setItemToDelete] = useState(null);
  const [lastRollDetails, setLastRollDetails] = useState(null);
  const [openConditionsId, setOpenConditionsId] = useState(null);
  const [customConditionInput, setCustomConditionInput] = useState('');

  // Per-campaign custom conditions (DM-added freeform strings).
  const { data: campaignDoc } = useFirestoreDocument(campaignId ? getPath.campaign(campaignId) : null);
  const customConditions = (campaignDoc && campaignDoc.customConditions) || [];
  // Merged condition list: built-ins + custom. Custom = { id: label, label, emoji: '' }.
  // id may contain spaces/emoji — toggleCondition stores raw string.
  const allConditions = [
    ...CONDITIONS,
    ...customConditions.map(label => ({ id: label, label, emoji: '' })),
  ];

  const participants = encounter.participants || [];
  const [editingField, setEditingField] = useState(null);
  const [editingId, setEditingId] = useState(null);
  const blurTimeoutRef = useRef(null);
  const handleFieldFocus = (id, label) => {
    if (blurTimeoutRef.current) { clearTimeout(blurTimeoutRef.current); blurTimeoutRef.current = null; }
    setEditingId(id);
    setEditingField(label);
  };
  const handleFieldBlur = () => {
    blurTimeoutRef.current = setTimeout(() => { setEditingField(null); setEditingId(null); }, 150);
  };

  useEffect(() => {
    if (participantType === 'character' && selectedCharacterId) {
      const selectedChar = campaignCharacters.find(c => c.id === selectedCharacterId);
      if (selectedChar && selectedChar.defaultMaxHp) {
        setMaxHp(selectedChar.defaultMaxHp);
      } else {
        setMaxHp('');
      }
      setAsNpc(false);
    } else if (participantType === 'monster') {
      setMaxHp('');
      setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
    }
  }, [selectedCharacterId, participantType, campaignCharacters]);

  const handleAddParticipant = async () => {
    if (!db) return;
    if (participantType === 'monster' && !participantName.trim()) return;
    if (participantType === 'character' && !selectedCharacterId) return;

    let nameToAdd = participantName.trim();
    const initiativeRoll = rollD20();
    let modifier = 0;
    let currentMaxHp = parseInt(maxHp, 10) || DEFAULT_MAX_HP;
    let participantAsNpc = false;
    const manualInit = manualInitiative !== '' && !isNaN(parseInt(manualInitiative, 10));
    const finalInitiative = manualInit ? parseInt(manualInitiative, 10) : null;

    if (participantType === 'character') {
      const character = campaignCharacters.find(c => c.id === selectedCharacterId);
      if (!character) {
        return;
      }
      if (participants.some(p => p.type === 'character' && p.originalCharacterId === selectedCharacterId)) {
        showInfo(`${character.name} is already in this encounter.`);
        return;
      }
      nameToAdd = character.name;
      currentMaxHp = character.defaultMaxHp || currentMaxHp;
      modifier = character.defaultInitMod || 0;
    } else {
      modifier = parseInt(monsterInitMod, 10) || 0;
      participantAsNpc = asNpc;
      // maxHp wins if both set; else roll formula
      if (!maxHp && hpFormula.trim()) {
        const rolled = shared.rollHpFormula(hpFormula);
        if (rolled) currentMaxHp = rolled;
      }
    }

    const computedInitiative = manualInit ? finalInitiative : (initiativeRoll + modifier);
    const newParticipant = {
      id: generateId(),
      name: nameToAdd,
      type: participantType === 'monster' && participantAsNpc ? 'npc' : participantType,
      originalCharacterId: participantType === 'character' ? selectedCharacterId : null,
      initiative: computedInitiative,
      maxHp: currentMaxHp,
      currentHp: currentMaxHp,
      hpFormula: participantType === 'monster' ? (hpFormula.trim() || null) : null,
      ac: participantType === 'monster' && monsterAc !== '' ? parseInt(monsterAc, 10) : (participantType === 'character' ? (campaignCharacters.find(c => c.id === selectedCharacterId)?.defaultAc ?? null) : null),
      conditions: [],
      isActive: true,
      status: 'conscious',
      deathSaveSuccesses: 0,
      deathSaveFailures: 0,
    };

    try {
      await addParticipant(encounter, newParticipant, buildCtx(encounterPath));

      setLastRollDetails({
        name: nameToAdd,
        roll: manualInit ? null : initiativeRoll,
        mod: manualInit ? null : modifier,
        total: computedInitiative,
        type: participantAsNpc ? 'NPC' : participantType,
        manual: manualInit,
      });
      setTimeout(() => setLastRollDetails(null), ROLL_DISPLAY_DURATION);

      setParticipantName('');
      setMaxHp('');
      setHpFormula('');
      setSelectedCharacterId('');
      setMonsterInitMod(MONSTER_DEFAULT_INIT_MOD);
      setMonsterAc('');
      setAsNpc(false);
      setManualInitiative('');
    } catch (err) {
      showToast("Failed to add participant. Please try again.");    }
  };

  const handleAddAllCampaignCharacters = async () => {
    if (!db || !campaignCharacters || campaignCharacters.length === 0) return;

    const existingCharacterIds = participants
      .filter(p => p.type === 'character' && p.originalCharacterId)
      .map(p => p.originalCharacterId);

    const newParticipants = campaignCharacters
      .filter(char => !existingCharacterIds.includes(char.id))
      .map(char => {
        const initiativeRoll = rollD20();
        const modifier = char.defaultInitMod || 0;
        const finalInitiative = initiativeRoll + modifier;
        return {
          id: generateId(),
          name: char.name,
          type: 'character',
          originalCharacterId: char.id,
          initiative: finalInitiative,
          maxHp: char.defaultMaxHp || DEFAULT_MAX_HP,
          currentHp: char.defaultMaxHp || DEFAULT_MAX_HP,
          conditions: [],
          isActive: true,
          status: 'conscious',
          deathSaveSuccesses: 0,
          deathSaveFailures: 0,
        };
      });

    if (newParticipants.length === 0) {
      showInfo("All campaign characters are already in this encounter.");      return;
    }

    try {
      await addParticipants(encounter, newParticipants, buildCtx(encounterPath));
    } catch (err) {
      showToast("Failed to add all characters. Please try again.");    }
  };

  const handleUpdateParticipant = async (updatedData) => {
    if (!db || !editingParticipant) return;
    try {
      await updateParticipant(encounter, editingParticipant.id, updatedData, buildCtx(encounterPath));
      setEditingParticipant(null);
    } catch (err) {
      showToast("Failed to update participant. Please try again.");    }
  };

  // Inline initiative edit (FEAT-3): blur/Enter commits. Reslots participant
  // into correct list position (stable sort by init desc, tie-break original
  // index). Display + AdminView both reflect new order. Pre-combat only —
  // field gated to !started||paused elsewhere.
  const handleInlineInitiative = async (participantId, value) => {
    if (!db) return;
    const n = parseInt(value, 10);
    if (isNaN(n)) return;
    try {
      await updateParticipant(encounter, participantId, { initiative: n }, buildCtx(encounterPath));
    } catch (err) {
      // fall through silently
    }
  };

  const requestDeleteParticipant = (participantId, participantName) => {
    setItemToDelete({ id: participantId, name: participantName });
    setShowDeleteConfirm(true);
  };

  const confirmDeleteParticipant = async () => {
    if (!db || !itemToDelete) return;
    try {
      await removeParticipant(encounter, itemToDelete.id, buildCtx(encounterPath));
    } catch (err) {
      showToast("Failed to delete participant. Please try again.");    }
    setShowDeleteConfirm(false);
    setItemToDelete(null);
  };

  const toggleParticipantActive = async (participantId) => {
    if (!db) return;
    try {
      await combatToggleActive(encounter, participantId, buildCtx(encounterPath));
    } catch (err) {
      // fall through silently
    }
  };

  const applyHpChange = async (participantId, changeType) => {
    if (!db) return;
    const amountStr = hpChangeValues[participantId];
    if (!amountStr || amountStr.trim() === '') return;
    const amount = parseInt(amountStr, 10);
    if (isNaN(amount) || amount === 0) {
      setHpChangeValues(prev => ({ ...prev, [participantId]: '' }));
      return;
    }
    try {
      await combatApplyHpChange(encounter, participantId, changeType, amount, buildCtx(encounterPath));
      setHpChangeValues(prev => ({ ...prev, [participantId]: '' }));
    } catch (err) {
      // fall through silently
    }
  };

  const handleInlineCurrentHp = async (participantId, value) => {
    let n = parseInt(value, 10);
    if (isNaN(n)) return;
    const p = participants.find(pp => pp.id === participantId);
    if (!p || n === p.currentHp) return;
    try {
      const isGeneric = (encounter.ruleset === 'generic');
      let patch;
      if (!isGeneric && n < 0) n = 0;
      if (isGeneric) {
        patch = { currentHp: n, status: n > 0 ? 'conscious' : 'down' };
      } else {
        patch = { currentHp: n, deathSaveSuccesses: 0, deathSaveFailures: 0 };
        if (n > 0) { patch.status = 'conscious'; }
        else if (p.type === 'monster') { patch.status = 'dead'; patch.isActive = false; }
        else { patch.status = 'dying'; }
      }
      await updateParticipant(encounter, participantId, patch, buildCtx(encounterPath));
    } catch (err) {
      showToast('Failed to update HP.');
    }
  };

  const handleInlineMaxHp = async (participantId, value) => {
    const n = parseInt(value, 10);
    if (isNaN(n)) return;
    const p = participants.find(pp => pp.id === participantId);
    if (!p || n === p.maxHp || n <= 0) return;
    try {
      await updateParticipant(encounter, participantId, { maxHp: n }, buildCtx(encounterPath));
    } catch (err) {
      showToast('Failed to update Max HP.');
    }
  };

  const handleInlineAc = async (participantId, value) => {
    const n = parseInt(value, 10);
    if (isNaN(n)) return;
    const p = participants.find(pp => pp.id === participantId);
    if (!p || n === p.ac) return;
    try {
      await updateParticipant(encounter, participantId, { ac: n }, buildCtx(encounterPath));
    } catch (err) {
      showToast('Failed to update AC.');
    }
  };

  const handleInlineTempHp = async (participantId, value) => {
    const n = Math.max(0, parseInt(value, 10) || 0);
    try {
      await updateParticipant(encounter, participantId, { tempHp: n }, buildCtx(encounterPath));
    } catch (err) {
      showToast('Failed to update Temp HP.');
    }
  };

  const handleDeathSaveChange = async (participantId, outcome) => {
    if (!db) return;
    try {
      await combatDeathSave(encounter, participantId, outcome, buildCtx(encounterPath));
    } catch (err) {
      showToast(err.message);
    }
  };

  const handleNat20 = async (participantId) => handleDeathSaveChange(participantId, 'nat20');

  const handleNat1 = async (participantId) => handleDeathSaveChange(participantId, 'nat1');

  const handleStabilize = async (participantId) => {
    if (!db) return;
    try {
      await stabilizeParticipant(encounter, participantId, buildCtx(encounterPath));
    } catch (err) {
      showToast(err.message);
    }
  };

  const handleRevive = async (participantId) => {
    if (!db) return;
    try {
      await reviveParticipant(encounter, participantId, buildCtx(encounterPath));
    } catch (err) {
      showToast(err.message);
    }
  };

  const handleMarkDead = async (participantId) => {
    if (!db) return;
    try {
      await markDead(encounter, participantId, buildCtx(encounterPath));
    } catch (err) {
      showToast(err.message);
    }
  };

  const handleCritDamage = async (participantId) => {
    if (!db) return;
    try {
      // Any damage at 0 HP + isCriticalHit = 2 death-save failures (5e crit-at-0).
      await combatApplyHpChange(encounter, participantId, 'damage', 1, { isCriticalHit: true }, buildCtx(encounterPath));
    } catch (err) {
      showToast(err.message);
    }
  };

  const toggleCondition = async (participantId, conditionId) => {
    if (!db) return;
    try {
      await combatToggleCondition(encounter, participantId, conditionId, buildCtx(encounterPath));
    } catch (err) {
      // fall through silently
    }
  };

  // Add freeform condition: persist to campaign palette + apply to current participant.
  const addCustomCondition = async (label, participantId = null) => {
    const trimmed = (label || '').trim();
    if (!trimmed || !campaignId) return;
    setCustomConditionInput('');
    const exists = allConditions.some(c => c.label.toLowerCase() === trimmed.toLowerCase());
    if (!exists) {
      try {
        await storage.updateDoc(getPath.campaign(campaignId), { customConditions: [...customConditions, trimmed] });
      } catch (err) {}
    }
    if (participantId) {
      const cur = (encounter.participants.find(p => p.id === participantId)?.conditions) || [];
      if (!cur.includes(trimmed)) {
        try {
          await combatToggleCondition(encounter, participantId, trimmed, buildCtx(encounterPath));
        } catch (err) {}
      }
    }
  };

  const handleDragStart = (e, id) => {
    setDraggedItemId(id);
    e.dataTransfer.effectAllowed = 'move';
  };

  const handleDragOver = (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  };

  const handleDrop = async (e, targetId) => {
    e.preventDefault();
    if (!db || draggedItemId === null || draggedItemId === targetId) {
      setDraggedItemId(null);
      return;
    }
    try {
      await reorderParticipants(encounter, draggedItemId, targetId, buildCtx(encounterPath));
    } catch (err) {
      // drag invalid (id not found) — ignore
    }
    setDraggedItemId(null);
  };

  // 1-list model: participants[] IS the display order. No re-sort.
  const sortedParticipants = participants;

  const initiativeGroups = participants.reduce((acc, p) => {
    acc[p.initiative] = (acc[p.initiative] || 0) + 1;
    return acc;
  }, {});
  const tiedInitiatives = Object.keys(initiativeGroups)
    .filter(init => initiativeGroups[init] > 1)
    .map(Number);

  return (
    <>
      <div className="p-3 bg-stone-900 rounded-md mt-4">
        {(() => {
          const open = addSectionOpen && !combatActive;
          return (
          <details open={open} className="mt-2">
            <summary
              onClick={(e) => { if (!combatActive) { e.preventDefault(); setAddSectionOpen(v => !v); } else { e.preventDefault(); } }}
              className="cursor-pointer text-lg font-medium text-amber-200 font-cinzel tracking-wide flex items-center gap-2 select-none"
            >
              {open ? <ChevronDown size={20} /> : <ChevronRight size={20} />}
              Add Participants
              {combatActive && <span className="text-xs text-stone-400 font-normal ml-2">(pause to add)</span>}
            </summary>
            {open && (
              <>
              <div className="flex justify-end items-center mb-3 mt-2">
                <button
                  onClick={handleAddAllCampaignCharacters}
                  className="px-3 py-1.5 text-xs font-medium text-white bg-violet-700 hover:bg-violet-800 rounded-md transition-colors flex items-center"
                  disabled={!campaignCharacters || campaignCharacters.length === 0 || combatActive}
                >
                  <Users2 size={16} className="mr-1.5" />
                  <Dices size={16} className="mr-1.5" /> Add All (Roll Init)
                </button>
              </div>

        {/* Warning when combat is active */}
        {encounter.isStarted && !encounter.isPaused && (
          <div className="mb-4 p-3 bg-yellow-900 bg-opacity-30 border border-yellow-600 rounded-md flex items-start">
            <AlertTriangle size={20} className="text-yellow-400 mr-3 flex-shrink-0 mt-0.5" />
            <div>
              <p className="text-yellow-300 font-semibold text-sm">Combat is Running</p>
              <p className="text-yellow-200 text-xs mt-1">
                Pause combat to add or remove participants. The turn order will be recalculated when combat is resumed.
              </p>
            </div>
          </div>
        )}

        <form
          onSubmit={(e) => {
            e.preventDefault();
            handleAddParticipant();
          }}
          className="grid grid-cols-1 md:grid-cols-12 gap-x-4 gap-y-2 mb-4 p-3 bg-stone-800 rounded items-end"
        >
          <div className="md:col-span-2">
            <label className="block text-sm font-medium text-stone-300">Type</label>
            <select
              value={participantType}
              onChange={(e) => {
                setParticipantType(e.target.value);
                setSelectedCharacterId('');
                setAsNpc(false);
              }}
              className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
            >
              <option value="monster">Monster</option>
              <option value="character">Character</option>
            </select>
          </div>

          {participantType === 'monster' ? (
            <>
              <div className="md:col-span-10">
                <label htmlFor="monsterName" className="block text-sm font-medium text-stone-300">
                  Monster Name
                </label>
                <input
                  type="text"
                  id="monsterName"
                  value={participantName}
                  onChange={(e) => setParticipantName(e.target.value)}
                  placeholder="e.g., Dire Wolf"
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-2">
                <label htmlFor="monsterInitMod" className="block text-sm font-medium text-stone-300">
                  Init Mod
                </label>
                <input
                  type="number"
                  id="monsterInitMod"
                  value={monsterInitMod}
                  onChange={(e) => setMonsterInitMod(e.target.value)}
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-2">
                <label htmlFor="manualInitiative" className="block text-sm font-medium text-stone-300">
                  Initiative <span className="text-xs text-stone-400">(blank=roll)</span>
                </label>
                <input
                  type="number"
                  id="manualInitiative"
                  value={manualInitiative}
                  onChange={(e) => setManualInitiative(e.target.value)}
                  placeholder="auto"
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-2">
                <label htmlFor="monsterAc" className="block text-sm font-medium text-stone-300">AC</label>
                <input
                  type="number"
                  id="monsterAc"
                  value={monsterAc}
                  onChange={(e) => setMonsterAc(e.target.value)}
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-2">
                <label htmlFor="monsterMaxHp" className="block text-sm font-medium text-stone-300">
                  Max HP
                </label>
                <input
                  type="number"
                  id="monsterMaxHp"
                  value={maxHp}
                  onChange={(e) => setMaxHp(e.target.value)}
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-4">
                <label htmlFor="monsterHpFormula" className="block text-sm font-medium text-stone-300">
                  HP Formula
                </label>
                <input
                  type="text"
                  id="monsterHpFormula"
                  value={hpFormula}
                  onChange={(e) => setHpFormula(e.target.value)}
                  placeholder="e.g. 2d6+9"
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-12 flex items-center pt-2">
                <input
                  type="checkbox"
                  id="asNpc"
                  checked={asNpc}
                  onChange={(e) => setAsNpc(e.target.checked)}
                  className="h-4 w-4 text-violet-600 border-stone-400 rounded focus:ring-violet-500"
                />
                <label htmlFor="asNpc" className="ml-2 block text-sm text-stone-300">
                  Is NPC?
                </label>
              </div>
            </>
          ) : (
            <>
              <div className="md:col-span-4">
                <label className="block text-sm font-medium text-stone-300">Select Character</label>
                <select
                  value={selectedCharacterId}
                  onChange={(e) => setSelectedCharacterId(e.target.value)}
                  className="mt-1 w-full px-3 py-2 bg-stone-700 border-stone-600 rounded text-white"
                >
                  <option value="">-- Select from Campaign --</option>
                  {campaignCharacters.filter(c => !participants.some(p => p.type === 'character' && p.originalCharacterId === c.id)).map(c => (
                    <option key={c.id} value={c.id}>
                      {c.name} (HP: {c.defaultMaxHp || 'N/A'}, Mod: {formatInitMod(c.defaultInitMod)})
                    </option>
                  ))}
                </select>
              </div>
              <div className="md:col-span-2">
                <label className="block text-sm font-medium text-stone-300">Max HP (Encounter)</label>
                <input
                  type="number"
                  value={maxHp}
                  onChange={(e) => setMaxHp(e.target.value)}
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
              <div className="md:col-span-2">
                <label htmlFor="charManualInitiative" className="block text-sm font-medium text-stone-300">
                  Initiative <span className="text-xs text-stone-400">(blank=roll)</span>
                </label>
                <input
                  type="number"
                  id="charManualInitiative"
                  value={manualInitiative}
                  onChange={(e) => setManualInitiative(e.target.value)}
                  placeholder="auto"
                  className="mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
                />
              </div>
            </>
          )}

          <div className="md:col-span-12 flex justify-end mt-2">
            <button
              type="submit"
              disabled={encounter.isStarted && !encounter.isPaused}
              className={`px-4 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center ${encounter.isStarted && !encounter.isPaused ? 'bg-stone-600 cursor-not-allowed opacity-50' : 'bg-red-700 hover:bg-red-800'}`}
              title={encounter.isStarted && !encounter.isPaused ? 'Pause combat to add participants' : 'Add participant and roll initiative'}
            >
              <Dices size={18} className="mr-1.5" /> Add to Encounter (Roll Init)
            </button>
          </div>
        </form>
              </>
            )}
          </details>
          );
        })()}

        {participants.length === 0 && <p className="text-sm text-stone-400">No participants added yet.</p>}


        <ul className="space-y-2">
          {sortedParticipants.map((p) => {
            const isCurrentTurn = encounter.isStarted && p.id === encounter.currentTurnParticipantId;
            const isDraggable = (!encounter.isStarted || encounter.isPaused) && tiedInitiatives.includes(Number(p.initiative));
            const participantDisplayType = p.type === 'npc' ? 'NPC' : (p.type === 'monster' ? 'Monster' : 'Character');
            const isGeneric = (encounter.ruleset || '5e') === 'generic';
            const hasDeathSaves = !isGeneric && (p.type === 'character' || p.type === 'npc');

            let bgColor = !p.isActive ? 'bg-stone-800' : (p.type === 'character' ? 'bg-indigo-950' : 'bg-[#8e351c]');
            if (isCurrentTurn && !encounter.isPaused) bgColor = 'bg-green-600';

            const participantStatus = p.status || (isGeneric
              ? (p.currentHp <= 0 ? 'down' : 'conscious')
              : (p.currentHp === 0 ? (p.type === 'monster' ? 'dead' : 'dying') : 'conscious'));
            const isZeroHp = p.currentHp <= 0;
            const isDead = participantStatus === 'dead';
            const isDown = participantStatus === 'down';

            return (
              <li
                key={p.id}
                draggable={isDraggable}
                onDragStart={isDraggable ? (e) => handleDragStart(e, p.id) : undefined}
                onDragOver={isDraggable ? handleDragOver : undefined}
                onDrop={isDraggable ? (e) => handleDrop(e, p.id) : undefined}
                onDragEnd={() => setDraggedItemId(null)}
                className={`relative p-3 rounded-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-2 transition-all duration-300 ${bgColor} ${isCurrentTurn && !encounter.isPaused ? 'ring-2 ring-green-300 shadow-lg' : ''} ${!p.isActive ? 'ring-2 ring-stone-500' : (isDead ? 'opacity-90 brightness-75 saturate-125' : '')} ${isDraggable ? 'cursor-grab' : ''} ${draggedItemId === p.id ? 'opacity-50 ring-2 ring-yellow-400' : ''} ${editingId === p.id ? 'ring-2 ring-amber-400' : ''}`}
              >
                {editingId === p.id && (
                  <span className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center">
                    <span className="px-4 py-1 bg-amber-600 rounded-full text-white text-sm font-bold shadow-lg">
                      ✎ Editing {editingField}
                    </span>
                  </span>
                )}
                <div className="flex-1 flex items-start sm:items-center">
                  {isDraggable && (
                    <ChevronsUpDown
                      size={18}
                      className="mr-2 text-stone-400 flex-shrink-0"
                      title="Drag to reorder in tie"
                    />
                  )}
                  <div className="flex-1">
                    <p className="font-semibold text-lg text-white">
                      {isZeroHp && <span className="mr-2">☠️</span>}
                      {p.name} <span className="text-xs">({participantDisplayType})</span>
                      {p.ac != null && (
                        <span className="ml-2 inline-flex items-baseline gap-1 px-2 py-0.5 bg-sky-900 border border-sky-500 rounded">
                          <span className="text-[10px] text-sky-300 font-semibold tracking-wide">AC</span>
                          <input
                            type="number"
                            defaultValue={p.ac}
                            key={`ac-${p.ac}`}
                            onFocus={(e) => { e.target.select(); handleFieldFocus(p.id, 'AC'); }}
                            onBlur={(e) => { if (e.target.value !== String(p.ac)) handleInlineAc(p.id, e.target.value); handleFieldBlur(); }}
                            onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') e.target.value = p.ac; }}
                            className="w-6 bg-transparent text-sky-100 text-lg font-bold leading-none text-center focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
                            aria-label={`AC for ${p.name}`}
                          />
                        </span>
                      )}
                      {isCurrentTurn && !encounter.isPaused && (
                        <span className="ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center">
                          <Zap size={12} className="mr-1" /> CURRENT
                        </span>
                      )}
                      {hasDeathSaves && participantStatus === 'dying' && <span className="ml-2 text-xs text-red-300 font-semibold animate-pulse">(Dying)</span>}
                      {hasDeathSaves && participantStatus === 'stable' && (p.conditions || []).includes('unconscious') && <span className="ml-2 text-xs text-emerald-300 font-semibold">(Unconscious)</span>}
                      {isDown && <span className="ml-2 text-xs text-red-300 font-semibold animate-pulse">(Down)</span>}
                      {isDead && <span className="ml-2 px-2 py-0.5 rounded bg-red-950 border border-red-500 text-red-200 text-xs font-bold">DEAD</span>}
                    </p>
                    <div className={`text-sm ${isCurrentTurn && !encounter.isPaused ? 'text-green-100' : 'text-stone-200'} flex items-center gap-2`}>
                      <span className="inline-flex items-center px-0.5 border border-amber-500 bg-stone-950 text-amber-300 rounded">
                        <label htmlFor={`init-${p.id}`} className="sr-only">Initiative</label>
                        <input
                          type="number"
                          id={`init-${p.id}`}
                          defaultValue={p.initiative}
                          key={`init-${p.initiative}`}
                          min="0"
                          max="99"
                          disabled={encounter.isStarted && !encounter.isPaused}
                          onChange={(e) => { if (e.target.value.length > 2) e.target.value = e.target.value.slice(0, 2); }}
                          onFocus={(e) => { e.target.select(); handleFieldFocus(p.id, 'Initiative'); }}
                          onBlur={(e) => { if (e.target.value !== String(p.initiative)) handleInlineInitiative(p.id, e.target.value); handleFieldBlur(); }}
                          onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); }}
                          className="w-7 bg-transparent text-amber-300 text-lg font-bold text-center focus:outline-none disabled:text-amber-300 disabled:opacity-100 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
                          aria-label={`Initiative for ${p.name}`}
                        />
                      </span>
                      <span className="inline-flex items-baseline">
                        <input
                          type="number"
                          id={`hp-${p.id}`}
                          defaultValue={p.currentHp}
                          key={`hp-${p.currentHp}`}
                          onFocus={(e) => { e.target.select(); handleFieldFocus(p.id, 'HP'); }}
                          onBlur={(e) => { if (e.target.value !== String(p.currentHp)) handleInlineCurrentHp(p.id, e.target.value); handleFieldBlur(); }}
                          onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') e.target.value = p.currentHp; }}
                          className="w-10 bg-transparent text-white text-base text-center border-b border-transparent hover:border-stone-500 focus:border-red-500 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
                          aria-label={`Current HP for ${p.name}`}
                        />
                        <span className="text-stone-500 text-base">/</span>
                        <input
                          type="number"
                          defaultValue={p.maxHp}
                          key={`maxhp-${p.maxHp}`}
                          onFocus={(e) => { e.target.select(); handleFieldFocus(p.id, 'Max HP'); }}
                          onBlur={(e) => { if (e.target.value !== String(p.maxHp)) handleInlineMaxHp(p.id, e.target.value); handleFieldBlur(); }}
                          onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') e.target.value = p.maxHp; }}
                          className="w-10 bg-transparent text-stone-200 text-base text-center border-b border-transparent hover:border-stone-400 focus:border-red-500 focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
                          aria-label={`Max HP for ${p.name}`}
                        />
                      </span>
                      <span className="inline-flex items-baseline gap-0.5 ml-1" title="Temporary HP">
                        <span className="text-[10px] text-cyan-400 font-semibold tracking-wide">+</span>
                        <input
                          type="number"
                          defaultValue={p.tempHp || 0}
                          key={`temphp-${p.tempHp || 0}`}
                          onFocus={(e) => { e.target.select(); handleFieldFocus(p.id, 'Temp HP'); }}
                          onBlur={(e) => { if (e.target.value !== String(p.tempHp || 0)) handleInlineTempHp(p.id, e.target.value); handleFieldBlur(); }}
                          onKeyDown={(e) => { if (e.key === 'Enter') e.target.blur(); if (e.key === 'Escape') e.target.value = p.tempHp || 0; }}
                          className={`w-7 bg-transparent text-base text-center border-b focus:outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none ${(p.tempHp || 0) > 0 ? 'text-cyan-200 border-cyan-500' : 'text-stone-300 border-transparent hover:border-stone-400'}`}
                          aria-label={`Temp HP for ${p.name}`}
                        />
                      </span>
                      <span className="ml-auto inline-flex items-center gap-1">
                        {participantStatus === 'dead' && (
                          <button onClick={() => handleRevive(p.id)} className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-emerald-800 hover:bg-emerald-700 text-white" title="Revive (clear dead status)">
                            <HeartPulse size={12} /> Revive
                          </button>
                        )}
                        {isGeneric && !isDead && (
                          <button onClick={() => handleMarkDead(p.id)} className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-red-900 hover:bg-red-800 text-white" title="Mark dead">
                            <HeartCrack size={12} /> Mark Dead
                          </button>
                        )}
                      </span>
                    </div>
                    
                    {hasDeathSaves && participantStatus === 'stable' && (
                      <div className="mt-2 text-xs text-emerald-300/80 italic">Stable — regains 1 HP after 1d4 hours</div>
                    )}

                    {/* Death saves - D&D 5e status state machine */}
                    {hasDeathSaves && encounter.isStarted && participantStatus !== 'conscious' && (
                      <div className="mt-2 flex flex-col space-y-1">
                        {participantStatus === 'dying' && (
                          <>
                            <div className="flex items-center space-x-2">
                              <span className="text-xs text-emerald-300 font-medium w-10">Saves:</span>
                              {[1, 2, 3].map(saveNum => (
                                <span
                                  key={saveNum}
                                  className={`w-6 h-6 rounded border-2 flex items-center justify-center ${(p.deathSaveSuccesses || 0) >= saveNum ? 'bg-emerald-600 border-emerald-500' : 'bg-stone-800 border-stone-600'}`}
                                  title={`Success pip ${saveNum}`}
                                >
                                  {(p.deathSaveSuccesses || 0) >= saveNum && <span className="text-white text-sm">✓</span>}
                                </span>
                              ))}
                            </div>
                            <div className="flex items-center space-x-2">
                              <span className="text-xs text-red-300 font-medium w-10">Fails:</span>
                              {[1, 2, 3].map(failNum => (
                                <span
                                  key={failNum}
                                  className={`w-6 h-6 rounded border-2 flex items-center justify-center ${(p.deathSaveFailures || 0) >= failNum ? 'bg-red-600 border-red-500' : 'bg-stone-800 border-stone-600'}`}
                                  title={`Fail pip ${failNum}`}
                                >
                                  {(p.deathSaveFailures || 0) >= failNum && <span className="text-white text-sm">✕</span>}
                                </span>
                              ))}
                            </div>
                            <div className="flex gap-2 mt-1">
                              <button onClick={() => handleDeathSaveChange(p.id, 'success')} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-emerald-700 hover:bg-emerald-600 text-white" title="Death save success">
                                Success
                              </button>
                              <button onClick={() => handleDeathSaveChange(p.id, 'fail')} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-red-700 hover:bg-red-600 text-white" title="Death save failure">
                                Fail
                              </button>
                              <button onClick={() => handleNat1(p.id)} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-red-800 hover:bg-red-700 text-white" title="Natural 1: +2 failures">
                                <AlertTriangle size={12} /> Nat1
                              </button>
                              <button onClick={() => handleNat20(p.id)} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-yellow-600 hover:bg-yellow-500 text-white" title="Natural 20: 1 HP + conscious">
                                <Zap size={12} /> Nat20
                              </button>
                              <button onClick={() => handleStabilize(p.id)} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-emerald-700 hover:bg-emerald-600 text-white" title="Stabilize at 0 HP">
                                <HeartPulse size={12} /> Stabilize
                              </button>
                            </div>
                          </>
                        )}
                      </div>
                    )}

                    {/* Active condition badges */}
                    {(p.conditions || []).length > 0 && (
                      <div className="mt-1 flex flex-wrap gap-1">
                        {(p.conditions || []).map(cId => {
                          const cond = allConditions.find(c => c.id === cId);
                          return cond ? (
                            <span
                              key={cId}
                              className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-purple-900 border border-purple-600 text-xs text-purple-200 cursor-pointer hover:bg-purple-800"
                              title={`Remove ${cond.label}`}
                              onClick={() => toggleCondition(p.id, cId)}
                            >
                              {cond.emoji} {cond.label}
                            </span>
                          ) : (
                            <span
                              key={cId}
                              className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full bg-purple-900 border border-purple-600 text-xs text-purple-200 cursor-pointer hover:bg-purple-800"
                              title={`Remove ${cId}`}
                              onClick={() => toggleCondition(p.id, cId)}
                            >
                              {cId}
                            </span>
                          );
                        })}
                      </div>
                    )}

                    {/* Expandable conditions picker */}
                    {openConditionsId === p.id && (
                      <div className="mt-2 p-2 bg-stone-900 rounded-md border border-stone-600">
                        <p className="text-xs text-stone-400 mb-1 font-medium">Toggle Conditions</p>
                        <div className="flex flex-wrap gap-1">
                          {allConditions.map(cond => {
                            const active = (p.conditions || []).includes(cond.id);
                            return (
                              <button
                                key={cond.id}
                                onClick={() => toggleCondition(p.id, cond.id)}
                                className={`px-2 py-1 rounded text-xs transition-colors ${active ? 'bg-purple-700 border border-purple-400 text-white' : 'bg-stone-700 border border-stone-500 text-stone-300 hover:bg-stone-600'}`}
                                title={cond.label}
                              >
                                {cond.emoji} {cond.label}
                              </button>
                            );
                          })}
                        </div>
                        {/* Add custom condition (per-campaign, freeform string) */}
                        <div className="mt-2 flex gap-1">
                          <input
                            type="text"
                            value={customConditionInput}
                            onChange={(e) => setCustomConditionInput(e.target.value)}
                            onKeyDown={(e) => {
                              if (e.key === 'Enter') { e.preventDefault(); addCustomCondition(customConditionInput, p.id); }
                            }}
                            placeholder="Add custom condition..."
                            className="flex-grow px-2 py-1 text-xs rounded bg-stone-800 border border-stone-600 text-stone-200 placeholder-stone-500 focus:outline-none focus:border-amber-500"
                            maxLength={40}
                          />
                          <button
                            type="button"
                            onClick={() => addCustomCondition(customConditionInput, p.id)}
                            disabled={!customConditionInput.trim()}
                            className="px-2 py-1 rounded text-xs bg-amber-700 border border-amber-500 text-white hover:bg-amber-600 disabled:opacity-40 disabled:cursor-not-allowed"
                          >
                            Add
                          </button>
                        </div>
                      </div>
                    )}
                  </div>
                </div>

                <div className="flex flex-wrap items-center space-x-2 mt-2 sm:mt-0">
                  {hasDeathSaves && (participantStatus === 'dying' || participantStatus === 'stable') && encounter.isStarted && (
                    <button onClick={() => handleCritDamage(p.id)} className="inline-flex items-center gap-1 px-2 py-1 text-xs rounded bg-red-900 hover:bg-red-800 text-red-100 border border-red-700" title="Critical hit at 0 HP: +2 death-save failures">
                      <Crosshair size={12} />Crit
                    </button>
                  )}
                  {encounter.isStarted && (
                    <div className="flex items-center space-x-1 bg-stone-800 p-1 rounded-md">
                      <input
                        type="number"
                        placeholder="HP"
                        value={hpChangeValues[p.id] || ''}
                        onChange={(e) => setHpChangeValues(prev => ({ ...prev, [p.id]: e.target.value }))}
                        className="w-16 p-1 text-sm bg-stone-700 border border-stone-600 rounded-md text-white focus:ring-amber-600 focus:border-amber-600"
                        aria-label={`HP change for ${p.name}`}
                      />
                      {!isDead && (
                        <button
                          onClick={() => applyHpChange(p.id, 'damage')}
                          className="p-1 bg-red-500 hover:bg-red-600 text-white rounded-md text-xs"
                          title="Damage"
                        >
                          <HeartCrack size={16} />
                        </button>
                      )}
                      <button
                        onClick={() => applyHpChange(p.id, 'heal')}
                        className="p-1 bg-emerald-600 hover:bg-emerald-700 text-white rounded-md text-xs"
                        title={isDead ? "Heal / Revive" : "Heal"}
                      >
                        <HeartPulse size={16} />
                      </button>
                    </div>
                  )}
                  <button
                    onClick={() => toggleParticipantActive(p.id)}
                    className={`p-1 rounded transition-all ring-2 ${p.isActive ? 'bg-emerald-700 hover:bg-emerald-600 text-white ring-emerald-300' : 'bg-red-900 hover:bg-red-800 text-red-100 ring-red-500'}`}
                    title={p.isActive ? "Participant active — click to disable" : "Participant inactive — click to enable"}
                    aria-label={p.isActive ? "Disable participant" : "Enable participant"}
                    aria-pressed={p.isActive}
                  >
                    {p.isActive ? <UserCheck size={18} /> : <UserX size={18} />}
                  </button>
                  <button
                    onClick={() => setOpenConditionsId(openConditionsId === p.id ? null : p.id)}
                    className={`p-1 rounded transition-all ${openConditionsId === p.id ? 'bg-purple-700 text-white ring-2 ring-purple-300' : (p.conditions || []).length > 0 ? 'bg-stone-700 text-purple-300 ring-1 ring-purple-500 hover:bg-stone-600' : 'bg-stone-700 text-stone-300 hover:bg-stone-600'}`}
                    title={openConditionsId === p.id ? 'Conditions open — click to close' : 'Open conditions'}
                    aria-label={openConditionsId === p.id ? 'Close conditions' : 'Open conditions'}
                    aria-expanded={openConditionsId === p.id}
                  >
                    <span className="text-base leading-none">✨</span>
                  </button>
                  <button
                    onClick={() => setEditingParticipant(editingParticipant?.id === p.id ? null : p)}
                    className={`p-1 rounded transition-all ${editingParticipant?.id === p.id ? 'bg-amber-500 text-stone-950 ring-2 ring-amber-200' : 'text-yellow-300 bg-stone-700 hover:bg-stone-600'}`}
                    title={editingParticipant?.id === p.id ? 'Editing — click to close' : 'Edit participant'}
                    aria-label={editingParticipant?.id === p.id ? 'Close participant editor' : 'Edit participant'}
                    aria-pressed={editingParticipant?.id === p.id}
                  >
                    <Edit3 size={18} />
                  </button>
                  <button
                    onClick={() => requestDeleteParticipant(p.id, p.name)}
                    className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
                    title="Remove"
                  >
                    <Trash2 size={18} />
                  </button>
                </div>
              </li>
            );
          })}
        </ul>

        {lastRollDetails && (
          <p className="text-sm text-green-400 mt-2 mb-2 text-center">
            {lastRollDetails.manual
              ? `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Set initiative ${lastRollDetails.total}`
              : `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Rolled d20 (${lastRollDetails.roll}) ${formatInitMod(lastRollDetails.mod)} = ${lastRollDetails.total} Initiative`
            }
          </p>
        )}


        {editingParticipant && (
          <EditParticipantModal
            participant={editingParticipant}
            onClose={() => setEditingParticipant(null)}
            onSave={handleUpdateParticipant}
          />
        )}
      </div>

      <ConfirmationModal
        isOpen={showDeleteConfirm}
        onClose={() => setShowDeleteConfirm(false)}
        onConfirm={confirmDeleteParticipant}
        title="Delete Participant?"
        message={`Are you sure you want to remove "${itemToDelete?.name}" from this encounter?`}
      />
    </>
  );
}

// ============================================================================
// INITIATIVE CONTROLS COMPONENT
// ============================================================================

function InitiativeControls({ campaignId, encounter, encounterPath }) {
  const { showToast, showInfo } = useUIFeedback();
  const [showEndConfirm, setShowEndConfirm] = useState(false);
  const { data: activeDisplayData } = useFirestoreDocument(getPath.activeDisplay());
  const hidePlayerHp = activeDisplayData?.hidePlayerHp ?? true;
  const hideNpcHp = activeDisplayData?.hideNpcHp ?? false;

  // Undo/redo queried on click. No live logs subscription during combat.
  const latestLogQuery = [where('encounterPath', '==', encounterPath), LOG_ORDER, limit(25)];
  const findUndoTarget = async (redo = false) => {
    const rows = (await storage.getCollection(getPath.logs(), latestLogQuery) || [])
      .filter(l => l.undo || l.undo_payload);
    if (!redo) return rows.find(l => !l.undone) || null;

    // Redo stack = contiguous undone entries at top of log (newest first).
    // If A3 then A2 undone, redo must replay A2 first, then A3.
    const undoneBlock = [];
    for (const row of rows) {
      if (!row.undone) break;
      undoneBlock.push(row);
    }
    return undoneBlock.length ? undoneBlock[undoneBlock.length - 1] : null;
  };

  const handleUndo = async () => {
    if (!db) return;
    try {
      if (storage.undo) {
        const undoTarget = await findUndoTarget(false);
        if (!undoTarget) { showToast('No action to undo.'); return; }
        const expanded = undoTarget.undo
          ? expandUndo(undoTarget, encounter) // lean: build patch from current enc
          : getUndo(undoTarget); // legacy
        if (!expanded) return;
        await storage.undo({ logPath: `${getPath.logs()}/${undoTarget.id}`, undo: expanded });
        // Restore character writeback (end_encounter sync)
        const cw = undoTarget.undo?.characterWriteback;
        const wbId = encounter?.campaignId || campaignId;
        if (cw && wbId) {
          try {
            const campDoc = await storage.getDoc(getPath.campaign(wbId));
            if (campDoc && Array.isArray(campDoc.players)) {
              const restored = campDoc.players.map(p => {
                const old = cw.find(c => c.id === p.id);
                return old ? { ...p, defaultMaxHp: old.defaultMaxHp, defaultAc: old.defaultAc, defaultCurrentHp: old.defaultCurrentHp } : p;
              });
              await storage.updateDoc(getPath.campaign(wbId), { players: restored });
            }
          } catch (e) { console.error('characterWriteback restore failed:', e); }
        }
      }
    } catch (err) {
      console.error('Error undoing action:', err);
      showToast('Failed to undo. The encounter may have changed or no longer exists.');
    }
  };

  const handleRedo = async () => {
    if (!db) return;
    try {
      if (storage.undo) {
        const redoTarget = await findUndoTarget(true);
        if (!redoTarget) { showToast('No action to redo.'); return; }
        const expanded = redoTarget.undo
          ? expandUndo(redoTarget, encounter)
          : getUndo(redoTarget);
        if (!expanded) return;
        await storage.undo({ logPath: `${getPath.logs()}/${redoTarget.id}`, undo: expanded, redo: true });
      }
    } catch (err) {
      console.error('Error redoing action:', err);
      showToast('Failed to redo.');
    }
  };

  const handleToggleHidePlayerHp = async () => {
    if (!db) return;
    try {
      await storage.setDoc(getPath.activeDisplay(), { hidePlayerHp: !hidePlayerHp }, { merge: true });
    } catch (err) {
      console.error("Error toggling hidePlayerHp:", err);
    }
  };

  const handleToggleHideNpcHp = async () => {
    if (!db) return;
    try {
      await storage.setDoc(getPath.activeDisplay(), { hideNpcHp: !hideNpcHp }, { merge: true });
    } catch (err) {
      console.error("Error toggling hideNpcHp:", err);
    }
  };

  const handleStartEncounter = async () => {
    if (!db || !encounter.participants || encounter.participants.length === 0) {
      showInfo("Add participants first.");      return;
    }

    try {
      await startEncounter(encounter, buildCtx(encounterPath));
      // Only claim player display if slot is empty or already showing this encounter.
      // Don't steal display from another live encounter.
      const displayEmpty = !activeDisplayData || !activeDisplayData.activeEncounterId;
      const displayIsOurs = activeDisplayData && activeDisplayData.activeCampaignId === campaignId && activeDisplayData.activeEncounterId === encounter.id;
      if (displayEmpty || displayIsOurs) {
        await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: campaignId, activeEncounterId: encounter.id }, { merge: true });
      }
    } catch (err) {
      showToast(err.message || "Failed to start encounter. Please try again.");    }
  };

  const handleTogglePause = async () => {
    if (!db || !encounter || !encounter.isStarted) return;
    try {
      await togglePause(encounter, buildCtx(encounterPath));
    } catch (err) {
      // fall through silently
    }
  };

  const handleNextTurn = async () => {
    if (!db || !encounter.isStarted || encounter.isPaused) return;
    if (!encounter.currentTurnParticipantId || !encounter.turnOrderIds || encounter.turnOrderIds.length === 0) return;

    try {
      await nextTurn(encounter, buildCtx(encounterPath));
    } catch (err) {
      // nextTurn throws if no active participants — auto-end combat
      if (err.message === 'Encounter not running.' || err.message === 'No active turn.') return;
      showInfo("No active participants left.");      await storage.updateDoc(encounterPath, {
        isStarted: false,
        isPaused: false,
        currentTurnParticipantId: null,
        round: encounter.round
      });
    }
  };

  const confirmEndEncounter = async () => {
    if (!db) return;
    try {
      const ctx = { ...buildCtx(encounterPath), campaignId };
      if (campaignId) {
        ctx.campaignPath = getPath.campaign(campaignId);
        try {
          ctx.campaign = await storage.getDoc(ctx.campaignPath);
        } catch {}
      }
      await endEncounter(encounter, ctx);
      // Only clear display if THIS encounter is the one showing.
      const displayIsThis = activeDisplayData &&
        activeDisplayData.activeCampaignId === campaignId &&
        activeDisplayData.activeEncounterId === encounter.id;
      if (displayIsThis) {
        await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null }, { merge: true });
      }
    } catch (err) {
      showToast("Failed to end encounter. Please try again.");    }

    setShowEndConfirm(false);
  };

  if (!encounter || !encounter.participants) return null;

  return (
    <>
      <div className="md:sticky md:top-2 p-3 bg-stone-900 rounded-md shadow-lg">
        <h4 className="text-base font-medium text-amber-200 mb-2 text-center font-cinzel tracking-wide">Combat Controls</h4>
        <div className="flex flex-col gap-2">
          {!encounter.isStarted ? (
            <button
              onClick={handleStartEncounter}
              className="w-full min-w-0 px-3 py-2 text-sm font-medium text-white bg-red-700 hover:bg-red-800 rounded-md transition-colors flex items-center justify-center"
              disabled={!encounter.participants || encounter.participants.filter(p => p.isActive).length === 0}
            >
              <PlayIcon size={16} className="mr-1 flex-shrink-0" /> <span className="truncate hidden lg:inline">Start Combat</span><span className="inline lg:hidden truncate">Start</span>
            </button>
          ) : (
            <>
              <button
                onClick={handleTogglePause}
                className={`w-full min-w-0 px-3 py-2 text-sm font-medium text-white rounded-md transition-colors flex items-center justify-center ${encounter.isPaused ? 'bg-red-700 hover:bg-red-800' : 'bg-amber-600 hover:bg-amber-700'}`}
                title={encounter.isPaused
                  ? 'Resume combat from the current turn. The initiative order will be recalculated to include any participants added while paused.'
                  : 'Pause combat to freeze the turn order. While paused, you can add or remove participants, adjust HP, and edit initiative values. The turn order will be recalculated when you resume.'}
              >
                {encounter.isPaused ? <PlayIcon size={16} className="mr-1 flex-shrink-0" /> : <PauseIcon size={16} className="mr-1 flex-shrink-0" />}
                <span className="truncate hidden lg:inline">{encounter.isPaused ? 'Resume Combat' : 'Pause Combat'}</span><span className="inline lg:hidden truncate">{encounter.isPaused ? 'Resume' : 'Pause'}</span>
              </button>
              <button
                onClick={handleNextTurn}
                className="w-full min-w-0 px-3 py-2 text-sm font-medium text-white bg-purple-700 hover:bg-purple-800 rounded-md transition-colors flex items-center justify-center"
                disabled={!encounter.currentTurnParticipantId || encounter.isPaused}
              >
                <SkipForwardIcon size={16} className="mr-1 flex-shrink-0" /> <span className="truncate hidden lg:inline">Next Turn</span><span className="inline lg:hidden truncate">Next</span>
              </button>
              <button
                onClick={() => setShowEndConfirm(true)}
                className="w-full min-w-0 px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-md transition-colors flex items-center justify-center"
              >
                <StopCircleIcon size={16} className="mr-1 flex-shrink-0" /> <span className="truncate hidden lg:inline">End Combat</span><span className="inline lg:hidden truncate">End</span>
              </button>
              
              {/* Round Counter */}
              <div className="mt-1 pt-2 border-t border-stone-700">
                <p className="text-center text-base font-semibold text-amber-300 font-cinzel">Round: {encounter.round}</p>
                {encounter.isPaused && (
                  <p className="text-center text-xs text-yellow-400 font-semibold mt-1">(Paused)</p>
                )}
              </div>
            </>
          )}
        </div>

        {/* Undo / Redo */}
        <div className="mt-2 flex gap-2">
          <button
            onClick={handleUndo}
            className="flex-1 px-3 py-1.5 text-xs font-medium text-white bg-amber-700 hover:bg-amber-600 rounded-md transition-colors flex items-center justify-center"
            title="Undo latest action for this encounter"
          >
            <Undo2 size={12} className="mr-1" /> <span className="lg:hidden">Un</span><span className="hidden lg:inline">Undo</span>
          </button>
          <button
            onClick={handleRedo}
            className="flex-1 px-3 py-1.5 text-xs font-medium text-white bg-sky-700 hover:bg-sky-600 rounded-md transition-colors flex items-center justify-center"
            title="Redo latest undone action for this encounter"
          >
            <Redo2 size={12} className="mr-1" /> <span className="lg:hidden">Re</span><span className="hidden lg:inline">Redo</span>
          </button>
        </div>

        {/* Display Settings */}
        <div className="mt-2 pt-2 border-t border-stone-700">
          <h5 className="text-xs font-semibold text-stone-400 uppercase tracking-wider mb-2">Player Display</h5>
          <label className="flex items-center justify-between cursor-pointer gap-2">
            <span className="text-xs text-stone-300">Hide player HP</span>
            <button
              role="switch"
              aria-checked={hidePlayerHp}
              onClick={handleToggleHidePlayerHp}
              className={`relative inline-flex h-4 w-8 flex-shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none ${hidePlayerHp ? 'bg-amber-600' : 'bg-stone-600'}`}
            >
              <span className={`inline-block h-3 w-3 transform rounded-full bg-white shadow transition-transform ${hidePlayerHp ? 'translate-x-4' : 'translate-x-0'}`} />
            </button>
          </label>
          <label className="flex items-center justify-between cursor-pointer gap-2 mt-2">
            <span className="text-xs text-stone-300">Hide NPC/monster HP</span>
            <button
              role="switch"
              aria-checked={hideNpcHp}
              onClick={handleToggleHideNpcHp}
              className={`relative inline-flex h-4 w-8 flex-shrink-0 rounded-full border-2 border-transparent transition-colors focus:outline-none ${hideNpcHp ? 'bg-amber-600' : 'bg-stone-600'}`}
            >
              <span className={`inline-block h-3 w-3 transform rounded-full bg-white shadow transition-transform ${hideNpcHp ? 'translate-x-4' : 'translate-x-0'}`} />
            </button>
          </label>
        </div>
      </div>

      <ConfirmationModal
        isOpen={showEndConfirm}
        onClose={() => setShowEndConfirm(false)}
        onConfirm={confirmEndEncounter}
        title="End Encounter?"
        message="Are you sure you want to end this encounter? Initiative order will be reset and it will be removed from the Player Display."
      />
    </>
  );
}

// ============================================================================
// ENCOUNTER MANAGER COMPONENT
// ============================================================================

function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters, encounterStartedRef, encounterActiveRef, wakeLockEnabled, onToggleWakeLock, isBrowserFullscreen, onToggleBrowserFullscreen }) {
  const { showToast } = useUIFeedback();
  const { data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection(
    campaignId ? getPath.encounters(campaignId) : null
  );
  const { data: activeDisplayInfo } = useFirestoreDocument(getPath.activeDisplay());
  const { data: campaignDoc } = useFirestoreDocument(campaignId ? getPath.campaign(campaignId) : null);

  const [encounters, setEncounters] = useState([]);
  const [selectedEncounterId, setSelectedEncounterId] = useState(() => {
    try { return localStorage.getItem(`ttrpg.selectedEncounter.${campaignId}`) || null; }
    catch { return null; }
  });
  const [showCreateModal, setShowCreateModal] = useState(false);
  const [encounterFullscreen, setEncounterFullscreen] = useState(false);
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [itemToDelete, setItemToDelete] = useState(null);
  const [draggedEncounterId, setDraggedEncounterId] = useState(null);

  const selectedEncounterIdRef = useRef(selectedEncounterId);

  // Restore scoped encounter selection when campaign changes.
  useEffect(() => {
    if (!campaignId) { setSelectedEncounterId(null); return; }
    try {
      const saved = localStorage.getItem(`ttrpg.selectedEncounter.${campaignId}`) || null;
      setSelectedEncounterId(saved);
    } catch {
      setSelectedEncounterId(null);
    }
  }, [campaignId]);

  useEffect(() => {
    if (encountersData) setEncounters(encountersData);
  }, [encountersData]);

  // Sort by order field (fallback createdAt). Drag reorder writes order.
  const sortedEncounters = [...(encounters || [])].sort((a, b) => {
    const ao = a.order ?? null, bo = b.order ?? null;
    if (ao !== null && bo !== null) return ao - bo;
    if (ao !== null) return -1;
    if (bo !== null) return 1;
    return (a.createdAt || '').localeCompare(b.createdAt || '');
  });

  const handleEncounterDragStart = (e, id) => {
    setDraggedEncounterId(id);
    e.dataTransfer.effectAllowed = 'move';
  };
  const handleEncounterDragOver = (e) => {
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
  };
  const handleEncounterDrop = async (e, targetId) => {
    e.preventDefault();
    if (!db || !draggedEncounterId || draggedEncounterId === targetId) {
      setDraggedEncounterId(null);
      return;
    }
    const reordered = [...sortedEncounters];
    const fromIdx = reordered.findIndex(x => x.id === draggedEncounterId);
    const toIdx = reordered.findIndex(x => x.id === targetId);
    if (fromIdx === -1 || toIdx === -1) { setDraggedEncounterId(null); return; }
    const [moved] = reordered.splice(fromIdx, 1);
    reordered.splice(toIdx, 0, moved);
    // assign order = index, batch update
    const ops = reordered.map((enc, i) => ({
      type: 'update',
      path: `${getPath.encounters(campaignId)}/${enc.id}`,
      data: { order: i },
    }));
    try { await storage.batchWrite(ops); } catch (err) {}
    setDraggedEncounterId(null);
  };

  useEffect(() => {
    selectedEncounterIdRef.current = selectedEncounterId;
    try {
      if (selectedEncounterId) localStorage.setItem(`ttrpg.selectedEncounter.${campaignId}`, selectedEncounterId);
      else localStorage.removeItem(`ttrpg.selectedEncounter.${campaignId}`);
    } catch {}
  }, [selectedEncounterId, campaignId]);

  useEffect(() => {
    if (!campaignId) {
      setSelectedEncounterId(null);
      return;
    }

    if (encounters && encounters.length > 0) {
      const currentSelection = selectedEncounterIdRef.current;

      if (currentSelection === null || !encounters.some(e => e.id === currentSelection)) {
        if (initialActiveEncounterId && encounters.some(e => e.id === initialActiveEncounterId)) {
          setSelectedEncounterId(initialActiveEncounterId);
        } else if (
          activeDisplayInfo &&
          activeDisplayInfo.activeCampaignId === campaignId &&
          encounters.some(e => e.id === activeDisplayInfo.activeEncounterId)
        ) {
          setSelectedEncounterId(activeDisplayInfo.activeEncounterId);
        }
      }
    } else if (encounters && encounters.length === 0) {
      setSelectedEncounterId(null);
    }
  }, [campaignId, initialActiveEncounterId, activeDisplayInfo, encounters]);

  const handleCreateEncounter = async (name, ruleset = '5e') => {
    if (!db || !name.trim() || !campaignId) return;

    const newEncounterId = generateId();

    try {
      await storage.setDoc(`${getPath.encounters(campaignId)}/${newEncounterId}`, {
        name: name.trim(),
        campaignId,
        createdAt: new Date().toISOString(),
        participants: [],
        round: 0,
        currentTurnParticipantId: null,
        isStarted: false,
        isPaused: false,
        ruleset: ruleset || '5e',
        order: (encounters || []).reduce((max, e) => Math.max(max, e.order ?? -1), -1) + 1,
      });

      setShowCreateModal(false);
      setSelectedEncounterId(newEncounterId);
    } catch (err) {
      console.error("Error creating encounter:", err);
      showToast("Failed to create encounter. Please try again.");    }
  };

  const requestDeleteEncounter = (encounterId, encounterName) => {
    setItemToDelete({ id: encounterId, name: encounterName });
    setShowDeleteConfirm(true);
  };

  const confirmDeleteEncounter = async () => {
    if (!db || !itemToDelete) return;

    const encounterId = itemToDelete.id;

    try {
      // cascade: delete logs for this encounter
      const encPath = getPath.encounter(campaignId, encounterId);
      const allLogs = await storage.getCollection(getPath.logs());
      const encLogs = allLogs.filter(l =>
        (l.encounterId && l.encounterId === encounterId) ||
        (l.encounterPath && l.encounterPath.includes(encPath))
      );
      if (encLogs.length > 0) {
        await storage.batchWrite(encLogs.map(l => ({
          type: 'delete',
          path: `${getPath.logs()}/${l.id || l.path?.split('/').pop()}`,
        })));
      }

      await storage.deleteDoc(encPath);

      if (selectedEncounterId === encounterId) {
        setSelectedEncounterId(null);
      }

      if (activeDisplayInfo && activeDisplayInfo.activeEncounterId === encounterId) {
        await storage.updateDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null });
      }
    } catch (err) {
      console.error("Error deleting encounter:", err);
      showToast("Failed to delete encounter. Please try again.");    }

    setShowDeleteConfirm(false);
    setItemToDelete(null);
  };

  const handleTogglePlayerDisplay = async (encounterId) => {
    if (!db) return;

    try {
      const currentActiveCampaign = activeDisplayInfo?.activeCampaignId;
      const currentActiveEncounter = activeDisplayInfo?.activeEncounterId;

      if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) {
        await storage.setDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null }, { merge: true });
      } else {
        await storage.setDoc(getPath.activeDisplay(), {
          activeCampaignId: campaignId,
          activeEncounterId: encounterId,
        });
      }
    } catch (err) {
      console.error("Error toggling Player Display:", err);
    }
  };

  const selectedEncounter = encounters?.find(e => e.id === selectedEncounterId);

  useEffect(() => {
    if (encounterStartedRef) {
      encounterStartedRef.current = !!(selectedEncounter && selectedEncounter.isStarted && !selectedEncounter.isPaused);
    }
    if (encounterActiveRef) {
      encounterActiveRef.current = !!(selectedEncounter && selectedEncounter.isStarted);
    }
  }, [selectedEncounter, encounterStartedRef, encounterActiveRef]);

  if (isLoadingEncounters && campaignId) {
    return <p className="text-center text-stone-300 mt-4">Loading encounters...</p>;
  }

  return (
    <>
      <div className="mt-3 md:mt-6 p-2 md:p-4 bg-stone-900 rounded-lg shadow">
        <div className="flex justify-between items-center mb-3">
          <h3 className="text-xl font-semibold text-amber-300 font-cinzel tracking-wide flex items-center">
            <Swords size={24} className="mr-2" /> Encounters
          </h3>
          <button
            onClick={() => setShowCreateModal(true)}
            className="px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center"
          >
            <PlusCircle size={18} className="mr-1" /> Create Encounter
          </button>
        </div>

        {(!encounters || encounters.length === 0) && (
          <p className="text-sm text-stone-400">No encounters yet.</p>
        )}

        <div className="space-y-3">
          {sortedEncounters.map(encounter => {
            const isLive = activeDisplayInfo &&
              activeDisplayInfo.activeCampaignId === campaignId &&
              activeDisplayInfo.activeEncounterId === encounter.id;

            return (
              <div
                key={encounter.id}
                draggable
                onDragStart={(e) => handleEncounterDragStart(e, encounter.id)}
                onDragOver={handleEncounterDragOver}
                onDrop={(e) => handleEncounterDrop(e, encounter.id)}
                onDragEnd={() => setDraggedEncounterId(null)}
                className={`p-3 rounded-md shadow transition-all ${selectedEncounterId === encounter.id ? 'bg-amber-900 ring-2 ring-amber-500' : 'bg-stone-800 hover:bg-stone-700'} ${isLive ? 'ring-2 ring-green-500 shadow-md shadow-green-500/30' : ''} ${encounter.isStarted && !encounter.endedAt ? 'ring-2 ring-red-500' : ''} ${draggedEncounterId === encounter.id ? 'opacity-50 ring-2 ring-yellow-400' : ''} cursor-grab`}
              >
                <div className="flex justify-between items-center">
                  <div onClick={() => setSelectedEncounterId(encounter.id)} className="cursor-pointer flex-grow">
                    <h4 className="font-medium text-white">{encounter.name} <span className={`ml-1 px-1.5 py-0.5 rounded text-xs font-bold tracking-wide ${encounter.ruleset === 'generic' ? 'bg-purple-900/80 text-purple-200 border border-purple-500' : 'bg-amber-900/80 text-amber-200 border border-amber-500'}`}>{encounter.ruleset === 'generic' ? 'GEN' : '5e'}</span>{encounter.isStarted && !encounter.endedAt && <span className="ml-1 px-1.5 py-0.5 rounded text-xs font-bold bg-red-600 text-white animate-pulse">⚔ IN PROGRESS</span>}</h4>
                    <p className="text-xs text-stone-300">
                      {encounter.createdAt && `${new Date(encounter.createdAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })} · `}Participants: {encounter.participants?.length || 0}
                    </p>
                    {(encounter.startedAt || encounter.endedAt) && (
                      <p className="text-xs text-green-400/80 mt-0.5">
                        {encounter.startedAt && `⚔ Started: ${new Date(encounter.startedAt).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })}`}{encounter.startedAt && encounter.endedAt && ' · '}{encounter.endedAt && `Ended: ${new Date(encounter.endedAt).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })}`}
                      </p>
                    )}
                    {isLive && (
                      <span className="text-xs text-green-400 font-semibold block mt-1">
                        LIVE ON PLAYER DISPLAY
                      </span>
                    )}
                  </div>
                  <div className="flex items-center space-x-2">
                    <ChevronsUpDown
                      size={18}
                      className="text-stone-400 flex-shrink-0 cursor-grab"
                      title="Drag to reorder"
                    />
                    <button
                      onClick={() => handleTogglePlayerDisplay(encounter.id)}
                      className={`p-1 rounded transition-colors ${isLive ? 'bg-red-500 hover:bg-red-600 text-white' : 'text-amber-400 hover:text-amber-300 bg-stone-700 hover:bg-stone-600'}`}
                      title={isLive ? "Deactivate for Player Display" : "Activate for Player Display"}
                    >
                      {isLive ? <EyeOff size={18} /> : <Eye size={18} />}
                    </button>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        requestDeleteEncounter(encounter.id, encounter.name);
                      }}
                      className="p-1 rounded transition-colors text-red-400 hover:text-red-300 bg-stone-700 hover:bg-stone-600"
                      title="Delete Encounter"
                    >
                      <Trash2 size={18} />
                    </button>
                  </div>
                </div>
              </div>
            );
          })}
        </div>

        {showCreateModal && (
          <Modal onClose={() => setShowCreateModal(false)} title="Create New Encounter">
            <CreateEncounterForm
              key={campaignId}
              onCreate={handleCreateEncounter}
              onCancel={() => setShowCreateModal(false)}
              defaultRuleset={campaignDoc?.ruleset || '5e'}
            />
          </Modal>
        )}

        {selectedEncounter && (
          <div className={`mt-3 md:mt-6 p-2 md:p-4 bg-stone-900 rounded-lg shadow-inner ${encounterFullscreen ? 'fixed inset-0 z-50 m-0 rounded-none overflow-y-auto' : ''}`}>
            <h3 className="text-xl font-semibold text-amber-300 mb-3 font-cinzel tracking-wide flex items-center justify-between gap-2">
              <span className="min-w-0 truncate">Managing Encounter: {selectedEncounter.name}</span>
              <span className="flex items-center gap-1 flex-shrink-0">
                <button
                  onClick={onToggleWakeLock}
                  className={`p-1.5 rounded-md transition-colors ${wakeLockEnabled ? 'bg-amber-600 text-white hover:bg-amber-700' : 'bg-stone-700 text-stone-300 hover:bg-stone-600'}`}
                  title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
                  aria-label={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
                >
                  {wakeLockEnabled ? <Coffee size={18} /> : <Moon size={18} />}
                </button>
                <button
                  onClick={onToggleBrowserFullscreen}
                  className="p-1.5 rounded-md text-stone-300 bg-stone-700 hover:bg-stone-600 transition-colors"
                  title={isBrowserFullscreen ? 'Exit browser fullscreen' : 'Enter browser fullscreen'}
                  aria-label={isBrowserFullscreen ? 'Exit browser fullscreen' : 'Enter browser fullscreen'}
                >
                  {isBrowserFullscreen ? <Minimize2 size={18} /> : <Maximize2 size={18} />}
                </button>
                <button
                  onClick={() => setEncounterFullscreen(v => !v)}
                  className="p-1.5 rounded-md text-stone-300 bg-stone-700 hover:bg-stone-600 transition-colors"
                  title={encounterFullscreen ? 'Exit encounter popout' : 'Expand encounter to full page'}
                  aria-label={encounterFullscreen ? 'Exit encounter popout' : 'Expand encounter to full page'}
                >
                  {encounterFullscreen ? <PanelTopClose size={18} /> : <PanelTopOpen size={18} />}
                </button>
              </span>
            </h3>
            <div className="flex flex-row gap-4">
              {/* Combat Controls - always left */}
              <div className="w-32 md:w-64 flex-shrink-0 sticky top-4 self-start">
                <InitiativeControls
                  campaignId={campaignId}
                  encounter={selectedEncounter}
                  encounterPath={getPath.encounter(campaignId, selectedEncounter.id)}
                />
              </div>
              
              {/* Participant Manager - Right Side */}
              <div className="flex-grow">
                <ParticipantManager
                  encounter={selectedEncounter}
                  encounterPath={getPath.encounter(campaignId, selectedEncounter.id)}
                  campaignCharacters={campaignCharacters}
                  campaignId={campaignId}
                />
              </div>
            </div>
          </div>
        )}
      </div>

      <ConfirmationModal
        isOpen={showDeleteConfirm}
        onClose={() => setShowDeleteConfirm(false)}
        onConfirm={confirmDeleteEncounter}
        title="Delete Encounter?"
        message={`Are you sure you want to delete the encounter "${itemToDelete?.name}"? This action cannot be undone.`}
      />
    </>
  );
}

// ============================================================================
// ADMIN VIEW COMPONENT
// ============================================================================

function AdminView({ userId }) {
  const { showToast } = useUIFeedback();
  const { data: campaignsData, isLoading: isLoadingCampaigns, error: campaignsError } = useFirestoreCollection(
    getPath.campaigns()
  );
  const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay());

  const [isFullscreen, setIsFullscreen] = useState(false);
  const [wakeLockEnabled, setWakeLockEnabled] = useState(() => {
    try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; }
    catch { return false; }
  });
  const wakeLockRef = useRef(null);
  const fullscreenWantedRef = useRef(false);

  const toggleFullscreen = async () => {
    try {
      if (!document.fullscreenElement) {
        fullscreenWantedRef.current = true;
        await document.documentElement.requestFullscreen();
      } else {
        fullscreenWantedRef.current = false;
        await document.exitFullscreen();
      }
    } catch (e) {
      showToast('Fullscreen failed. Tap fullscreen again.');
    }
  };

  useEffect(() => {
    const onFsChange = () => {
      const active = !!document.fullscreenElement;
      setIsFullscreen(active);
      if (active) fullscreenWantedRef.current = true;
      else if (document.visibilityState === 'visible') fullscreenWantedRef.current = false;
    };
    document.addEventListener('fullscreenchange', onFsChange);
    return () => document.removeEventListener('fullscreenchange', onFsChange);
  }, []);

  useEffect(() => {
    const restoreFullscreen = async () => {
      if (document.visibilityState === 'visible' && fullscreenWantedRef.current && !document.fullscreenElement) {
        try { await document.documentElement.requestFullscreen(); } catch (e) { /* browser may require tap */ }
      }
    };
    document.addEventListener('visibilitychange', restoreFullscreen);
    return () => document.removeEventListener('visibilitychange', restoreFullscreen);
  }, []);

  useEffect(() => {
    if (!wakeLockEnabled) {
      wakeLockRef.current?.release();
      wakeLockRef.current = null;
      return;
    }
    const acquire = async () => {
      if (document.visibilityState !== 'visible' || wakeLockRef.current?.released === false) return;
      try {
        if (!navigator.wakeLock?.request) throw new Error('Wake Lock API unavailable');
        const sentinel = await navigator.wakeLock.request('screen');
        wakeLockRef.current = sentinel;
        sentinel.addEventListener?.('release', () => { wakeLockRef.current = null; });
      } catch (e) {
        console.error('Wake lock failed:', e);
        showToast('Prevent Sleep failed. Requires HTTPS or Chrome flag: chrome://flags/#unsafely-treat-insecure-origin-as-secure');
      }
    };
    acquire();
    const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
    const onFsChange = () => { if (document.fullscreenElement) acquire(); };
    document.addEventListener('visibilitychange', onVisChange);
    document.addEventListener('fullscreenchange', onFsChange);
    return () => {
      document.removeEventListener('visibilitychange', onVisChange);
      document.removeEventListener('fullscreenchange', onFsChange);
      wakeLockRef.current?.release();
      wakeLockRef.current = null;
    };
  }, [wakeLockEnabled, showToast]);

  const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
  const [draggedCampaignId, setDraggedCampaignId] = useState(null);
  const [selectedCampaignId, setSelectedCampaignId] = useState(() => {
    try { return localStorage.getItem('ttrpg.selectedCampaign') || null; }
    catch { return null; }
  });
  const encounterStartedRef = useRef(false);
  const encounterActiveRef = useRef(false);
  const manualSelectRef = useRef(false);
  const prevDisplayCampaignRef = useRef(null);
  const scrollRestoredRef = useRef(false);

  // Save scroll on unload + visibility change + interval.
  useEffect(() => {
    const onSave = () => {
      try { localStorage.setItem(`ttrpg.scrollY.${selectedCampaignId}`, String(window.scrollY)); } catch {}
    };
    window.addEventListener('beforeunload', onSave);
    window.addEventListener('pagehide', onSave);
    document.addEventListener('visibilitychange', onSave);
    const interval = setInterval(onSave, 2000);
    return () => {
      window.removeEventListener('beforeunload', onSave);
      window.removeEventListener('pagehide', onSave);
      document.removeEventListener('visibilitychange', onSave);
      clearInterval(interval);
    };
  }, [selectedCampaignId]);

  // Restore scroll once data loaded.
  useEffect(() => {
    if (scrollRestoredRef.current) return;
    if (campaignsWithDetails.length === 0) return;
    try {
      const y = parseInt(localStorage.getItem(`ttrpg.scrollY.${selectedCampaignId}`) || '0', 10);
      if (y > 0) {
        scrollRestoredRef.current = true;
        setTimeout(() => window.scrollTo(0, y), 300);
      }
    } catch {}
  }, [campaignsWithDetails, selectedCampaignId]);

  // Persist selections across reload.
  useEffect(() => {
    try {
      if (selectedCampaignId) localStorage.setItem('ttrpg.selectedCampaign', selectedCampaignId);
      else localStorage.removeItem('ttrpg.selectedCampaign');
    } catch {}
  }, [selectedCampaignId]);

  // External display change (replay/other DM) = clear manual override, allow follow.
  useEffect(() => {
    const cur = initialActiveInfo?.activeCampaignId || null;
    if (cur !== prevDisplayCampaignRef.current) {
      manualSelectRef.current = false;
      prevDisplayCampaignRef.current = cur;
    }
  }, [initialActiveInfo]);
  const [showCreateModal, setShowCreateModal] = useState(false);
  const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  const [itemToDelete, setItemToDelete] = useState(null);
  const [campaignsCollapsed, setCampaignsCollapsed] = useState(() => {
    try { return localStorage.getItem('ttrpg.campaignsCollapsed') === 'true'; }
    catch { return false; }
  });

  useEffect(() => {
    try { localStorage.setItem('ttrpg.campaignsCollapsed', String(campaignsCollapsed)); }
    catch {}
  }, [campaignsCollapsed]);

  useEffect(() => {
    if (campaignsData && db) {
      const fetchDetails = async () => {
        const detailedCampaigns = await Promise.all(
          campaignsData.map(async (campaign) => {
            const characters = campaign.players || [];
            let encounterCount = 0;

            try {
              const encounters = await storage.getCollection(getPath.encounters(campaign.id));
              encounterCount = encounters.length;
            } catch (err) {
              console.error(`Failed to fetch encounters for campaign ${campaign.id}:`, err);
            }

            return { ...campaign, characters, encounterCount };
          })
        );

        // sort by order field (fallback createdAt)
        detailedCampaigns.sort((a, b) => {
          const ao = a.order ?? null, bo = b.order ?? null;
          if (ao !== null && bo !== null) return ao - bo;
          if (ao !== null) return -1;
          if (bo !== null) return 1;
          const at = a.createdAt || 0;
          const bt = b.createdAt || 0;
          if (at === bt) return (a.name || '').localeCompare(b.name || '');
          return bt - at;
        });

        setCampaignsWithDetails(detailedCampaigns);
      };

      fetchDetails();
    } else if (campaignsData) {
      const sorted = [...campaignsData].sort((a, b) => {
        const ao = a.order ?? null, bo = b.order ?? null;
        if (ao !== null && bo !== null) return ao - bo;
        if (ao !== null) return -1;
        if (bo !== null) return 1;
        const at = a.createdAt || 0;
        const bt = b.createdAt || 0;
        if (at === bt) return (a.name || '').localeCompare(b.name || '');
        return bt - at;
      });
      setCampaignsWithDetails(
        sorted.map(c => ({ ...c, characters: c.players || [], encounterCount: 0 }))
      );
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [campaignsData]);

  useEffect(() => {
    // Skip follow only if user manually selected AND display hasn't changed.
    // (external display change clears manualSelectRef via prevDisplay effect)
    if (manualSelectRef.current && selectedCampaignId !== initialActiveInfo?.activeCampaignId) return;
    if (
      initialActiveInfo &&
      initialActiveInfo.activeCampaignId &&
      campaignsWithDetails.length > 0 &&
      !encounterStartedRef.current &&
      !encounterActiveRef.current
    ) {
      const campaignExists = campaignsWithDetails.some(c => c.id === initialActiveInfo.activeCampaignId);
      if (campaignExists && selectedCampaignId !== initialActiveInfo.activeCampaignId) {
        setSelectedCampaignId(initialActiveInfo.activeCampaignId);
      }
    }
  }, [initialActiveInfo, campaignsWithDetails, selectedCampaignId]);

  const handleCreateCampaign = async (name, backgroundUrl, ruleset = '5e') => {
    if (!db || !name.trim()) return;

    const newCampaignId = generateId();

    try {
      await storage.setDoc(getPath.campaign(newCampaignId), {
        name: name.trim(),
        playerDisplayBackgroundUrl: backgroundUrl.trim() || '',
        ownerId: userId,
        createdAt: new Date().toISOString(),
        players: [],
        ruleset: ruleset || '5e',
        order: (campaignsWithDetails || []).reduce((max, c) => Math.max(max, c.order ?? -1), -1) + 1,
      });

      setShowCreateModal(false);
      setSelectedCampaignId(newCampaignId);
    } catch (err) {
      console.error("Error creating campaign:", err);
      showToast("Failed to create campaign. Please try again.");    }
  };

  const requestDeleteCampaign = (campaignId, campaignName) => {
    setItemToDelete({ id: campaignId, name: campaignName });
    setShowDeleteConfirm(true);
  };

  const confirmDeleteCampaign = async () => {
    if (!db || !itemToDelete) return;

    const campaignId = itemToDelete.id;
    await deleteCampaignCascade(campaignId);

    if (selectedCampaignId === campaignId) {
      setSelectedCampaignId(null);
    }

    setShowDeleteConfirm(false);
    setItemToDelete(null);
    showToast('Campaign deleted.');
  };

  // Cascade delete: encounters + logs + display + campaign doc.
  const deleteCampaignCascade = async (campaignId) => {
    const encountersPath = getPath.encounters(campaignId);
    const encounters = await storage.getCollection(encountersPath);
    const deleteOps = encounters.map(e => {
      const id = e.id || e.path?.split('/').pop();
      return { type: 'delete', path: `${encountersPath}/${id}` };
    });

    const allLogs = await storage.getCollection(getPath.logs());
    const encPaths = new Set(encounters.map(e => `${encountersPath}/${e.id || e.path?.split('/').pop()}`));
    const encIds = new Set(encounters.map(e => e.id || e.path?.split('/').pop()));
    const logOps = allLogs
      .filter(l =>
        (l.encounterId && encIds.has(l.encounterId)) ||
        (l.encounterPath && [...encPaths].some(p => l.encounterPath.includes(p)))
      )
      .map(l => ({
        type: 'delete',
        path: `${getPath.logs()}/${l.id || l.path?.split('/').pop()}`,
      }));

    if (deleteOps.length > 0 || logOps.length > 0) {
      await storage.batchWrite([...deleteOps, ...logOps]);
    }

    await storage.deleteDoc(getPath.campaign(campaignId));

    const activeDisplay = await storage.getDoc(getPath.activeDisplay());
    if (activeDisplay && activeDisplay.activeCampaignId === campaignId) {
      await storage.updateDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null });
    }
  };

  // Dev-only: wipe all campaigns + encounters + logs. Irreversible.
  // Uses deleteCollection (SQL bulk DELETE, no fetch). Parallel per campaign.
  const [showDeleteAllConfirm, setShowDeleteAllConfirm] = useState(false);
  const handleDeleteAllCampaigns = async () => {
    if (!db) return;
    try {
      const all = await storage.getCollection(getPath.campaigns());
      await Promise.all(all.map(async (c) => {
        const cid = c.id || c.path?.split('/').pop();
        // bulk-delete all encounters under this campaign (SQL DELETE, no fetch)
        if (storage.deleteCollection) {
          await storage.deleteCollection(getPath.encounters(cid));
        }
        await storage.deleteDoc(getPath.campaign(cid));
      }));
      // nuke all logs in one shot (orphans acceptable in dev; logs span campaigns)
      if (storage.deleteCollection) {
        await storage.deleteCollection(getPath.logs());
      }
      const activeDisplay = await storage.getDoc(getPath.activeDisplay());
      if (activeDisplay && activeDisplay.activeCampaignId) {
        await storage.updateDoc(getPath.activeDisplay(), { activeCampaignId: null, activeEncounterId: null });
      }
      setSelectedCampaignId(null);
      setShowDeleteAllConfirm(false);
      showToast(`Deleted ${all.length} campaign(s).`);
    } catch (err) {
      console.error('Error deleting all campaigns:', err);
      showToast('Failed to delete all campaigns.');
    }
  };

  const selectedCampaign = campaignsWithDetails.find(c => c.id === selectedCampaignId);

  if (isLoadingCampaigns) {
    return <p className="text-center text-stone-300">Loading campaigns...</p>;
  }

  if (campaignsError) {
    return (
      <p className="text-center text-red-400">
        Error loading campaigns: {campaignsError.message || String(campaignsError)}
      </p>
    );
  }

  return (
    <>
      <div className="space-y-3 md:space-y-6">
        <div>
          <div className="flex justify-between items-center mb-4">
            <button
              onClick={() => setCampaignsCollapsed(c => !c)}
              className="flex items-center gap-2 text-2xl font-semibold text-amber-300 font-cinzel tracking-wide hover:text-amber-200 transition-colors"
              aria-expanded={!campaignsCollapsed}
              aria-controls="campaigns-grid"
            >
              {campaignsCollapsed
                ? <ChevronRight size={24} />
                : <ChevronDown size={24} />}
              Campaigns
              <span className="text-sm font-normal text-stone-400">({campaignsWithDetails.length})</span>
            </button>
            <button
              onClick={() => setShowCreateModal(true)}
              className="bg-red-700 hover:bg-red-800 text-white font-bold py-2 px-4 rounded-lg flex items-center transition-colors"
            >
              <PlusCircle size={20} className="mr-2" /> Create Campaign
            </button>
            <div className="flex gap-1">
              <button
                onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
                title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
                className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 hover:bg-stone-700 text-stone-300 hover:text-white'}`}
              >
                {wakeLockEnabled ? <Coffee size={20} /> : <Moon size={20} />}
              </button>
              <button
                onClick={toggleFullscreen}
                title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
                className="bg-stone-800 hover:bg-stone-700 text-stone-300 hover:text-white p-2 rounded-lg transition-all"
              >
                {isFullscreen ? <Minimize2 size={20} /> : <Maximize2 size={20} />}
              </button>
            </div>
          </div>

          {!campaignsCollapsed && (
            <>
              {campaignsWithDetails.length === 0 && !isLoadingCampaigns && (
                <p className="text-stone-400">No campaigns yet. Create one to get started!</p>
              )}

              <div id="campaigns-grid" className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
                {campaignsWithDetails.map(campaign => {
              const cardStyle = campaign.playerDisplayBackgroundUrl
                ? { backgroundImage: `url(${campaign.playerDisplayBackgroundUrl})` }
                : {};

              const cardClasses = `h-40 flex flex-col justify-between rounded-lg shadow-md cursor-grab transition-all relative overflow-hidden bg-cover bg-center ${selectedCampaignId === campaign.id ? 'ring-4 ring-amber-500' : ''} ${!campaign.playerDisplayBackgroundUrl ? 'bg-stone-800 hover:bg-stone-700' : 'hover:shadow-xl'} ${draggedCampaignId === campaign.id ? 'opacity-50 ring-2 ring-yellow-400' : ''}`;

              return (
                <div
                  key={campaign.id}
                  draggable
                  onDragStart={(e) => { setDraggedCampaignId(campaign.id); e.dataTransfer.effectAllowed = 'move'; }}
                  onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
                  onDrop={async (e) => {
                    e.preventDefault();
                    if (!db || !draggedCampaignId || draggedCampaignId === campaign.id) { setDraggedCampaignId(null); return; }
                    const reordered = [...campaignsWithDetails];
                    const fromIdx = reordered.findIndex(c => c.id === draggedCampaignId);
                    const toIdx = reordered.findIndex(c => c.id === campaign.id);
                    if (fromIdx === -1 || toIdx === -1) { setDraggedCampaignId(null); return; }
                    const [moved] = reordered.splice(fromIdx, 1);
                    reordered.splice(toIdx, 0, moved);
                    const ops = reordered.map((c, i) => ({ type: 'update', path: getPath.campaign(c.id), data: { order: i } }));
                    try { await storage.batchWrite(ops); } catch (err) {}
                    setDraggedCampaignId(null);
                  }}
                  onDragEnd={() => setDraggedCampaignId(null)}
                  onClick={() => {
                    if (encounterStartedRef.current) {
                      showToast('End or pause active encounter before switching campaigns.');
                      return;
                    }
                    manualSelectRef.current = true;
                    setSelectedCampaignId(campaign.id);
                  }}
                  className={cardClasses}
                  style={cardStyle}
                >
                  <div
                    className={`relative z-10 flex flex-col justify-between h-full ${campaign.playerDisplayBackgroundUrl ? 'bg-black bg-opacity-60 p-3' : 'p-4'}`}
                  >
                    <div>
                      <div className="flex items-center gap-1">
                        <ChevronsUpDown size={14} className="text-white/60 flex-shrink-0" title="Drag to reorder" />
                        <h3 className="text-xl font-semibold text-white">{campaign.name}</h3>
                      </div>
                      <div className="text-xs text-stone-100 mt-1 space-x-3">
                        <span className="inline-flex items-center">
                          <Users size={12} className="mr-1" /> {campaign.characters?.length || 0} Characters
                        </span>
                        <span className="inline-flex items-center">
                          <Swords size={12} className="mr-1" /> {campaign.encounterCount === undefined ? '...' : campaign.encounterCount} Encounters
                        </span>
                      </div>
                      {campaign.createdAt && (
                        <div className="text-xs text-stone-300 opacity-70 mt-1">
                          <Clock size={12} className="mr-1 inline-block" /> Created: {new Date(campaign.createdAt).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })}
                        </div>
                      )}
                    </div>
                    <button
                      onClick={(e) => {
                        e.stopPropagation();
                        requestDeleteCampaign(campaign.id, campaign.name);
                      }}
                      className="mt-auto text-red-300 hover:text-red-100 text-xs flex items-center self-start bg-black bg-opacity-50 hover:bg-opacity-70 px-2 py-1 rounded"
                    >
                      <Trash2 size={14} className="mr-1" /> Delete
                    </button>
                    <span
                      className={`absolute bottom-2 right-2 z-20 px-2 py-0.5 rounded text-xs font-bold tracking-wide ${campaign.ruleset === 'generic' ? 'bg-purple-900/80 text-purple-200 border border-purple-500' : 'bg-amber-900/80 text-amber-200 border border-amber-500'}`}
                      title={campaign.ruleset === 'generic' ? 'Generic ruleset' : 'D&D 5e ruleset'}
                    >
                      {campaign.ruleset === 'generic' ? 'GEN' : '5e'}
                    </span>
                  </div>
                </div>
              );
            })}
              </div>

              {isDevToolsEnabled() && campaignsWithDetails.length > 0 && (
                <div className="mt-4">
                  <button
                    onClick={() => setShowDeleteAllConfirm(true)}
                    className="px-3 py-1.5 text-xs rounded bg-red-900 hover:bg-red-800 text-red-100 border border-red-700"
                    title="DEV ONLY: Delete every campaign, encounter, and log. Irreversible."
                  >
                    <Trash2 size={12} className="inline mr-1" />DEV: Delete All Campaigns
                  </button>
                </div>
              )}
            </>
          )}
        </div>

        {showCreateModal && (
          <Modal onClose={() => setShowCreateModal(false)} title="Create New Campaign">
            <CreateCampaignForm
              onCreate={handleCreateCampaign}
              onCancel={() => setShowCreateModal(false)}
            />
          </Modal>
        )}

        {selectedCampaign && (
          <div className="mt-3 md:mt-6 p-3 md:p-6 bg-stone-900 rounded-lg shadow-xl">
            <h2 className="text-2xl font-semibold text-amber-300 mb-4 font-cinzel tracking-wide">
              Managing: {selectedCampaign.name}
            </h2>
            <CharacterManager
              campaignId={selectedCampaignId}
              campaignCharacters={selectedCampaign.characters || []}
              syncCharacters={!!selectedCampaign.syncCharacters}
            />
            <hr className="my-6 border-stone-700" />
            <EncounterManager
              campaignId={selectedCampaignId}
              initialActiveEncounterId={
                initialActiveInfo && initialActiveInfo.activeCampaignId === selectedCampaignId
                  ? initialActiveInfo.activeEncounterId
                  : null
              }
              campaignCharacters={selectedCampaign.characters || []}
              encounterStartedRef={encounterStartedRef}
              encounterActiveRef={encounterActiveRef}
              wakeLockEnabled={wakeLockEnabled}
              onToggleWakeLock={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
              isBrowserFullscreen={isFullscreen}
              onToggleBrowserFullscreen={toggleFullscreen}
            />
          </div>
        )}
      </div>

      <ConfirmationModal
        isOpen={showDeleteConfirm}
        onClose={() => setShowDeleteConfirm(false)}
        onConfirm={confirmDeleteCampaign}
        title="Delete Campaign?"
        message={`Are you sure you want to delete the campaign "${itemToDelete?.name}" and all its encounters? This action cannot be undone.`}
      />

      <ConfirmationModal
        isOpen={showDeleteAllConfirm}
        onClose={() => setShowDeleteAllConfirm(false)}
        onConfirm={handleDeleteAllCampaigns}
        title="Delete ALL Campaigns?"
        message="DEV ONLY: This permanently deletes EVERY campaign, all encounters, and all logs. Cannot be undone. Are you absolutely sure?"
      />
    </>
  );
}

// ============================================================================
// DISPLAY VIEW COMPONENT (Player View)
// ============================================================================

// Player participant card: animate state transitions.
//   mount/activate  : animate in (opacity+scale+slide)
//   deactivate       : animate out then signal exit
//   alive→dead       : transition to death cue (dim+desaturate+skull pulse+red rim)
//   conscious/dying/stable: full visible, no fade
function PlayerParticipantCard({ id, isActive, status, onExit, className, children, divRef }) {
  // phase: 'hidden' (opacity-0, start state) -> 'visible' (opacity-100)
  // disable: 'visible' -> 'exiting' (opacity-0, transition plays) -> onExit
  const [phase, setPhase] = useState('hidden');
  const isDead = status === 'dead';

  useEffect(() => {
    if (!isActive) {
      // ensure current 'visible' frame painted, THEN flip to exiting so
      // browser sees opacity change and plays the fade-out.
      const raf = requestAnimationFrame(() => {
        setPhase('exiting');
      });
      const t = setTimeout(() => onExit(id), 1000);
      return () => { cancelAnimationFrame(raf); clearTimeout(t); };
    }
    // enable/reactivate: paint hidden frame first, THEN flip to visible
    // so browser sees opacity change and plays the transition.
    setPhase('hidden');
    const raf = requestAnimationFrame(() => {
      requestAnimationFrame(() => setPhase('visible'));
    });
    return () => cancelAnimationFrame(raf);
  }, [isActive, id, onExit]);

  // animate: opacity + scale + slide. Visible motion, not just fade.
  const fadeClass = phase === 'visible'
    ? 'opacity-100 scale-100 translate-y-0'
    : 'opacity-0 scale-75 translate-y-4';

  // death transition: fires once when alive→dead. dim + desaturate + red rim.
  const deathClass = isDead
    ? 'brightness-50 saturate-0 border-2 border-red-900/70 shadow-red-900/50 shadow-2xl'
    : '';

  return (
    <div
      ref={divRef}
      className={`[transition:opacity_1s_ease-in-out,transform_1s_ease-in-out,filter_1s_ease-in-out,border-color_1s_ease-in-out,box-shadow_300ms_ease-in-out] ${fadeClass} ${deathClass} ${className}`}
    >
      {children}
      {isDead && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
          <span className="text-6xl opacity-80 animate-pulse">☠️</span>
        </div>
      )}
    </div>
  );
}

function DisplayView() {
  const { data: activeDisplayData, isLoading: isLoadingActiveDisplay, error: activeDisplayError } = useFirestoreDocument(
    getPath.activeDisplay()
  );

  const [activeEncounterData, setActiveEncounterData] = useState(null);
  const [isLoadingEncounter, setIsLoadingEncounter] = useState(true);
  const [encounterError, setEncounterError] = useState(null);
  const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
  const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [wakeLockEnabled, setWakeLockEnabled] = useState(() => {
    try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; }
    catch { return false; }
  });
  const [displayParticipants, setDisplayParticipants] = useState([]);
  const wakeLockRef = useRef(null);
  const fullscreenWantedRef = useRef(false);
  const currentParticipantRef = useRef(null);

  // Player display transition state. Active participants render normally.
  // Active→inactive: keep prior card, mark __displayActive=false, animate out,
  // then PlayerParticipantCard calls handleExit to remove from display list.
  // Inactive→active: card reappears with __displayActive=true and fades in.
  useEffect(() => {
    const source = (activeEncounterData && activeEncounterData.participants) || [];
    setDisplayParticipants(prev => {
      const prevById = new Map(prev.map(p => [p.id, p]));
      const out = [];
      for (const p of source) {
        const prevP = prevById.get(p.id);
        if (p.isActive !== false) {
          out.push({ ...p, __displayActive: true });
        } else if (prevP && prevP.__displayActive !== false) {
          out.push({ ...p, __displayActive: false });
        } else if (prevP && prevP.__displayActive === false) {
          out.push(prevP);
        }
      }
      return out;
    });
  }, [activeEncounterData]);

  const handleExit = useCallback((id) => {
    setDisplayParticipants(prev => prev.filter(p => p.id !== id));
  }, []);

  useEffect(() => {
    const onFsChange = () => {
      const active = !!document.fullscreenElement;
      setIsFullscreen(active);
      if (active) fullscreenWantedRef.current = true;
      else if (document.visibilityState === 'visible') fullscreenWantedRef.current = false;
    };
    document.addEventListener('fullscreenchange', onFsChange);
    return () => document.removeEventListener('fullscreenchange', onFsChange);
  }, []);

  const toggleFullscreen = async () => {
    try {
      if (!document.fullscreenElement) {
        fullscreenWantedRef.current = true;
        await document.documentElement.requestFullscreen();
      } else {
        fullscreenWantedRef.current = false;
        await document.exitFullscreen();
      }
    } catch (e) {
      console.error('Fullscreen failed:', e);
    }
  };

  useEffect(() => {
    const restoreFullscreen = async () => {
      if (document.visibilityState === 'visible' && fullscreenWantedRef.current && !document.fullscreenElement) {
        try { await document.documentElement.requestFullscreen(); } catch (e) { /* browser may require tap */ }
      }
    };
    document.addEventListener('visibilitychange', restoreFullscreen);
    return () => document.removeEventListener('visibilitychange', restoreFullscreen);
  }, []);

  useEffect(() => {
    if (!wakeLockEnabled) {
      wakeLockRef.current?.release();
      wakeLockRef.current = null;
      return;
    }

    const acquire = async () => {
      if (document.visibilityState !== 'visible' || wakeLockRef.current?.released === false) return;
      try {
        if (!navigator.wakeLock?.request) throw new Error('Wake Lock API unavailable');
        const sentinel = await navigator.wakeLock.request('screen');
        wakeLockRef.current = sentinel;
        sentinel.addEventListener?.('release', () => { wakeLockRef.current = null; });
      } catch (e) {
        console.error('Wake lock failed:', e);
      }
    };

    acquire();

    // Re-acquire after tab becomes visible again (browser auto-releases on hide).
    const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
    document.addEventListener('visibilitychange', onVisChange);
    const onFsChange = () => { if (document.fullscreenElement) acquire(); };
    document.addEventListener('fullscreenchange', onFsChange);
    return () => {
      document.removeEventListener('visibilitychange', onVisChange);
      document.removeEventListener('fullscreenchange', onFsChange);
      wakeLockRef.current?.release();
      wakeLockRef.current = null;
    };
  }, [wakeLockEnabled]);

  useEffect(() => {
    if (!db) {
      setEncounterError("Firestore not available.");
      setIsLoadingEncounter(false);
      setIsPlayerDisplayActive(false);
      return;
    }

    let unsubscribeEncounter;
    let unsubscribeCampaign;

    if (activeDisplayData) {
      const { activeCampaignId, activeEncounterId } = activeDisplayData;

      if (activeCampaignId && activeEncounterId) {
        setIsPlayerDisplayActive(true);
        setIsLoadingEncounter(true);
        setEncounterError(null);

        unsubscribeCampaign = storage.subscribeDoc(
          getPath.campaign(activeCampaignId),
          (camp) => {
            setCampaignBackgroundUrl((camp && camp.playerDisplayBackgroundUrl) || '');
          },
          (err) => console.error("Error fetching campaign background:", err)
        );

        unsubscribeEncounter = storage.subscribeDoc(
          getPath.encounter(activeCampaignId, activeEncounterId),
          (enc) => {
            if (enc) {
              setActiveEncounterData({ id: activeEncounterId, ...enc });
            } else {
              setActiveEncounterData(null);
              setEncounterError("Active encounter data not found.");
            }
            setIsLoadingEncounter(false);
          },
          (err) => {
            console.error("Error fetching active encounter details:", err);
            setEncounterError("Error loading active encounter data.");
            setIsLoadingEncounter(false);
          }
        );
      } else {
        setActiveEncounterData(null);
        setCampaignBackgroundUrl('');
        setIsPlayerDisplayActive(false);
        setIsLoadingEncounter(false);
      }
    } else if (!isLoadingActiveDisplay) {
      setActiveEncounterData(null);
      setCampaignBackgroundUrl('');
      setIsPlayerDisplayActive(false);
      setIsLoadingEncounter(false);
    }

    return () => {
      if (unsubscribeEncounter) unsubscribeEncounter();
      if (unsubscribeCampaign) unsubscribeCampaign();
    };
  }, [activeDisplayData, isLoadingActiveDisplay]);

  // Auto-scroll current participant into view
  useEffect(() => {
    if (currentParticipantRef.current && activeEncounterData?.isStarted && !activeEncounterData?.isPaused) {
      currentParticipantRef.current.scrollIntoView({
        behavior: 'smooth',
        block: 'center',
        inline: 'nearest'
      });
    }
  }, [activeEncounterData?.currentTurnParticipantId, activeEncounterData?.isStarted, activeEncounterData?.isPaused]);

  if (isLoadingActiveDisplay || (isPlayerDisplayActive && isLoadingEncounter)) {
    return <div className="text-center py-10 text-2xl text-stone-300">Loading Player Display...</div>;
  }

  if (activeDisplayError || (isPlayerDisplayActive && encounterError)) {
    return <div className="text-center py-10 text-2xl text-red-400">{activeDisplayError || encounterError}</div>;
  }

  if (!isPlayerDisplayActive || !activeEncounterData) {
    return (
      <div className="min-h-screen bg-black text-stone-400 flex flex-col items-center justify-center p-4 text-center">
        <div className="fixed top-3 right-3 z-50 flex gap-2">
          <button
            onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
            title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
            className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white'}`}
          >
            {wakeLockEnabled ? <Coffee size={20} /> : <Moon size={20} />}
          </button>
          <button
            onClick={toggleFullscreen}
            title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
            className="bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white p-2 rounded-lg transition-all"
          >
            {isFullscreen ? <Minimize2 size={20} /> : <Maximize2 size={20} />}
          </button>
        </div>
        <EyeOff size={64} className="mb-4 text-stone-500" />
        <h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2>
        <p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p>
      </div>
    );
  }

  const { name, participants, round, currentTurnParticipantId, isStarted, isPaused } = activeEncounterData;
  const hidePlayerHp = activeDisplayData?.hidePlayerHp ?? true;
  const hideNpcHp = activeDisplayData?.hideNpcHp ?? false;

  // 1-list model: displayParticipants IS the display order (participants[] order
  // plus temporary exiting cards). Do NOT re-sort by initiative.
  const participantsToRender = displayParticipants;

  const displayStyles = campaignBackgroundUrl
    ? {
        backgroundImage: `url(${campaignBackgroundUrl})`,
        backgroundSize: 'cover',
        backgroundPosition: 'center center',
        backgroundRepeat: 'no-repeat',
        minHeight: '100vh'
      }
    : { minHeight: '100vh' };

  return (
    <div
      className={`p-4 md:p-8 rounded-xl shadow-2xl ${!campaignBackgroundUrl ? 'bg-stone-950' : ''}`}
      style={displayStyles}
    >
      <div className="fixed top-3 right-3 z-50 flex gap-2">
        <button
          onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
          title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
          className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white'}`}
        >
          {wakeLockEnabled ? <Coffee size={20} /> : <Moon size={20} />}
        </button>
        <button
          onClick={toggleFullscreen}
          title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
          className="bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white p-2 rounded-lg transition-all"
        >
          {isFullscreen ? <Minimize2 size={20} /> : <Maximize2 size={20} />}
        </button>
      </div>
      <div className={campaignBackgroundUrl ? 'bg-stone-950 bg-opacity-75 p-4 md:p-6 rounded-lg' : ''}>
        <h2 className="text-4xl md:text-5xl font-bold text-center text-amber-400 mb-2 font-cinzel tracking-wide">{name}</h2>

        {isStarted && <p className="text-2xl text-center text-amber-300 mb-1 font-cinzel">Round: {round}</p>}

        {isStarted && isPaused && (
          <p className="text-xl text-center text-yellow-400 mb-4 font-semibold">(Combat Paused)</p>
        )}

        {!isStarted && participants?.length > 0 && (
          <p className="text-2xl text-center text-stone-400 mb-6">Awaiting Start</p>
        )}

        {!isStarted && (!participants || participants.length === 0) && (
          <p className="text-2xl text-stone-500 mb-6">No participants.</p>
        )}

        {participantsToRender.length === 0 && isStarted && (
          <p className="text-xl text-stone-400">No active participants.</p>
        )}

        <div className="space-y-4 max-w-3xl mx-auto">
          {participantsToRender.map(p => {
            const isGeneric = (activeEncounterData.ruleset || '5e') === 'generic';
            const status = p.status || (isGeneric ? (p.currentHp <= 0 ? 'down' : 'conscious') : (p.currentHp === 0 ? 'dying' : 'conscious'));
            const isZeroHp = p.currentHp <= 0;
            let participantBgColor = p.type === 'monster' || p.type === 'npc'
              ? 'bg-[#8e351c]'
              : 'bg-indigo-950';

            const isCurrentTurn = p.id === currentTurnParticipantId && isStarted && !isPaused;

            if (isCurrentTurn) {
              participantBgColor = 'bg-green-700 ring-4 ring-green-400 scale-105';
            } else if (isPaused && p.id === currentTurnParticipantId) {
              participantBgColor += ' ring-2 ring-yellow-400';
            }

            return (
              <PlayerParticipantCard
                key={p.id}
                id={p.id}
                isActive={p.isActive !== false}
                status={status}
                onExit={handleExit}
                divRef={isCurrentTurn ? currentParticipantRef : null}
                className={`relative p-4 md:p-6 rounded-lg shadow-lg ${participantBgColor}`}
              >
                <div className="flex justify-between items-center mb-2">
                  <h3
                    className={`text-2xl md:text-3xl font-bold font-cinzel ${isCurrentTurn ? 'text-white' : (p.type === 'character' ? 'text-amber-100' : 'text-white')}`}
                  >
                    {isZeroHp && <span className="mr-2">☠️</span>}
                    {p.name}
                    {isCurrentTurn && (
                      <span className="text-yellow-300 animate-pulse ml-2">(Current)</span>
                    )}
                    {status === 'dying' && <span className="text-red-300 text-lg ml-2">(Dying)</span>}
                    {status === 'stable' && (p.conditions || []).includes('unconscious') && <span className="text-emerald-300 text-lg ml-2">(Unconscious)</span>}
                    {status === 'down' && <span className="text-red-300 text-lg ml-2 animate-pulse">(Down)</span>}
                    {status === 'dead' && <span className="text-red-300 text-lg ml-2">(Dead)</span>}
                  </h3>
                  <span
                    className={`text-xl md:text-2xl font-semibold ${isCurrentTurn ? 'text-green-200' : 'text-stone-200'}`}
                  >
                    Init: {p.initiative}
                  </span>
                </div>

                {!(hidePlayerHp && p.type === 'character') && !(hideNpcHp && p.type !== 'character') && (
                  <div className="flex justify-between items-center">
                    <div className="w-full bg-stone-700 rounded-full h-6 md:h-8 relative overflow-hidden border-2 border-stone-600">
                      <div
                        className={`h-full rounded-full transition-all ${isZeroHp ? 'bg-red-900' : (p.currentHp <= p.maxHp / 4 ? 'bg-red-500' : (p.currentHp <= p.maxHp / 2 ? 'bg-yellow-500' : 'bg-green-500'))}`}
                        style={{ width: `${Math.max(0, (p.currentHp / p.maxHp) * 100)}%` }}
                      ></div>
                      {p.type !== 'monster' && (
                        <span className="absolute inset-0 flex items-center justify-center text-xs md:text-sm font-medium text-white px-2">
                          HP: {p.currentHp} / {p.maxHp}
                        </span>
                      )}
                    </div>
                  </div>
                )}

                {p.conditions?.length > 0 && (
                  <div className="flex flex-wrap gap-1 mt-2">
                    {p.conditions.map(cId => {
                      const cond = CONDITIONS.find(c => c.id === cId);
                      return cond ? (
                        <span
                          key={cId}
                          className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-purple-900 border border-purple-500 text-xs md:text-sm text-purple-200 font-medium"
                        >
                          {cond.emoji} {cond.label}
                        </span>
                      ) : (
                        <span
                          key={cId}
                          className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-purple-900 border border-purple-500 text-xs md:text-sm text-purple-200 font-medium"
                        >
                          {cId}
                        </span>
                      );
                    })}
                  </div>
                )}

                {!p.isActive && !isZeroHp && (
                  <p className="text-center text-lg font-semibold text-stone-300 mt-2">(Inactive)</p>
                )}
              </PlayerParticipantCard>
            );
          })}
        </div>
      </div>
    </div>
  );
}

// ============================================================================
// LOGS VIEW COMPONENT
// ============================================================================

function LogsView() {
  const { showToast } = useUIFeedback();
  const [page, setPage] = useState(0);
  const pageQuery = useMemo(() => [LOG_ORDER, limit(LOG_PAGE_SIZE), offset(page * LOG_PAGE_SIZE)], [page]);
  const { data: logs, isLoading } = useFirestoreCollection(getPath.logs(), pageQuery);
  const { data: campaignsData } = useFirestoreCollection(getPath.campaigns());
  const [showClearConfirm, setShowClearConfirm] = useState(false);
  const [undoingId, setUndoingId] = useState(null);
  const [dlMenu, setDlMenu] = useState(false);
  const [cpMenu, setCpMenu] = useState(false);

  // Full typed-log set for download/copy dropdown lists. Page query (LOG_QUERY,
  // 100/page) only sees current page, so the encounter list was truncated.
  // Lazy-fetch on first menu open; reused after. Server getCollection with no
  // constraint returns all docs, so this is the full set (not page-limited).
  // Ref guard (not state): StrictMode double-mount sets state then unmounts
  // before fetch resolves; state-based loading flag dead-locks the remount.
  const [allLogs, setAllLogs] = useState(null);
  const allLogsFetched = useRef(false);
  useEffect(() => {
    if (!dlMenu && !cpMenu) return;
    if (allLogs || allLogsFetched.current) return;
    allLogsFetched.current = true;
    let cancelled = false;
    getStorage().getCollection(getPath.logs())
      .then(docs => { if (!cancelled) setAllLogs(docs.filter(l => l.type)); })
      .catch(err => { console.error('Error loading all logs:', err); allLogsFetched.current = false; })
      .finally(() => { if (cancelled) allLogsFetched.current = false; });
    return () => { cancelled = true; };
  }, [dlMenu, cpMenu, allLogs]);

  // Total log count (separate cheap server count query, not full fetch).
  const [totalCount, setTotalCount] = useState(null);
  useEffect(() => {
    let cancelled = false;
    getStorage().countCollection(getPath.logs())
      .then(n => { if (!cancelled) setTotalCount(n); })
      .catch(() => { if (!cancelled) setTotalCount(null); });
    return () => { cancelled = true; };
  }, [logs]);

  const shown = (logs || []).length;
  const rangeStart = totalCount ? page * LOG_PAGE_SIZE + 1 : 0;
  const rangeEnd = totalCount ? Math.min((page + 1) * LOG_PAGE_SIZE, totalCount) : 0;
  const hasPrev = page > 0;
  const hasNext = totalCount === null ? shown === LOG_PAGE_SIZE : rangeEnd < totalCount;

  // Parse campaign id out of encounterPath (.../campaigns/{cid}/encounters/{eid}).
  const cidFromPath = (p) => {
    if (!p) return null;
    const m = String(p).match(/campaigns\/([^/]+)\/encounters/);
    return m ? m[1] : null;
  };
  const campaignNameById = new Map(
    (campaignsData || []).map(c => [c.id, c.name || '(unnamed campaign)'])
  );

  // Group typed logs by encounter. Each item shows campaign · encounter ·
  // last-entry date · entry count. Sorted newest first by last-entry ts.
  // Legacy logs (no type) excluded — kept for human scroll only.
  const encounterGroups = (() => {
    const src = allLogs ?? (logs || []);
    const map = new Map(); // key: encounterId || encounterPath || 'none'
    for (const l of src) {
      if (!l.type) continue;
      const key = l.encounterId || l.encounterPath || 'none';
      const cid = cidFromPath(l.encounterPath);
      const campName = cid ? (campaignNameById.get(cid) || '(unknown campaign)') : '(unknown campaign)';
      const g = map.get(key) || {
        key,
        encounter: l.encounterName || '(unnamed encounter)',
        campaign: campName,
        count: 0,
        latest: 0,
      };
      g.count++;
      g.latest = Math.max(g.latest, l.ts || l.timestamp || 0);
      map.set(key, g);
    }
    return [...map.values()].sort((a, b) => b.latest - a.latest);
  })();

  const typedCount = encounterGroups.reduce((n, g) => n + g.count, 0);
  const fmtDate = (ts) => ts ? new Date(ts).toLocaleString() : '—';
  // Filename-safe human date: 2026-07-04_15-30
  const fileDate = (ts) => ts
    ? new Date(ts).toISOString().slice(0,16).replace('T', '_').replace(':', '-')
    : new Date().toISOString().slice(0,16).replace('T', '_').replace(':', '-');
  const dlFileName = (g) => g
    ? `ttrpg-logs-${(g.campaign + '_' + g.encounter).replace(/[^a-z0-9]+/gi, '_')}-${fileDate(g.latest)}.json`
    : `ttrpg-logs-all-${fileDate(Date.now())}.json`;

  const handleDownloadFor = async (encKey) => {
    setDlMenu(false);
    try {
      const all = await storage.getCollection(getPath.logs());
      const filtered = encKey
        ? all.filter(l => (l.encounterId || l.encounterPath || 'none') === encKey)
        : all;
      const json = shared.logEvent.serializeEvents(filtered);
      if (json === '[]') {
        showToast('No typed log entries for that selection.');
        return;
      }
      const blob = new Blob([json], { type: 'application/json' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      const grp = encKey ? encounterGroups.find(g => g.key === encKey) : null;
      a.download = dlFileName(grp);
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    } catch (err) {
      console.error('Error downloading logs:', err);
      showToast('Failed to download logs.');
    }
  };

  const handleCopyFor = async (encKey) => {
    setCpMenu(false);
    try {
      const all = await storage.getCollection(getPath.logs());
      const filtered = encKey
        ? all.filter(l => (l.encounterId || l.encounterPath || 'none') === encKey)
        : all;
      const json = shared.logEvent.serializeEvents(filtered);
      if (json === '[]') {
        showToast('No typed log entries for that selection.');
        return;
      }
      await navigator.clipboard.writeText(json);
      showToast('Logs copied to clipboard.');
    } catch (err) {
      console.error('Error copying logs:', err);
      showToast('Failed to copy logs.');
    }
  };

  const handleClearLogs = async () => {
    try {
      const logs = await storage.getCollection(getPath.logs());
      if (logs.length > 0) {
        const ops = logs.map(l => {
          const id = l.id || l.path?.split('/').pop();
          return { type: 'delete', path: `${getPath.logs()}/${id}` };
        });
        await storage.batchWrite(ops);
      }
    } catch (err) {
      console.error('Error clearing logs:', err);
    }
    setShowClearConfirm(false);
  };

  const handleUndo = async (entry) => {
    if (!db) return;
    setUndoingId(entry.id);
    try {
      if (storage.undo) {
        const expanded = entry.undo
          ? expandUndo(entry, await storage.getDoc(entry.encounterPath))
          : getUndo(entry);
        if (!expanded) { setUndoingId(null); return; }
        await storage.undo({ logPath: `${getPath.logs()}/${entry.id}`, undo: expanded });
      }
    } catch (err) {
      console.error('Error undoing action:', err);
      showToast("Failed to roll back. The encounter may have changed or no longer exists.");    }
    setUndoingId(null);
  };

  return (
    <div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
      <header className="bg-stone-950 p-4 shadow-lg border-b border-amber-900">
        <div className="container mx-auto flex justify-between items-center">
          <h1 className="text-3xl font-bold text-amber-400 font-cinzel tracking-wide">Combat Log</h1>
          <div className="flex gap-2">
            <a
              href="/"
              className="px-4 py-2 rounded-md text-sm font-medium bg-stone-700 hover:bg-stone-600 text-white transition-colors"
            >
              ← Back to Tracker
            </a>
            <div className="relative">
              <button
                onClick={() => { setDlMenu(v => !v); setCpMenu(false); }}
                disabled={isLoading || typedCount === 0}
                className="px-4 py-2 rounded-md text-sm font-medium bg-stone-700 hover:bg-stone-600 text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
              >
                Download ▾
              </button>
              {dlMenu && (
                <div className="absolute right-0 mt-1 w-80 bg-stone-800 border border-stone-700 rounded-md shadow-xl z-20 py-1 max-h-96 overflow-y-auto">
                  <button
                    onClick={() => handleDownloadFor(null)}
                    className="block w-full text-left px-4 py-2 text-sm text-stone-100 hover:bg-stone-700"
                  >
                    All encounters ({typedCount})
                  </button>
                  {encounterGroups.length > 0 && (
                    <div className="border-t border-stone-700 my-1" />
                  )}
                  {encounterGroups.map(g => (
                    <button
                      key={g.key}
                      onClick={() => handleDownloadFor(g.key)}
                      className="block w-full text-left px-3 py-2 text-sm text-stone-100 hover:bg-stone-700"
                    >
                      <div className="font-medium truncate">{g.encounter}</div>
                      <div className="text-xs text-stone-400 truncate">{g.campaign}</div>
                      <div className="text-xs text-stone-500">{fmtDate(g.latest)} · {g.count} entries</div>
                    </button>
                  ))}
                </div>
              )}
            </div>
            <div className="relative">
              <button
                onClick={() => { setCpMenu(v => !v); setDlMenu(false); }}
                disabled={isLoading || typedCount === 0}
                className="px-4 py-2 rounded-md text-sm font-medium bg-stone-700 hover:bg-stone-600 text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
              >
                Copy ▾
              </button>
              {cpMenu && (
                <div className="absolute right-0 mt-1 w-80 bg-stone-800 border border-stone-700 rounded-md shadow-xl z-20 py-1 max-h-96 overflow-y-auto">
                  <button
                    onClick={() => handleCopyFor(null)}
                    className="block w-full text-left px-4 py-2 text-sm text-stone-100 hover:bg-stone-700"
                  >
                    All encounters ({typedCount})
                  </button>
                  {encounterGroups.length > 0 && (
                    <div className="border-t border-stone-700 my-1" />
                  )}
                  {encounterGroups.map(g => (
                    <button
                      key={g.key}
                      onClick={() => handleCopyFor(g.key)}
                      className="block w-full text-left px-3 py-2 text-sm text-stone-100 hover:bg-stone-700"
                    >
                      <div className="font-medium truncate">{g.encounter}</div>
                      <div className="text-xs text-stone-400 truncate">{g.campaign}</div>
                      <div className="text-xs text-stone-500">{fmtDate(g.latest)} · {g.count} entries</div>
                    </button>
                  ))}
                </div>
              )}
            </div>
            <button
              onClick={() => setShowClearConfirm(true)}
              disabled={isLoading || (logs || []).length === 0}
              className="px-4 py-2 rounded-md text-sm font-medium bg-red-800 hover:bg-red-700 text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
            >
              Clear Log
            </button>
          </div>
        </div>
      </header>

      <main className="mx-auto p-4 md:p-8 max-w-7xl">
        {isLoading ? (
          <LoadingSpinner message="Loading logs..." />
        ) : logs.length === 0 ? (
          <p className="text-stone-400 text-center mt-12 text-lg">No log entries yet.</p>
        ) : (
          <>
            <div className="flex items-center justify-between mb-4">
              <p className="text-stone-500 text-sm">
                {totalCount === null
                  ? `${shown} entries — newest first`
                  : `Showing ${rangeStart}–${rangeEnd} of ${totalCount}`}
              </p>
              <div className="flex items-center gap-2">
                <button
                  onClick={() => setPage(p => Math.max(0, p - 1))}
                  disabled={!hasPrev}
                  className="px-3 py-1 rounded text-sm bg-stone-700 hover:bg-stone-600 text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
                >
                  ← Prev
                </button>
                <span className="text-stone-500 text-sm">Page {page + 1}</span>
                <button
                  onClick={() => setPage(p => p + 1)}
                  disabled={!hasNext}
                  className="px-3 py-1 rounded text-sm bg-stone-700 hover:bg-stone-600 text-white transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
                >
                  Next →
                </button>
              </div>
            </div>
            <div className="space-y-1 max-w-5xl">
              {logs.map(entry => (
                <div
                  key={entry.id}
                  className={`flex gap-3 items-center p-3 rounded-md border text-sm transition-opacity ${
                    entry.undone
                      ? 'bg-stone-900/50 border-stone-800/50 opacity-50'
                      : 'bg-stone-900 border-stone-800'
                  }`}
                >
                  <span className="text-stone-500 whitespace-nowrap font-mono shrink-0">
                    {fmtDate(entry.ts || entry.timestamp)}
                  </span>
                  {entry.encounterName && (
                    <span className="text-amber-600 whitespace-nowrap shrink-0">[{entry.encounterName}]</span>
                  )}
                  <span className={`flex-1 ${entry.undone ? 'line-through text-stone-500' : 'text-stone-100'}`}>
                    {entry.message}
                  </span>
                  {entry.undone ? (
                    <span className="shrink-0 text-xs text-stone-600 italic">rolled back</span>
                  ) : getUndo(entry) ? (
                    <button
                      onClick={() => handleUndo(entry)}
                      disabled={undoingId === entry.id}
                      className="shrink-0 px-2 py-0.5 text-xs rounded bg-stone-700 hover:bg-amber-800 text-stone-300 hover:text-white transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                      title="Roll back this action"
                    >
                      {undoingId === entry.id ? '…' : '↩ Undo'}
                    </button>
                  ) : null}
                </div>
              ))}
            </div>
          </>
        )}
      </main>

      <ConfirmationModal
        isOpen={showClearConfirm}
        onClose={() => setShowClearConfirm(false)}
        onConfirm={handleClearLogs}
        title="Clear All Logs?"
        message="This will permanently delete all log entries and cannot be undone."
      />
    </div>
  );
}

// ============================================================================
// MAIN APP COMPONENT
// ============================================================================

function App() {
  const [userId, setUserId] = useState(null);
  const [isAuthReady, setIsAuthReady] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState(null);
  const [isPlayerViewOnlyMode, setIsPlayerViewOnlyMode] = useState(false);
  const [isLogsMode, setIsLogsMode] = useState(false);

  useEffect(() => {
    const queryParams = new URLSearchParams(window.location.search);
    const isDisplay = queryParams.get('playerView') === 'true' || window.location.pathname === '/display';
    if (isDisplay) {
      setIsPlayerViewOnlyMode(true);
      // swap manifest so Android home-screen installs launch /display
      const link = document.querySelector('link[rel="manifest"]');
      if (link) link.href = `${process.env.PUBLIC_URL || ''}/display-manifest.json`;
      const favicon = document.querySelector('link[rel="icon"]');
      if (favicon) favicon.href = `${process.env.PUBLIC_URL || ''}/player-favicon.ico`;
    }
    if (window.location.pathname === '/logs') {
      setIsLogsMode(true);
    }

    if (!auth) {
      setError("Auth not initialized.");
      setIsLoading(false);
      setIsAuthReady(false);
      return;
    }

    // ws/memory mode: stub auth, no SDK sign-in. Unblock UI immediately.
    if (STORAGE_MODE !== 'firebase') {
      setUserId(auth.currentUser?.uid || 'local-user');
      setIsAuthReady(true);
      setIsLoading(false);
      return;
    }

    const initAuth = async () => {
      try {
        const token = window.__initial_auth_token;
        if (token) {
          await signInWithCustomToken(auth, token);
        } else {
          await signInAnonymously(auth);
        }
      } catch (err) {
        console.error("Authentication error:", err);
        setError("Failed to authenticate. Please try again later.");
        // Auth failed and onAuthStateChanged won't fire with a user, so unblock the UI here
        setIsAuthReady(true);
        setIsLoading(false);
      }
    };

    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUserId(user ? user.uid : null);
      // Only mark auth ready once we have an actual authenticated user.
      // onAuthStateChanged fires with null before signInAnonymously completes,
      // which would cause Firestore queries to run unauthenticated.
      if (user) {
        setIsAuthReady(true);
        setIsLoading(false);
      }
    });

    initAuth();

    return () => unsubscribe();
  }, []);

  if (!isInitialized || !auth) {
    return (
      <ErrorDisplay
        critical
        message={`${STORAGE_MODE === 'firebase' ? 'Firebase' : 'Storage'} is not properly configured. Check your .env.local file and ensure all REACT_APP_* variables are correctly set.`}
      />
    );
  }

  if (isLoading || !isAuthReady) {
    return <LoadingSpinner message="Loading Initiative Tracker..." />;
  }

  if (error) {
    return <ErrorDisplay message={error} />;
  }

  const openPlayerWindow = () => {
    const playerViewUrl = window.location.origin + '/display';
    window.open(playerViewUrl, '_blank', 'noopener,noreferrer,width=1024,height=768');
  };

  if (isPlayerViewOnlyMode) {
    return (
      <UIFeedbackProvider>
        <div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
          {isAuthReady && <DisplayView />}
          {!isAuthReady && !error && <p>Authenticating for Player Display...</p>}
        </div>
      </UIFeedbackProvider>
    );
  }

  if (isLogsMode) {
    return (
      <UIFeedbackProvider>
        {isAuthReady ? <LogsView /> : <LoadingSpinner message="Authenticating..." />}
      </UIFeedbackProvider>
    );
  }

  return (
    <UIFeedbackProvider>
      <div className="min-h-screen bg-stone-950 text-stone-100 font-garamond">
      <header className="bg-stone-950 p-4 shadow-lg border-b border-amber-900">
        <div className="container mx-auto flex justify-between items-center">
          <h1 className="text-3xl font-bold text-amber-400 font-cinzel tracking-wide">TTRPG Initiative Tracker</h1>
          <div className="flex gap-2">
            <a
              href="/logs"
              target="_blank"
              rel="noopener noreferrer"
              className="px-4 py-2 rounded-md text-sm font-medium transition-colors bg-stone-700 hover:bg-stone-600 text-white flex items-center"
            >
              <ScrollText size={16} className="mr-2" /> View Logs
            </a>
            <button
              onClick={openPlayerWindow}
              className="px-4 py-2 rounded-md text-sm font-medium transition-colors bg-amber-700 hover:bg-amber-800 text-white flex items-center"
            >
              <ExternalLink size={16} className="mr-2" /> Open Player Window
            </button>
          </div>
        </div>
      </header>

      <main className="container mx-auto p-4 md:p-8">
        {isAuthReady && userId && <AdminView userId={userId} />}
        {!isAuthReady && !error && <p>Authenticating...</p>}
      </main>

      <footer className="bg-stone-950 p-4 text-center text-sm text-stone-400 mt-8">
        TTRPG Initiative Tracker {APP_VERSION}
      </footer>
      </div>
    </UIFeedbackProvider>
  );
}

export default App;
