// Main AOX WBS app — Supabase-backed with Realtime + offline cache
const STORAGE_TASKS = 'aox_tasks_v1';
const STORAGE_TWEAKS = 'aox_tweaks_v1';
const STORAGE_LAST_SYNC = 'aox_last_sync_v1';
const STORAGE_QUEUE = 'aox_pending_ops_v1';

function loadCachedTasks(){
  try {
    const raw = localStorage.getItem(STORAGE_TASKS);
    if (raw) return JSON.parse(raw);
  } catch {}
  return window.AOX_INITIAL_TASKS.map(t => ({...t}));
}
function cacheTasks(t){ try { localStorage.setItem(STORAGE_TASKS, JSON.stringify(t)); } catch{} }

const DEFAULT_TWEAKS = /*EDITMODE-BEGIN*/{
  "theme": "light",
  "density": "comfortable",
  "lang": "ko",
  "defaultView": "gantt",
  "font": "sf",
  "accent": "blue"
}/*EDITMODE-END*/;

function loadTweaks(){
  try { return {...DEFAULT_TWEAKS, ...(JSON.parse(localStorage.getItem(STORAGE_TWEAKS))||{})}; }
  catch { return {...DEFAULT_TWEAKS}; }
}
function saveTweaksLs(tw){ localStorage.setItem(STORAGE_TWEAKS, JSON.stringify(tw)); }

const ACCENTS = {
  blue:   { light:'#007AFF', dark:'#0A84FF', soft:'rgba(0,122,255,0.12)', softDark:'rgba(10,132,255,0.22)' },
  indigo: { light:'#5856D6', dark:'#5E5CE6', soft:'rgba(88,86,214,0.14)', softDark:'rgba(94,92,230,0.24)' },
  purple: { light:'#AF52DE', dark:'#BF5AF2', soft:'rgba(175,82,222,0.14)', softDark:'rgba(191,90,242,0.24)' },
  pink:   { light:'#FF2D55', dark:'#FF375F', soft:'rgba(255,45,85,0.14)',  softDark:'rgba(255,55,95,0.24)' },
  orange: { light:'#FF9500', dark:'#FF9F0A', soft:'rgba(255,149,0,0.15)', softDark:'rgba(255,159,10,0.24)' },
  green:  { light:'#34C759', dark:'#30D158', soft:'rgba(52,199,89,0.14)', softDark:'rgba(48,209,88,0.24)' },
  graphite:{light:'#3A3A3C', dark:'#8E8E93', soft:'rgba(58,58,60,0.12)',  softDark:'rgba(142,142,147,0.2)' },
};

const FONT_STACKS = {
  sf:       '-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Pretendard", "Apple SD Gothic Neo", system-ui, sans-serif',
  inter:    '"Inter", -apple-system, "Pretendard", "Apple SD Gothic Neo", sans-serif',
  pretendard:'"Pretendard", -apple-system, "Apple SD Gothic Neo", sans-serif',
  serif:    '"New York", "Noto Serif KR", "Times New Roman", serif',
  mono:     '"SF Mono", ui-monospace, "JetBrains Mono", Menlo, monospace',
};

function applyTweaks(tw) {
  const root = document.documentElement;
  root.dataset.theme = tw.theme;
  root.dataset.density = tw.density;
  const acc = ACCENTS[tw.accent] || ACCENTS.blue;
  const isDark = tw.theme === 'dark';
  root.style.setProperty('--accent', isDark ? acc.dark : acc.light);
  root.style.setProperty('--accent-soft', isDark ? acc.softDark : acc.soft);
  root.style.setProperty('--font-sans', FONT_STACKS[tw.font] || FONT_STACKS.sf);
}

