import { h } from 'preact';
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';

// ── Types (mirror internal/access AccessSnapshot + the /admin/stats payload) ──

interface MemberInfo { user: string; role: string; }

interface GroupInfo {
  name: string;
  parent: string;
  members: MemberInfo[];
}

interface UserInfo {
  name: string;
  effectiveRole: string;
  roles: Record<string, string>; // group → role token
}

interface AccessSnapshot {
  defaultUser: string;
  publicGroup: string;
  roles: string[]; // role ladder, low → high
  configured: boolean;
  users: UserInfo[];
  groups: GroupInfo[];
}

interface ServerStats {
  uptimeSeconds: number;
  wsConnections: number;
  observedSignals: number;
  msgIn: number;
  msgOut: number;
  writeOps: number;
  historyReqs: number;
  goroutines: number;
  memAllocBytes: number;
  memSysBytes: number;
  heapInuseBytes: number;
  loadAvg: number[] | null;
  dataSources: string[];
}

interface Props { onClose: () => void; }

const msg = (err: unknown): string => (err instanceof Error ? err.message : String(err));

async function apiJSON<T = any>(url: string, opts?: RequestInit): Promise<T | null> {
  const res = await fetch(url, opts);
  if (!res.ok && res.status !== 204) {
    let m = `HTTP ${res.status}`;
    try { const e = await res.json(); if (e && e.error) m = e.error; } catch { /* ignore */ }
    throw new Error(m);
  }
  if (res.status === 204) return null;
  return res.json();
}

const jsonPut = (body: any): RequestInit => ({ method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const jsonPost = (body: any): RequestInit => ({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });

const ROLE_LABELS: Record<string, string> = {
  viewer: 'Viewer',
  operator: 'Operator',
  logiceditor: 'Logic editor',
  auditor: 'Auditor',
  admin: 'Admin',
};
const roleLabel = (r: string): string => ROLE_LABELS[r] ?? r;

/** Order group names so children follow their parent, and compute nesting depth. */
function orderGroups(groups: GroupInfo[]): { group: GroupInfo; depth: number }[] {
  const byParent = new Map<string, GroupInfo[]>();
  for (const g of groups) {
    const key = g.parent || '';
    (byParent.get(key) ?? byParent.set(key, []).get(key)!).push(g);
  }
  for (const list of byParent.values()) list.sort((a, b) => a.name.localeCompare(b.name));
  const out: { group: GroupInfo; depth: number }[] = [];
  const names = new Set(groups.map(g => g.name));
  const walk = (parent: string, depth: number) => {
    for (const g of byParent.get(parent) ?? []) {
      out.push({ group: g, depth });
      walk(g.name, depth + 1);
    }
  };
  walk('', 0);
  // Orphans (parent missing) rendered at root.
  for (const g of groups) {
    if (g.parent && !names.has(g.parent) && !out.some(o => o.group.name === g.name)) {
      out.push({ group: g, depth: 0 });
    }
  }
  return out;
}

export default function AdminPane({ onClose }: Props) {
  const [tab, setTab] = useState<'users' | 'groups' | 'stats'>('users');
  const [snap, setSnap] = useState<AccessSnapshot | null>(null);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    setError(null);
    try {
      setSnap(await apiJSON<AccessSnapshot>('/api/v1/admin/access'));
    } catch (err) { setError(msg(err)); }
  }, []);
  useEffect(() => { load(); }, [load]);

  return (
    <div class="cl-overlay" onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div class="cl-modal cl-modal-full">
        <header class="cl-header">
          <span class="cl-title">Admin</span>
          <span class="hint cl-subtitle">Manage role-based access through group memberships, and view live server statistics.</span>
          <div class="cl-header-actions">
            <button class="panel-btn" onClick={onClose}>Close</button>
          </div>
        </header>

        <div class="cfg-tabs">
          <button class={`cfg-tab${tab === 'users' ? ' cfg-tab-active' : ''}`} onClick={() => setTab('users')}>Users</button>
          <button class={`cfg-tab${tab === 'groups' ? ' cfg-tab-active' : ''}`} onClick={() => setTab('groups')}>Groups</button>
          <button class={`cfg-tab${tab === 'stats' ? ' cfg-tab-active' : ''}`} onClick={() => setTab('stats')}>Server stats</button>
        </div>

        {error && <div class="cl-error">{error}</div>}
        {snap && !snap.configured && tab !== 'stats' && (
          <div class="hint admin-banner">
            No roles assigned yet — access is fully open (everyone is admin). Assigning any role
            switches to strict mode where unlisted users are read-only viewers.
          </div>
        )}

        {tab === 'users' && snap && <UsersManager snap={snap} onSnap={setSnap} onError={setError} />}
        {tab === 'groups' && snap && <GroupsManager snap={snap} onSnap={setSnap} onError={setError} />}
        {tab === 'stats' && <StatsView onError={setError} />}
      </div>
    </div>
  );
}