function TweaksPanel({ tweaks, setTweaks, onClose, t, lang, perms }) {
  return (
    <div className="tweaks-panel">
      <header>
        <h3>Tweaks</h3>
        <button className="iconbtn" onClick={onClose} style={{width:22, height:22}}><Icon name="x" size={13}/></button>
      </header>
      <div className="body">
        <div className="tweak-row">
          <label>{t.labels.theme}</label>
          <div className="seg">
            {[['light','☀︎ Light'],['dark','☾ Dark']].map(([k,l])=>(
              <button key={k} className={tweaks.theme===k?'active':''} onClick={()=>setTweaks({theme:k})}>{l}</button>
            ))}
          </div>
        </div>
        <div className="tweak-row">
          <label>{t.labels.density}</label>
          <div className="seg">
            {[['comfortable',t.labels.comfortable],['compact',t.labels.compact]].map(([k,l])=>(
              <button key={k} className={tweaks.density===k?'active':''} onClick={()=>setTweaks({density:k})}>{l}</button>
            ))}
          </div>
        </div>
        <div className="tweak-row">
          <label>{t.labels.lang}</label>
          <div className="seg">
            {[['ko','한국어'],['en','English']].map(([k,l])=>(
              <button key={k} className={tweaks.lang===k?'active':''} onClick={()=>setTweaks({lang:k})}>{l}</button>
            ))}
          </div>
        </div>
        <div className="tweak-row">
          <label>{lang==='en'?'Default view':'기본 뷰'}</label>
          <div className="seg">
            {['gantt','kanban','table'].map(v=>(
              <button key={v} className={tweaks.defaultView===v?'active':''} onClick={()=>setTweaks({defaultView:v})}>{t.views[v]}</button>
            ))}
          </div>
        </div>
        <div className="tweak-row">
          <label>{t.labels.font}</label>
          <div className="seg">
            {[['sf','SF'],['inter','Inter'],['pretendard','Pret'],['serif','Serif']].map(([k,l])=>(
              <button key={k} className={tweaks.font===k?'active':''} onClick={()=>setTweaks({font:k})}>{l}</button>
            ))}
          </div>
        </div>
        <div className="tweak-row">
          <label>{lang==='en'?'Accent':'강조색'}</label>
          <div className="swatch-row">
            {Object.entries(ACCENTS).map(([k,v])=>(
              <button key={k} onClick={()=>setTweaks({accent:k})}
                className={'swatch' + (tweaks.accent===k ? ' active':'')}
                style={{background: tweaks.theme==='dark' ? v.dark : v.light}}/>
            ))}
          </div>
        </div>

        {perms && (
          <div className="tweak-row" style={{
            marginTop: 8, paddingTop: 12,
            borderTop: '1px dashed var(--divider)',
            flexDirection:'column', alignItems:'flex-start', gap: 6,
          }}>
            <label style={{fontSize: 10.5, textTransform:'uppercase', letterSpacing: 0.06, color:'var(--fg-subtle)'}}>
              {lang==='en'?'Current role (debug)':'현재 권한 (디버그)'}
            </label>
            <div style={{display:'flex', alignItems:'center', gap: 6, flexWrap:'wrap'}}>
              <RoleBadge role={perms.role} team={perms.team} lang={lang}/>
              <span className="muted mono" style={{fontSize: 10.5}}>
                {perms.canEdit ? 'edit ✓' : 'edit ✗'} · {perms.canDelete ? 'delete ✓' : 'delete ✗'}
              </span>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function App() {
  const [tasks, setTasks] = useState(loadCachedTasks);
  const [tweaks, setTweaksState] = useState(loadTweaks);
  const [view, setView] = useState(()=> loadTweaks().defaultView || 'gantt');
  const [search, setSearch] = useState('');
  const searchInputName = useRef(`task-search-${Math.random().toString(36).slice(2, 11)}`).current;
  const [filterPhase, setFilterPhase] = useState('');
  const [filterTrack, setFilterTrack] = useState('');
  const [openTaskObj, setOpenTaskObj] = useState(null);
  const [connectionOpen, setConnectionOpen] = useState(false);
  const [adminOpen, setAdminOpen] = useState(false);
  const [tweaksOpen, setTweaksOpen] = useState(false);
  const [addMenuOpen, setAddMenuOpen] = useState(false);
  // Dynamic catalog (loaded from DB once session is ready)
  const [phaseConfig, setPhaseConfig]   = useState(() => window.AOX_DB?.DEFAULT_PHASE_CONFIG    || []);
  const [milestones, setMilestones]     = useState(() => window.AOX_DB?.DEFAULT_MILESTONES      || []);
  const [workstreams, setWorkstreams]   = useState(() => window.AOX_DB?.DEFAULT_WORKSTREAM_CONFIG|| []);
  const [teamMembers, setTeamMembers] = useState([]);
  const [toast, setToast] = useState('');
  const [dbStatus, setDbStatus] = useState('connecting'); // connecting | online | offline
  const [lastSync, setLastSync] = useState(()=>Number(localStorage.getItem(STORAGE_LAST_SYNC))||0);
  const suppressLocalEchoRef = useRef(new Set()); // task ids we just wrote — ignore realtime echo briefly

  // --- Auth state -------------------------------------------------------
  const [authReady, setAuthReady] = useState(false);
  const [session, setSession] = useState(null);
  const [profile, setProfile] = useState(null);
  const perms = useMemo(() => window.AOX_AUTH.permissionsFor(profile), [profile]);

  const t = window.AOX_I18N[tweaks.lang];

  // Apply tweaks early (so login screen uses the right theme/font)
  useEffect(()=>{ applyTweaks(tweaks); }, []);

  // Subscribe to auth state
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const s = await window.AOX_DB.getSession();
      if (cancelled) return;
      setSession(s);
      setAuthReady(true);
    })();
    const stop = window.AOX_DB.onAuthStateChange((event, s) => {
      setSession(s);
      setAuthReady(true);
      if (event === 'SIGNED_OUT') {
        setProfile(null);
      }
    });
    return () => { cancelled = true; stop && stop(); };
  }, []);

  // Load profile when session arrives
  useEffect(() => {
    if (!session?.user?.id) { setProfile(null); return; }
    let cancelled = false;
    (async () => {
      const p = await window.AOX_DB.fetchProfile(session.user.id);
      if (cancelled) return;
      // Fallback profile if no row exists — viewer
      setProfile(p || {
        id: session.user.id,
        role: 'viewer',
        team: null,
        full_name: session.user.user_metadata?.full_name || null,
        email: session.user.email,
        avatar_url: session.user.user_metadata?.avatar_url || null,
      });
    })();
    return () => { cancelled = true; };
  }, [session?.user?.id]);

  async function doSignOut(){
    await window.AOX_DB.signOut();
    setSession(null); setProfile(null);
    if (channelRef.current) {
      try { await window.AOX_DB.unsubscribe(channelRef.current); } catch{}
      channelRef.current = null;
    }
  }

  function denyToast(){
    setToast(tweaks.lang==='en' ? 'No edit permission' : '편집 권한이 없습니다');
  }

  useEffect(()=>{ applyTweaks(tweaks); saveTweaksLs(tweaks); }, [tweaks]);
  // Refetch profile + reload data after sign-in
  useEffect(() => {
    if (!session?.user?.id) return;
    reloadFromDb();
    // re-subscribe handled by main effect on mount; if needed re-init handled there
  // eslint-disable-next-line
  }, [session?.user?.id]);
  useEffect(()=>{ cacheTasks(tasks); }, [tasks]);
  useEffect(()=>{ if (!toast) return; const id = setTimeout(()=>setToast(''), 2800); return ()=>clearTimeout(id); }, [toast]);

  function setTweaks(patch){
    setTweaksState(prev => ({...prev, ...patch}));
    try { window.parent?.postMessage({type: '__edit_mode_set_keys', edits: patch}, '*'); } catch {}
  }

  // Edit-mode protocol
  useEffect(()=>{
    function onMsg(e){
      if (e.data?.type === '__activate_edit_mode') setTweaksOpen(true);
      if (e.data?.type === '__deactivate_edit_mode') setTweaksOpen(false);
    }
    window.addEventListener('message', onMsg);
    window.parent?.postMessage({type: '__edit_mode_available'}, '*');
    return ()=>window.removeEventListener('message', onMsg);
  }, []);

  // Online/offline listener
  useEffect(()=>{
    function onOnline(){ setDbStatus(s => s === 'offline' ? 'connecting' : s); setToast(tweaks.lang==='en'?'Back online':'온라인 상태로 복구'); }
    function onOffline(){ setDbStatus('offline'); setToast(tweaks.lang==='en'?'Offline — edits cached locally':'오프라인 — 로컬에 캐시됩니다'); }
    window.addEventListener('online', onOnline);
    window.addEventListener('offline', onOffline);
    return ()=>{
      window.removeEventListener('online', onOnline);
      window.removeEventListener('offline', onOffline);
    };
  }, [tweaks.lang]);

  // Initial fetch from Supabase + realtime subscribe
  const channelRef = useRef(null);
  const reloadFromDb = useCallback(async () => {
    try {
      const [rows, phaseRows, msRows, teamRows, wsRows] = await Promise.all([
        window.AOX_DB.fetchTasks(),
        window.AOX_DB.fetchPhaseConfig().catch(e => { console.warn('phase_config load failed', e); return null; }),
        window.AOX_DB.fetchMilestones().catch(e => { console.warn('milestones load failed', e); return null; }),
        window.AOX_DB.fetchTeamMembers().catch(e => { console.warn('team load failed', e); return []; }),
        window.AOX_DB.fetchWorkstreamConfig().catch(e => { console.warn('workstream_config load failed', e); return null; }),
      ]);
      if (rows && rows.length) {
        setTasks(rows);
        const now = Date.now();
        setLastSync(now);
        try { localStorage.setItem(STORAGE_LAST_SYNC, String(now)); } catch {}
      }
      if (phaseRows && phaseRows.length) setPhaseConfig(phaseRows);
      if (msRows && msRows.length)       setMilestones(msRows);
      if (teamRows)                      { setTeamMembers(teamRows); window.AOX_TEAM_MEMBERS = teamRows; }
      if (wsRows && wsRows.length)       setWorkstreams(wsRows);
      setDbStatus('online');
    } catch (e) {
      console.warn('Supabase fetch failed — using cache', e);
      setDbStatus('offline');
      setToast((tweaks.lang==='en'?'Offline — using cached data. ':'오프라인 — 캐시된 데이터 사용. ') + (e.message||''));
    }
  }, [tweaks.lang]);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      await reloadFromDb();
      if (cancelled) return;
      try {
        const ch = await window.AOX_DB.subscribeAll((evt) => {
          if (evt.table === 'phase_config') {
            window.AOX_DB.fetchPhaseConfig().then(r => { if (r && r.length) setPhaseConfig(r); }).catch(()=>{});
            return;
          }
          if (evt.table === 'milestones') {
            window.AOX_DB.fetchMilestones().then(r => { if (r) setMilestones(r); }).catch(()=>{});
            return;
          }
          if (evt.table === 'workstream_config') {
            window.AOX_DB.fetchWorkstreamConfig().then(r => { if (r && r.length) setWorkstreams(r); }).catch(()=>{});
            return;
          }
          if (evt.table !== 'wbs_tasks') return;
          const { eventType, new: n, old: o } = evt;
          setTasks(prev => {
            if (eventType === 'DELETE') {
              const id = o?.id || o?.wbsId;
              return prev.filter(x => x.id !== id);
            }
            if (!n) return prev;
            if (suppressLocalEchoRef.current.has(n.id)) return prev;
            const idx = prev.findIndex(x => x.id === n.id);
            if (idx === -1) return [...prev, n];
            const copy = prev.slice();
            copy[idx] = {...copy[idx], ...n};
            return copy;
          });
          const now = Date.now();
          setLastSync(now);
          try { localStorage.setItem(STORAGE_LAST_SYNC, String(now)); } catch {}
        });
        channelRef.current = ch;
      } catch (e) {
        console.warn('Realtime subscribe failed', e);
      }
    })();
    return () => {
      cancelled = true;
      if (channelRef.current) window.AOX_DB.unsubscribe(channelRef.current);
    };
  // eslint-disable-next-line
  }, []);

  // --- Optimistic write helpers -----------------------------------------
  function markEcho(id){
    suppressLocalEchoRef.current.add(id);
    setTimeout(()=>suppressLocalEchoRef.current.delete(id), 1200);
  }
  function handleDbError(e){
    if (!e) return;
    if (e.code === '409' || e.status === 409 || /conflict/i.test(e.message||'')) {
      setToast(tweaks.lang==='en'
        ? 'Conflict — please reload and retry'
        : '편집 충돌 — 새로고침 후 다시 시도해 주세요');
    } else if (!navigator.onLine) {
      setDbStatus('offline');
      setToast(tweaks.lang==='en'?'Offline — saved locally':'오프라인 — 로컬에 저장됨');
    } else {
      setDbStatus('offline');
      setToast('Supabase: ' + (e.message || e.code || 'error'));
    }
  }

  const updateTask = useCallback((id, patch) => {
    if (!perms.canEdit) { denyToast(); return; }
    // Snapshot previous state for rollback
    let prevSnapshot = null;
    setTasks(prev => {
      prevSnapshot = prev.find(tk => tk.id === id);
      return prev.map(tk => {
        if (tk.id !== id) return tk;
        const next = {...tk, ...patch};
        // duration is computed locally for display only (DB has GENERATED column)
        if (patch.start || patch.end) {
          const s = AOX_UTIL.parseDate(next.start);
          const e = AOX_UTIL.parseDate(next.end);
          if (s && e) next.duration = AOX_UTIL.diffDays(s, e) + 1;
        }
        return next;
      });
    });
    // Send ONLY the changed fields; db.js will strip generated columns via whitelist
    markEcho(id);
    window.AOX_DB.updateTask(id, patch)
      .then(()=>{
        setDbStatus('online');
        const now = Date.now(); setLastSync(now);
        try { localStorage.setItem(STORAGE_LAST_SYNC, String(now)); } catch {}
      })
      .catch(err => {
        // Rollback optimistic update
        if (prevSnapshot) {
          setTasks(prev => prev.map(tk => tk.id === id ? prevSnapshot : tk));
        }
        handleDbError(err);
      });
  }, [tweaks.lang, perms.canEdit]);

  const deleteTask = useCallback(id => {
    if (!perms.canDelete) { denyToast(); return; }
    if (!id) return;
    setTasks(prev => prev.filter(tk => tk.id !== id));
    markEcho(id);
    window.AOX_DB.deleteTask(id)
      .then(()=>{ setDbStatus('online'); })
      .catch(handleDbError);
  }, [tweaks.lang, perms.canDelete]);

  const [hideDone, setHideDone] = useState(() => localStorage.getItem('aox_hide_done') === '1');
  const [filterOwner, setFilterOwner] = useState('');
  useEffect(() => { localStorage.setItem('aox_hide_done', hideDone ? '1' : '0'); }, [hideDone]);

  const [justAddedId, setJustAddedId] = useState(null);
  const justAddedTimerRef = useRef(null);
  const addTask = useCallback((phaseOverride, trackOverride) => {
    if (!perms.canCreate) { denyToast(); return; }
    // Remember the user's last chosen (phase, track) so the next single-click
    // Add-task lands in the same place. Falls back to current filter, then P1/dev.
    const lastPhase = localStorage.getItem('aox_last_add_phase');
    const lastTrack = localStorage.getItem('aox_last_add_track');
    const phase = phaseOverride || filterPhase || lastPhase || 'Phase 1';
    const track = trackOverride || filterTrack || lastTrack || workstreams[0]?.key || '개발';
    if (phaseOverride) localStorage.setItem('aox_last_add_phase', phaseOverride);
    if (trackOverride) localStorage.setItem('aox_last_add_track', trackOverride);
    const pre = phase.replace('Phase ','');
    const existing = tasks.filter(t => t.id.startsWith(pre+'.'));
    const id = `${pre}.X.${existing.length+1}`;
    const today = AOX_UTIL.fmtDate(new Date());
    const end = AOX_UTIL.fmtDate(AOX_UTIL.addDays(new Date(), 7));
    // Put the new row at the BOTTOM of its (phase, workstream) group so it
    // picks up the next natural display-id.
    const siblings = tasks.filter(t => t.phase === phase && t.workstream === track);
    const maxSort = siblings.reduce((m, x) => Math.max(m, x.sortOrder ?? 0), 0);
    const nt = {
      id, wbsId: id, phase, workstream: track, deliverable: t.labels.newTask, task: t.labels.newTask,
      owner:'', start: today, end, duration: 8, dependency:'',
      status:'Not Started', progress:0, budgetPlan:0, budgetActual:0, risk:'Low', note:'',
      sortOrder: maxSort + 10,
    };
    setTasks(prev => [...prev, nt]);
    setOpenTaskObj(nt);
    setJustAddedId(id);
    if (justAddedTimerRef.current) clearTimeout(justAddedTimerRef.current);
    justAddedTimerRef.current = setTimeout(() => {
      setJustAddedId(prev => prev === id ? null : prev);
    }, 500);
    markEcho(id);
    window.AOX_DB.insertTask(nt)
      .then(()=>{ setDbStatus('online'); })
      .catch(handleDbError);
  }, [tasks, filterPhase, filterTrack, t, perms.canCreate]);

  const filtered = useMemo(() => {
    const q = search.trim().toLowerCase();
    return tasks.filter(tk => {
      if (filterPhase && tk.phase !== filterPhase) return false;
      if (filterTrack && tk.workstream !== filterTrack) return false;
      if (filterOwner === 'me' && profile) {
        const mine = [profile.email, profile.full_name, profile.english_name].filter(Boolean);
        if (!mine.some(v => v && v === tk.owner)) return false;
      }
      if (q) {
        const hay = [tk.id, tk.task, tk.deliverable, tk.note, tk.owner].join(' ').toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    });
  }, [tasks, search, filterPhase, filterTrack, filterOwner, profile]);

  // Dynamic sort-order map built from DB workstream_config. Fallback 9 = end.
  const wsOrderMap = useMemo(
    () => Object.fromEntries(workstreams.map(ws => [ws.key, ws.sort_order])),
    [workstreams]
  );

  // sortedFiltered: used by Budget + Milestones (hideDone does NOT apply here).
  const sortedFiltered = useMemo(() => {
    return [...filtered].sort((a, b) => {
      const pa = AOX_UTIL.phaseNum(a.phase), pb = AOX_UTIL.phaseNum(b.phase);
      if (pa !== pb) return pa - pb;
      const wa = wsOrderMap[a.workstream] ?? 9, wb = wsOrderMap[b.workstream] ?? 9;
      if (wa !== wb) return wa - wb;
      const sa = a.sortOrder ?? 1e9, sb = b.sortOrder ?? 1e9;
      if (sa !== sb) return sa - sb;
      return AOX_UTIL.compareWbs(a.id, b.id);
    });
  }, [filtered, wsOrderMap]);

  // viewFiltered / sortedViewFiltered: used by Gantt / Kanban / Table.
  // Respects hideDone in addition to all other filters.
  const viewFiltered = useMemo(() => {
    return hideDone ? filtered.filter(tk => tk.status !== 'Done') : filtered;
  }, [filtered, hideDone]);
  const sortedViewFiltered = useMemo(() => {
    return [...viewFiltered].sort((a, b) => {
      const pa = AOX_UTIL.phaseNum(a.phase), pb = AOX_UTIL.phaseNum(b.phase);
      if (pa !== pb) return pa - pb;
      const wa = wsOrderMap[a.workstream] ?? 9, wb = wsOrderMap[b.workstream] ?? 9;
      if (wa !== wb) return wa - wb;
      const sa = a.sortOrder ?? 1e9, sb = b.sortOrder ?? 1e9;
      if (sa !== sb) return sa - sb;
      return AOX_UTIL.compareWbs(a.id, b.id);
    });
  }, [viewFiltered, wsOrderMap]);

  const phases = phaseConfig.map(p => p.phase_code);
  const phaseLabelMap = Object.fromEntries(phaseConfig.map(p => [p.phase_code, p.label]));
  const tracks = workstreams.map(ws => ws.key);

  // Compute display-ids from the FULL task set (not the filtered one) so that
  // numbering doesn't jump when the user narrows the filter.
  const displayIds = useMemo(() => AOX_UTIL.computeDisplayIds(tasks), [tasks]);

  /**
   * Reorder handler. Takes an array of { id, sortOrder, phase?, workstream? }.
   * Optimistically updates local state then persists in the background.
   * If the DB call fails, we roll back to the pre-reorder snapshot.
   */
  const reorderTasks = useCallback((updates) => {
    if (!perms.canEdit) { denyToast(); return; }
    if (!updates || !updates.length) return;
    const prevSnapshot = tasks;
    setTasks(prev => prev.map(tk => {
      const u = updates.find(x => x.id === tk.id);
      if (!u) return tk;
      return {
        ...tk,
        sortOrder: u.sortOrder != null ? u.sortOrder : tk.sortOrder,
        phase:      u.phase      != null ? u.phase      : tk.phase,
        workstream: u.workstream != null ? u.workstream : tk.workstream,
      };
    }));
    updates.forEach(u => markEcho(u.id));
    window.AOX_DB.reorderTasks(updates)
      .then(() => { setDbStatus('online'); })
      .catch(err => {
        setTasks(prevSnapshot);
        handleDbError(err);
      });
  }, [tasks, perms.canEdit, tweaks.lang]);

  const viewEls = {
    gantt: <GanttView tasks={viewFiltered} allTasks={tasks} displayIds={displayIds} updateTask={updateTask} reorderTasks={reorderTasks} openTask={setOpenTaskObj} onAddTaskAt={addTask} justAddedId={justAddedId} t={t} lang={tweaks.lang} density={tweaks.density} perms={perms} milestones={milestones} workstreams={workstreams} phaseConfig={phaseConfig}/>,
    kanban: <KanbanView tasks={sortedViewFiltered} displayIds={displayIds} updateTask={updateTask} openTask={setOpenTaskObj} t={t} lang={tweaks.lang} perms={perms} workstreams={workstreams}/>,
    table: <TableView tasks={sortedViewFiltered} displayIds={displayIds} updateTask={updateTask} deleteTask={deleteTask} openTask={setOpenTaskObj} justAddedId={justAddedId} t={t} lang={tweaks.lang} density={tweaks.density} perms={perms} workstreams={workstreams}/>,
    budget: <BudgetView tasks={sortedFiltered} updateTask={updateTask} t={t} lang={tweaks.lang} perms={perms} workstreams={workstreams}/>,
    milestones: <MilestonesView tasks={sortedFiltered} t={t} lang={tweaks.lang} milestones={milestones} phaseConfig={phaseConfig}/>,
  };

  // --- Auth gates -------------------------------------------------------
  if (!authReady) {
    return (
      <div style={{position:'fixed', inset:0, display:'flex', alignItems:'center', justifyContent:'center', background:'var(--bg)', color:'var(--fg-muted)', fontSize: 13}}>
        <div style={{display:'flex', alignItems:'center', gap: 10}}>
          <span style={{
            width: 14, height: 14, borderRadius:'50%',
            border: '2px solid var(--border)', borderTopColor:'var(--accent)',
            animation: 'spin 0.8s linear infinite', display:'inline-block'
          }}/>
          {tweaks.lang==='en' ? 'Checking session…' : '세션 확인 중…'}
        </div>
        <style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
      </div>
    );
  }
  if (!session) {
    return <LoginScreen lang={tweaks.lang} onLangToggle={()=>setTweaks({lang: tweaks.lang==='en' ? 'ko' : 'en'})}/>;
  }

  const connLabel = dbStatus === 'online'
    ? (tweaks.lang==='en'?'Live':'실시간')
    : dbStatus === 'connecting'
    ? (tweaks.lang==='en'?'Connecting…':'연결 중…')
    : (tweaks.lang==='en'?'Offline':'오프라인');
  const connLedColor = dbStatus === 'online' ? 'var(--status-done)'
    : dbStatus === 'offline' ? 'var(--status-blocked)'
    : 'var(--phase1)';

  return (
    <div className="app">
      <div className="topbar">
        <div className="brand">
          <div className="brand-mark"><AoxLogo size="sm"/></div>
          <div className="brand-text">
            <div className="brand-title">{t.appName}</div>
            <div className="brand-sub">{t.appSub}</div>
          </div>
        </div>

        <div className="segment">
          {Object.entries(t.views).map(([k,l]) => (
            <button key={k}
              className={'segment-item' + (view===k?' active':'')}
              onClick={()=>setView(k)}>
              {l}
            </button>
          ))}
        </div>

        <div className="spacer"/>

        <div className="search">
          <Icon name="search" size={13}/>
          <input type="text" name="prevent_autofill" autoComplete="username" style={{display:'none'}} tabIndex={-1} aria-hidden="true"/>
          <input type="password" name="prevent_autofill_pw" autoComplete="current-password" style={{display:'none'}} tabIndex={-1} aria-hidden="true"/>
          <input
            type="search"
            name={searchInputName}
            autoComplete="off"
            role="searchbox"
            aria-label={t.labels.search}
            placeholder={t.labels.search}
            value={search}
            onChange={e=>setSearch(e.target.value)}
          />
        </div>

        <div style={{position:'relative', display:'inline-flex'}}>
          <button className="btn btn-primary" onClick={()=>addTask()}
            disabled={!perms.canCreate}
            title={!perms.canCreate ? (tweaks.lang==='en'?'No permission':'권한 없음') : ''}
            style={{
              ...(perms.canCreate ? {} : {opacity: 0.45, cursor:'not-allowed'}),
              borderTopRightRadius: 0, borderBottomRightRadius: 0, paddingRight: 10,
            }}>
            <Icon name="plus" size={14}/>{t.labels.addTask}
          </button>
          <button className="btn btn-primary"
            onClick={()=>{ if (perms.canCreate) setAddMenuOpen(v=>!v); }}
            disabled={!perms.canCreate}
            title={tweaks.lang==='en'?'Choose phase':'단계 선택'}
            aria-label={tweaks.lang==='en'?'Choose phase':'단계 선택'}
            style={{
              ...(perms.canCreate ? {} : {opacity: 0.45, cursor:'not-allowed'}),
              borderTopLeftRadius: 0, borderBottomLeftRadius: 0,
              paddingLeft: 6, paddingRight: 8,
              borderLeft: '1px solid rgba(255,255,255,0.25)',
            }}>
            <Icon name="chevron-down" size={12}/>
          </button>
          {addMenuOpen && (
            <>
              <div onClick={()=>setAddMenuOpen(false)} style={{
                position:'fixed', inset:0, zIndex: 40,
              }}/>
              <div style={{
                position:'absolute', top:'calc(100% + 6px)', right:0, zIndex: 41,
                minWidth: 260,
                background:'var(--bg-elev)',
                backdropFilter:'blur(20px)',
                border:'1px solid var(--border)',
                borderRadius: 12,
                boxShadow:'var(--shadow-2)',
                padding: 6,
                fontSize: 12.5,
                maxHeight: '70vh', overflowY:'auto',
              }}>
                <div style={{
                  padding:'6px 10px 4px', fontSize: 10.5, fontWeight: 600,
                  color:'var(--fg-muted)', textTransform:'uppercase', letterSpacing: 0.06,
                }}>
                  {tweaks.lang==='en'?'Add task to…':'작업 추가 위치…'}
                </div>
                {phases.flatMap(ph =>
                  tracks.map(tr => (
                    <button key={ph+'|'+tr}
                      onClick={()=>{ setAddMenuOpen(false); addTask(ph, tr); }}
                      style={{
                        display:'flex', alignItems:'center', gap: 8,
                        width:'100%', padding:'7px 10px',
                        background:'transparent', border:'none', cursor:'pointer',
                        borderRadius: 8, fontSize: 12.5, textAlign:'left',
                        color:'var(--fg)',
                      }}
                      onMouseEnter={e=>e.currentTarget.style.background='var(--hover)'}
                      onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
                      <PhaseTag phase={ph}/>
                      <TrackTag track={tr}/>
                      <span style={{flex:1}}/>
                      <span className="muted" style={{fontSize: 11}}>{phaseLabelMap[ph]}</span>
                    </button>
                  ))
                )}
              </div>
            </>
          )}
        </div>

        <button className={'sheet-pill' + (dbStatus==='online' ? ' connected':'')}
          onClick={()=>setConnectionOpen(true)}
          style={{cursor:'pointer'}}
          title={connLabel}>
          <span className="led" style={{
            background: connLedColor,
            boxShadow: dbStatus==='online' ? '0 0 8px rgba(52,199,89,0.6)' : 'none',
            animation: dbStatus==='online' ? 'pulse 1.6s ease-in-out infinite' : 'none'
          }}/>
          <span>Supabase · {connLabel}</span>
        </button>

        <button className={'iconbtn' + (tweaksOpen?' active':'')} onClick={()=>setTweaksOpen(v=>!v)} title="Tweaks">
          <Icon name="sliders" size={16}/>
        </button>

        {perms.role === 'admin' && (
          <button className="iconbtn" onClick={()=>setAdminOpen(true)}
            title={tweaks.lang==='en' ? 'Admin settings' : '관리자 설정'}>
            <Icon name="settings" size={16}/>
          </button>
        )}

        <UserMenu user={session.user} profile={profile} perms={perms} lang={tweaks.lang} onSignOut={doSignOut} filterOwner={filterOwner} setFilterOwner={setFilterOwner}/>
      </div>

      <div className="subbar">
        <button className={'chip' + (!filterPhase ? ' active':'')} onClick={()=>setFilterPhase('')}>
          {t.labels.all}
        </button>
        {phaseConfig.map(pc => {
          const n = (pc.phase_code||'').match(/\d/)?.[0] || '1';
          const chipColor = pc.color || `var(--phase${n})`;
          const label = pc.label;
          return (
            <button key={pc.phase_code} className={'chip' + (filterPhase===pc.phase_code ? ' active':'')} onClick={()=>setFilterPhase(pc.phase_code===filterPhase ? '' : pc.phase_code)}
              title={label || ''}>
              <span className="chip-dot" style={{color: chipColor}}/>
              {pc.phase_code}{label ? <span style={{marginLeft: 6, color:'var(--fg-muted)', fontWeight: 400}}>· {label}</span> : null}
            </button>
          );
        })}
        <span style={{width: 1, background:'var(--border)', alignSelf:'stretch', margin:'0 4px'}}/>
        {workstreams.map(ws => (
          <button key={ws.key} className={'chip' + (filterTrack===ws.key ? ' active':'')} onClick={()=>setFilterTrack(ws.key===filterTrack?'':ws.key)}>
            <span className="chip-dot" style={{color: ws.color}}/>
            {ws.label}
          </button>
        ))}
        <span style={{flex:1}}/>
        {['table','gantt','kanban'].includes(view) && (
          <button
            className={'chip' + (hideDone ? ' active' : '')}
            onClick={() => setHideDone(v => !v)}
            title={tweaks.lang === 'en' ? 'Hide completed tasks' : '완료된 업무 숨기기'}>
            {hideDone ? '☑' : '☐'} {tweaks.lang === 'en' ? 'Hide done' : '완료 숨김'}
          </button>
        )}
        <span className="muted" style={{fontSize:11.5}}>
          {filtered.length} / {tasks.length} {t.labels.tasksCount}
        </span>
      </div>

      <div className="main">
        <div className={'view view-' + view} key={view}>
          {viewEls[view]}
        </div>
      </div>

      {openTaskObj && (
        <TaskModal
          task={tasks.find(x => x.id === openTaskObj.id) || openTaskObj}
          displayId={displayIds[openTaskObj.id]}
          isNew={openTaskObj.id === justAddedId}
          onClose={()=>setOpenTaskObj(null)}
          updateTask={updateTask}
          deleteTask={deleteTask}
          t={t} lang={tweaks.lang}
          perms={perms}
          phaseConfig={phaseConfig}
          milestones={milestones}
          workstreams={workstreams}
        />
      )}
      {connectionOpen && (
        <ConnectionModal
          onClose={()=>setConnectionOpen(false)}
          status={dbStatus}
          lastSync={lastSync}
          onReload={()=>{ setConnectionOpen(false); reloadFromDb().then(()=>setToast(tweaks.lang==='en'?'Reloaded':'다시 불러왔습니다')); }}
          t={t} lang={tweaks.lang}/>
      )}
      {tweaksOpen && (
        <TweaksPanel tweaks={tweaks} setTweaks={setTweaks} onClose={()=>setTweaksOpen(false)} t={t} lang={tweaks.lang} perms={perms}/>
      )}
      {adminOpen && (
        <AdminSettings
          onClose={()=>setAdminOpen(false)}
          perms={perms}
          phases={phaseConfig}
          milestones={milestones}
          team={teamMembers}
          lang={tweaks.lang} t={t}
          reload={reloadFromDb}
          workstreams={workstreams}
          onWorkstreamsChange={setWorkstreams}
        />
      )}
      {toast && <div className="toast">{toast}</div>}
    </div>
  );
}

applyTweaks(loadTweaks());
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