interface ManagerProps {
  snap: AccessSnapshot;
  onSnap: (s: AccessSnapshot) => void;
  onError: (e: string | null) => void;
}

// ── Users ─────────────────────────────────────────────────────────────────────

function UsersManager({ snap, onSnap, onError }: ManagerProps) {
  const [selected, setSelected] = useState<string | null>(null);
  // Draft maps group → role token for the selected user (only groups with a role).
  const [draft, setDraft] = useState<Record<string, string> | null>(null);
  const [newName, setNewName] = useState('');
  const [busy, setBusy] = useState(false);

  const ordered = useMemo(() => orderGroups(snap.groups), [snap.groups]);

  function select(name: string) {
    const u = snap.users.find(x => x.name === name);
    setSelected(name);
    setDraft({ ...(u?.roles ?? {}) });
  }

  function addUser() {
    const n = newName.trim();
    if (!n) return;
    setNewName('');
    select(n);
  }

  const setGroupRole = (group: string, role: string) => setDraft(d => {
    const next = { ...(d ?? {}) };
    if (role === '') delete next[group];
    else next[group] = role;
    return next;
  });

  async function save() {
    if (!draft || !selected) return;
    setBusy(true);
    onError(null);
    try {
      const updated = await apiJSON<AccessSnapshot>(
        `/api/v1/admin/users/${encodeURIComponent(selected)}`, jsonPut({ roles: draft }));
      if (updated) onSnap(updated);
    } catch (err) { onError(msg(err)); }
    finally { setBusy(false); }
  }

  const effective = selected ? snap.users.find(u => u.name === selected)?.effectiveRole : undefined;

  return (
    <div class="cl-body">
      <div class="cl-list">
        <div class="cl-list-head">
          <input class="audit-filter" type="text" placeholder="Add user…" value={newName}
            onInput={(e) => setNewName((e.target as HTMLInputElement).value)}
            onKeyDown={(e) => { if (e.key === 'Enter') addUser(); }} />
          <button class="panel-btn" onClick={addUser}>Add</button>
        </div>
        {snap.users.length === 0 && <div class="hint cl-list-empty">No users with explicit roles. Everyone is a viewer of the public group.</div>}
        {snap.users.map(u => (
          <div key={u.name} class={`cl-list-item${selected === u.name ? ' cl-list-item-active' : ''}`} onClick={() => select(u.name)}>
            <span class="cl-list-name" title={u.name}>{u.name}</span>
            <span class={`admin-role-tag admin-role-${u.effectiveRole}`}>{roleLabel(u.effectiveRole)}</span>
          </div>
        ))}
      </div>

      <div class="cl-editor-wrap">
        {!draft && <div class="hint admin-empty">Select a user, or add one, to assign roles per group.</div>}
        {draft && (
          <div class="admin-editor">
            <h3 class="admin-editor-title">
              {selected}
              {effective && <span class={`admin-role-tag admin-role-${effective}`}>{roleLabel(effective)}</span>}
            </h3>
            <p class="hint">Effective access is the highest role across all groups. Every user is at least a viewer via the public group.</p>

            <label class="admin-field-label">Role per group</label>
            <div class="admin-role-table">
              {ordered.map(({ group, depth }) => (
                <div key={group.name} class="admin-role-row">
                  <span class="admin-role-group" style={{ paddingLeft: `${depth * 1.1}rem` }}>
                    {depth > 0 && <span class="admin-tree-mark">└ </span>}
                    {group.name}
                    {group.name === snap.publicGroup && <span class="hint"> (everyone)</span>}
                  </span>
                  <select class="prop-select admin-role-select"
                    value={draft[group.name] ?? ''}
                    onChange={(e) => setGroupRole(group.name, (e.target as HTMLSelectElement).value)}>
                    <option value="">— none —</option>
                    {snap.roles.map(r => <option key={r} value={r}>{roleLabel(r)}</option>)}
                  </select>
                </div>
              ))}
            </div>

            <div class="admin-editor-actions">
              <button class="panel-btn" disabled={busy} onClick={save}>{busy ? 'Saving…' : 'Save'}</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ── Groups ──────────────────────────────────────────────────────────────────

function descendantsOf(groups: GroupInfo[], name: string): Set<string> {
  const out = new Set<string>();
  const walk = (parent: string) => {
    for (const g of groups) {
      if (g.parent === parent && !out.has(g.name)) {
        out.add(g.name);
        walk(g.name);
      }
    }
  };
  walk(name);
  return out;
}

function GroupsManager({ snap, onSnap, onError }: ManagerProps) {
  const [selected, setSelected] = useState<string | null>(null);
  const [name, setName] = useState('');
  const [parent, setParent] = useState('');
  const [members, setMembers] = useState<MemberInfo[]>([]);
  const [newMember, setNewMember] = useState('');
  const [newGroup, setNewGroup] = useState('');
  const [busy, setBusy] = useState(false);

  const ordered = useMemo(() => orderGroups(snap.groups), [snap.groups]);
  const isPublic = selected === snap.publicGroup;

  function select(gname: string) {
    const g = snap.groups.find(x => x.name === gname);
    if (!g) return;
    setSelected(gname);
    setName(g.name);
    setParent(g.parent);
    setMembers(g.members.map(m => ({ ...m })));
    setNewMember('');
  }

  // Parent options exclude the group itself and its descendants (cycle guard) and public.
  const parentOptions = useMemo(() => {
    if (!selected) return [];
    const blocked = descendantsOf(snap.groups, selected);
    blocked.add(selected);
    return snap.groups.filter(g => !blocked.has(g.name)).map(g => g.name);
  }, [snap.groups, selected]);

  async function create() {
    const n = newGroup.trim();
    if (!n) return;
    setBusy(true);
    onError(null);
    try {
      const updated = await apiJSON<AccessSnapshot>('/api/v1/admin/groups', jsonPost({ name: n }));
      if (updated) onSnap(updated);
      setNewGroup('');
    } catch (err) { onError(msg(err)); }
    finally { setBusy(false); }
  }

  function addMember() {
    const u = newMember.trim();
    if (!u || members.some(m => m.user === u)) { setNewMember(''); return; }
    setMembers(ms => [...ms, { user: u, role: 'operator' }]);
    setNewMember('');
  }

  const setMemberRole = (user: string, role: string) =>
    setMembers(ms => ms.map(m => m.user === user ? { ...m, role } : m));
  const removeMember = (user: string) => setMembers(ms => ms.filter(m => m.user !== user));

  async function save() {
    if (!selected) return;
    setBusy(true);
    onError(null);
    try {
      const memberMap: Record<string, string> = {};
      for (const m of members) memberMap[m.user] = m.role;
      const body = { name: name.trim() || selected, parent: isPublic ? '' : parent, members: memberMap };
      const updated = await apiJSON<AccessSnapshot>(
        `/api/v1/admin/groups/${encodeURIComponent(selected)}`, jsonPut(body));
      if (updated) { onSnap(updated); setSelected(body.name); }
    } catch (err) { onError(msg(err)); }
    finally { setBusy(false); }
  }

  async function remove() {
    if (!selected || isPublic || !confirm(`Delete group "${selected}"? Members keep their other groups.`)) return;
    setBusy(true);
    onError(null);
    try {
      const updated = await apiJSON<AccessSnapshot>(
        `/api/v1/admin/groups/${encodeURIComponent(selected)}`, { method: 'DELETE' });
      if (updated) onSnap(updated);
      setSelected(null);
    } catch (err) { onError(msg(err)); }
    finally { setBusy(false); }
  }

  return (
    <div class="cl-body">
      <div class="cl-list">
        <div class="cl-list-head">
          <input class="audit-filter" type="text" placeholder="New group…" value={newGroup}
            onInput={(e) => setNewGroup((e.target as HTMLInputElement).value)}
            onKeyDown={(e) => { if (e.key === 'Enter') create(); }} />
          <button class="panel-btn" disabled={busy} onClick={create}>Create</button>
        </div>
        {ordered.map(({ group, depth }) => (
          <div key={group.name} class={`cl-list-item${selected === group.name ? ' cl-list-item-active' : ''}`} onClick={() => select(group.name)}>
            <span class="cl-list-name" title={group.name} style={{ paddingLeft: `${depth * 0.9}rem` }}>
              {depth > 0 && <span class="admin-tree-mark">└ </span>}
              {group.name}
            </span>
            <span class="hint">{group.members.length}</span>
          </div>
        ))}
      </div>

      <div class="cl-editor-wrap">
        {!selected && <div class="hint admin-empty">Select a group, or create one, to manage its parent and member roles.</div>}
        {selected && (
          <div class="admin-editor">
            <label class="admin-field-label">Name</label>
            <input class="admin-input" type="text" value={name} disabled={isPublic}
              onInput={(e) => setName((e.target as HTMLInputElement).value)} />
            {isPublic && <p class="hint">The built-in public group cannot be renamed, reparented or deleted; every user is an implicit viewer member.</p>}

            <label class="admin-field-label">Parent group (nesting)</label>
            <select class="prop-select admin-input" value={parent} disabled={isPublic}
              onChange={(e) => setParent((e.target as HTMLSelectElement).value)}>
              <option value="">— none (top level) —</option>
              {parentOptions.map(p => <option key={p} value={p}>{p}</option>)}
            </select>
            {!isPublic && <p class="hint">Members of a parent group inherit their role on this group too, unless overridden here.</p>}

            <label class="admin-field-label">Members</label>
            <div class="admin-member-add">
              <input class="admin-input" type="text" placeholder="Add member…" value={newMember}
                onInput={(e) => setNewMember((e.target as HTMLInputElement).value)}
                onKeyDown={(e) => { if (e.key === 'Enter') addMember(); }} />
              <button class="panel-btn" onClick={addMember}>Add</button>
            </div>
            {members.length === 0 && <div class="hint">No explicit members.</div>}
            <div class="admin-role-table">
              {members.map(m => (
                <div key={m.user} class="admin-role-row">
                  <span class="admin-role-group">{m.user}</span>
                  <select class="prop-select admin-role-select" value={m.role}
                    onChange={(e) => setMemberRole(m.user, (e.target as HTMLSelectElement).value)}>
                    {snap.roles.map(r => <option key={r} value={r}>{roleLabel(r)}</option>)}
                  </select>
                  <button class="admin-row-remove" title="Remove member" onClick={() => removeMember(m.user)}>✕</button>
                </div>
              ))}
            </div>

            <div class="admin-editor-actions">
              <button class="panel-btn" disabled={busy} onClick={save}>{busy ? 'Saving…' : 'Save'}</button>
              {!isPublic && <button class="panel-btn" disabled={busy} onClick={remove}>Delete</button>}
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// ── Server stats ─────────────────────────────────────────────────────────────

function fmtUptime(s: number): string {
  s = Math.floor(s);
  const d = Math.floor(s / 86400); s -= d * 86400;
  const h = Math.floor(s / 3600); s -= h * 3600;
  const m = Math.floor(s / 60); s -= m * 60;
  const parts: string[] = [];
  if (d) parts.push(`${d}d`);
  if (h || d) parts.push(`${h}h`);
  parts.push(`${m}m`, `${s}s`);
  return parts.join(' ');
}

const fmtMB = (b: number): string => `${(b / (1024 * 1024)).toFixed(1)} MB`;

function StatsView({ onError }: { onError: (e: string | null) => void }) {
  const [stats, setStats] = useState<ServerStats | null>(null);

  useEffect(() => {
    let live = true;
    const load = async () => {
      try {
        const s = await apiJSON<ServerStats>('/api/v1/admin/stats');
        if (live && s) setStats(s);
      } catch (err) { if (live) onError(msg(err)); }
    };
    load();
    const t = setInterval(load, 2000);
    return () => { live = false; clearInterval(t); };
  }, [onError]);

  if (!stats) return <div class="cl-body"><div class="hint admin-empty">Loading…</div></div>;

  const rows: [string, string][] = [
    ['Uptime', fmtUptime(stats.uptimeSeconds)],
    ['WebSocket connections', String(stats.wsConnections)],
    ['Observed signals', String(stats.observedSignals)],
    ['Messages in', String(stats.msgIn)],
    ['Messages out', String(stats.msgOut)],
    ['Signal writes', String(stats.writeOps)],
    ['History requests', String(stats.historyReqs)],
    ['Goroutines', String(stats.goroutines)],
    ['Memory allocated', fmtMB(stats.memAllocBytes)],
    ['Memory from OS', fmtMB(stats.memSysBytes)],
    ['Heap in use', fmtMB(stats.heapInuseBytes)],
    ['Load average', stats.loadAvg ? stats.loadAvg.map(n => n.toFixed(2)).join(' / ') : '—'],
  ];

  return (
    <div class="cl-body">
      <div class="admin-stats">
        <table class="audit-table admin-stats-table">
          <tbody>
            {rows.map(([k, v]) => (
              <tr key={k}><th class="admin-stat-key">{k}</th><td class="admin-stat-val">{v}</td></tr>
            ))}
          </tbody>
        </table>
        <div class="admin-stats-ds">
          <h4 class="admin-field-label">Data sources ({stats.dataSources.length})</h4>
          <div class="admin-ds-list">
            {stats.dataSources.map(d => <span key={d} class="admin-ds-tag">{d}</span>)}
          </div>
        </div>
      </div>
    </div>
  );
}
