/* ui.jsx — shared atoms */
const { useState, useEffect, useRef, useCallback } = React;

/* ---- small helpers ---- */
function cx(...a){ return a.filter(Boolean).join(' '); }
function fmtBytes(b){ if(b<1024) return b+' B'; if(b<1048576) return (b/1024).toFixed(b<10240?1:0)+' KB'; return (b/1048576).toFixed(1)+' MB'; }

const STATUS_META = {
  published:  { label:'Published',  dot:'var(--text-3)',  text:'ink3' },
  new:        { label:'New',        dot:'var(--success)', text:'success' },
  modified:   { label:'Modified',   dot:'var(--accent)',  text:'accent' },
  renamed:    { label:'Renamed',    dot:'var(--accent)',  text:'accent' },
  deprecated: { label:'Deprecated', dot:'var(--text-3)',  text:'ink3' },
};
const ACTION_META = {
  added:      { label:'Added',      color:'var(--success)' },
  modified:   { label:'Modified',   color:'var(--accent)' },
  renamed:    { label:'Renamed',    color:'var(--accent)' },
  removed:    { label:'Removed',    color:'var(--danger)' },
  deprecated: { label:'Deprecated', color:'var(--text-2)' },
};

/* ---- Button ---- */
function Button({ variant='secondary', size='md', icon, iconRight, children, className='', ...rest }) {
  const base = 'inline-flex items-center justify-center font-medium select-none transition-all duration-150 focus-ring disabled:opacity-40 disabled:pointer-events-none whitespace-nowrap';
  const sizes = { sm:'h-7 px-2.5 text-[12px] gap-1.5 rounded-[7px]', md:'h-8 px-3 text-[13px] gap-1.5 rounded-[8px]', lg:'h-9 px-4 text-[13px] gap-2 rounded-[8px]' };
  const variants = {
    primary:  'bg-accent text-onAccent hover:brightness-110 active:brightness-95',
    secondary:'bg-panel text-ink border border-line hover:bg-hover',
    ghost:    'text-ink2 hover:text-ink hover:bg-hover',
    subtle:   'bg-hover text-ink hover:brightness-95',
    danger:   'text-danger hover:bg-[color:var(--danger)]/10 border border-transparent hover:border-[color:var(--danger)]/30',
  };
  return (
    <button className={cx(base, sizes[size], variants[variant], className)} {...rest}>
      {icon && <Icon name={icon} size={size==='sm'?13:15} />}
      {children}
      {iconRight && <Icon name={iconRight} size={size==='sm'?13:15} />}
    </button>
  );
}

/* ---- Icon button ---- */
function IconButton({ name, size=16, title, active, className='', ...rest }) {
  return (
    <button title={title} aria-label={title}
      className={cx('inline-flex items-center justify-center rounded-[7px] transition-colors duration-150 focus-ring',
        'w-7 h-7', active ? 'bg-accentWeak text-accent' : 'text-ink2 hover:text-ink hover:bg-hover', className)} {...rest}>
      <Icon name={name} size={size} />
    </button>
  );
}

/* ---- StatusDot + StatusTag ---- */
function StatusDot({ status, className='' }) {
  const m = STATUS_META[status] || STATUS_META.published;
  if (status === 'published') return null;
  return <span className={cx('inline-block w-1.5 h-1.5 rounded-full shrink-0', className)} style={{ background:m.dot }} />;
}
function StatusTag({ status }) {
  const m = STATUS_META[status] || STATUS_META.published;
  if (status === 'published') return null;
  const deprecated = status === 'deprecated';
  return (
    <span className={cx('inline-flex items-center gap-1 h-[18px] px-1.5 rounded-full text-[10.5px] font-medium tracking-wide uppercase',
      deprecated ? 'bg-hover text-ink3' : '')}
      style={ deprecated ? {} : { background:'color-mix(in srgb, '+m.dot+' 13%, transparent)', color:m.dot }}>
      {!deprecated && <span className="w-1 h-1 rounded-full" style={{ background:m.dot }} />}
      {m.label}
    </span>
  );
}

/* ---- Badge (count) ---- */
function CountBadge({ n, tone='accent' }) {
  if (!n) return null;
  const bg = tone==='accent' ? 'var(--accent)' : 'var(--text-2)';
  return <span className="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full text-[11px] font-semibold text-onAccent" style={{ background:bg }}>{n}</span>;
}

/* ---- Search input ---- */
function SearchInput({ value, onChange, placeholder='Search', className='', size='md' }) {
  const h = size==='sm' ? 'h-7' : 'h-8';
  return (
    <div className={cx('relative flex items-center', className)}>
      <Icon name="search" size={14} className="absolute left-2.5 text-ink3 pointer-events-none" />
      <input value={value} onChange={e=>onChange(e.target.value)} placeholder={placeholder}
        className={cx('w-full bg-sunken border border-line rounded-[8px] pl-8 pr-7 text-[13px] text-ink focus-ring focus:border-line2', h)} />
      {value && <button onClick={()=>onChange('')} className="absolute right-2 text-ink3 hover:text-ink"><Icon name="close" size={13} /></button>}
    </div>
  );
}

/* ---- Segmented control ---- */
function Segmented({ options, value, onChange, size='md' }) {
  const h = size==='sm' ? 'h-7' : 'h-8';
  return (
    <div className={cx('inline-flex items-center p-0.5 bg-sunken border border-line rounded-[8px] gap-0.5', h)}>
      {options.map(o => {
        const active = o.value === value;
        return (
          <button key={o.value} onClick={()=>onChange(o.value)}
            className={cx('inline-flex items-center gap-1.5 h-full px-2.5 rounded-[7px] text-[12.5px] font-medium transition-all duration-150',
              active ? 'bg-panel text-ink shadow-[0_1px_2px_rgba(0,0,0,0.06)]' : 'text-ink2 hover:text-ink')}>
            {o.icon && <Icon name={o.icon} size={13} />}{o.label}
          </button>
        );
      })}
    </div>
  );
}

/* ---- Field shell ---- */
function Field({ label, hint, children, error }) {
  return (
    <div className="flex flex-col gap-1.5">
      {label && <label className="text-[12px] font-medium text-ink2">{label}</label>}
      {children}
      {error ? <p className="text-[11.5px] text-danger flex items-center gap-1"><Icon name="warningTri" size={12} />{error}</p>
        : hint ? <p className="text-[11.5px] text-ink3">{hint}</p> : null}
    </div>
  );
}
function TextInput({ mono, className='', ...rest }) {
  return <input className={cx('w-full h-8 bg-sunken border border-line rounded-[8px] px-2.5 text-[13px] text-ink focus-ring focus:border-line2', mono&&'font-mono', className)} {...rest} />;
}
function TextArea({ rows=3, className='', ...rest }) {
  return <textarea rows={rows} className={cx('w-full bg-sunken border border-line rounded-[8px] px-2.5 py-2 text-[13px] text-ink leading-relaxed focus-ring focus:border-line2 resize-none', className)} {...rest} />;
}

/* ---- TagInput — shared inline chip editor (tags / keywords); NO prompt() ---- */
function TagInput({ value=[], onChange, placeholder, disabled, mono }) {
  const [draft, setDraft] = useState('');
  const add = () => {
    const v = draft.trim();
    if (v && !value.includes(v)) onChange([...value, v]);
    setDraft('');
  };
  const remove = (t) => onChange(value.filter(x => x !== t));
  return (
    <div className="flex flex-wrap items-center gap-1.5">
      {value.map(t => (
        <span key={t} className={cx('inline-flex items-center gap-1 h-[22px] pl-2 pr-1 rounded-[4px] bg-sunken border border-line text-[12px] text-ink2', mono&&'font-mono')}>
          {t}
          {!disabled && <button onClick={()=>remove(t)} className="text-ink3 hover:text-danger"><Icon name="close" size={11}/></button>}
        </span>
      ))}
      {!disabled && (
        <span className="inline-flex items-center gap-1.5">
          <TextInput value={draft} placeholder={placeholder || tr('Add…','添加…')} mono={mono}
            onChange={e=>setDraft(e.target.value)}
            onKeyDown={e=>{ if(e.key==='Enter'){ e.preventDefault(); add(); } }}
            className="h-[22px] w-32 text-[12px]" />
          <IconButton name="plus" title={placeholder || tr('Add…','添加…')} onClick={add} />
        </span>
      )}
    </div>
  );
}

/* ---- Drawer shell (right slide-in, faint scrim) ---- */
function Drawer({ open, onClose, width=460, children }) {
  useEffect(() => {
    function onKey(e){ if(e.key==='Escape') onClose(); }
    if (open) window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [open, onClose]);
  if (!open) return null;
  return (
    <div className="fixed inset-0 z-40" style={{ animation:'fadeIn 140ms ease' }}>
      <div className="absolute inset-0" style={{ background:'rgba(0,0,0,0.16)' }} onClick={onClose} />
      <div className="absolute top-0 right-0 h-full bg-panel border-l border-line flex flex-col ds-scroll"
        style={{ width, boxShadow:'-1px 0 0 var(--border), var(--shadow-overlay,-12px 0 32px rgba(0,0,0,0.12))', animation:'drawerIn 190ms cubic-bezier(0.22,1,0.36,1)' }}>
        {children}
      </div>
    </div>
  );
}

/* ---- Reference chip (mono alias) ---- */
function RefChip({ id, onClick }) {
  return (
    <button onClick={onClick} className={cx('inline-flex items-center gap-1 h-[20px] px-1.5 rounded-[4px] font-mono text-[11.5px] text-accent bg-accentWeak hover:brightness-95 transition', onClick?'cursor-pointer':'cursor-default')}>
      <Icon name="link" size={11} />{'{'+id+'}'}
    </button>
  );
}

/* ---- Reference chain (a → {b} → {c} → value) ---- */
function RefChain({ chain, mono=true, size=11.5 }) {
  if (!chain || chain.length <= 1) return null;
  const items = chain.slice(1); // drop self
  return (
    <span className="inline-flex items-center gap-1 flex-wrap text-ink3" style={{ fontSize:size }}>
      {items.map((c, i) => (
        <span key={c.id} className="inline-flex items-center gap-1">
          {i>0 && <Icon name="chevronR" size={10} className="text-ink3 opacity-60" />}
          <span className={cx(mono&&'font-mono', 'text-ink3')}>{'{'+c.id+'}'}</span>
        </span>
      ))}
    </span>
  );
}

/* ---- Empty state ---- */
function EmptyState({ icon='layers', title, body, action }) {
  return (
    <div className="flex flex-col items-center justify-center text-center py-16 px-6" style={{ animation:'fadeIn 200ms ease' }}>
      <div className="w-12 h-12 rounded-full bg-sunken border border-line flex items-center justify-center text-ink3 mb-4">
        <Icon name={icon} size={20} />
      </div>
      <h3 className="text-[15px] font-semibold text-ink">{title}</h3>
      {body && <p className="text-[13px] text-ink2 mt-1 max-w-[320px]">{body}</p>}
      {action && <div className="mt-4">{action}</div>}
    </div>
  );
}

/* ---- Tiny inline confirm (danger second-step) ---- */
function ConfirmInline({ open, label='Confirm?', onConfirm, onCancel }) {
  if (!open) return null;
  return (
    <div className="flex items-center gap-1.5" style={{ animation:'fadeIn 120ms ease' }}>
      <span className="text-[12px] text-ink2">{label}</span>
      <Button size="sm" variant="danger" onClick={onConfirm}>Yes</Button>
      <Button size="sm" variant="ghost" onClick={onCancel}>No</Button>
    </div>
  );
}

/* ---- Visual previews per token kind ---- */
function Swatch({ color, size=22, radius=6 }) {
  return <span className="inline-block checker shrink-0 border border-line2" style={{ width:size, height:size, borderRadius:radius }}>
    <span className="block w-full h-full" style={{ background:color, borderRadius:radius-1 }} />
  </span>;
}
function KindPreview({ token, ctx, size=24 }) {
  const r = resolve(token.id, ctx);
  if (token.kind === 'color') return <Swatch color={r.value} size={size} />;
  if (token.kind === 'spacing') {
    const w = Math.max(2, Math.min(r.value, 48));
    return <span className="inline-flex items-center" style={{ width:48, height:size }}>
      <span className="h-2.5 rounded-[2px] bg-accent" style={{ width:w }} /></span>;
  }
  if (token.kind === 'radius') return <span className="inline-block bg-sunken border-2 border-ink2" style={{ width:size, height:size, borderTopLeftRadius:r.value, borderBottomRightRadius:r.value }} />;
  if (token.kind === 'shadow') return <span className="inline-flex items-center justify-center" style={{ width:48, height:size }}><span className="bg-panel border border-line" style={{ width:size-2, height:size-2, borderRadius:5, boxShadow:r.value }} /></span>;
  if (token.kind === 'type') return <span className="inline-flex items-end justify-center text-ink font-semibold leading-none" style={{ width:34, height:size, fontSize:Math.min(r.value,22) }}>Aa</span>;
  return null;
}

/* ---- unified chip atom (category selector + filter dropdown share this) ---- */
function FilterChip({ selected, onClick, dense, children }) {
  return (
    <button onClick={onClick}
      className={cx('inline-flex items-center rounded-[7px] font-medium transition whitespace-nowrap',
        dense ? 'h-6 px-2 text-[12px]' : 'h-7 px-2.5 text-[12.5px]',
        selected ? 'bg-accentWeak text-accent' : 'text-ink2 hover:text-ink hover:bg-hover')}>
      {children}
    </button>
  );
}

/* ---- flat category selector (<=~10 options); implicit leading "All" chip ---- */
function CategoryChips({ value, onChange, options, allLabel }) {
  const items = [{ value:'all', label: allLabel || tr('All','全部') }].concat(options || []);
  return (
    <div className="inline-flex items-center h-7 p-0.5 bg-sunken border border-line rounded-[8px] gap-0.5">
      {items.map(o => {
        const on = (value||'all')===o.value;
        return (
          <button key={o.value} onClick={()=>onChange(o.value)}
            className={cx('inline-flex items-center h-full px-2.5 rounded-[7px] text-[12.5px] font-medium transition whitespace-nowrap',
              on ? 'bg-panel text-ink shadow-[0_1px_2px_rgba(0,0,0,0.06)]' : 'text-ink2 hover:text-ink')}>
            {o.label}{o.count!=null && <span className="ml-1 text-ink3 tabular-nums">{o.count}</span>}
          </button>
        );
      })}
    </div>
  );
}

/* ---- shared multi-facet filter dropdown (replaces every per-page filter menu) ---- */
function FilterMenu({ groups, value, onChange, width=256 }) {
  const [open, setOpen] = React.useState(false);
  const gs = groups || [];
  const activeKeys = gs.filter(g => value[g.key] && value[g.key] !== 'all');
  const reset = () => onChange(Object.fromEntries(gs.map(g => [g.key,'all'])));
  return (
    <div className="relative">
      <Button variant="secondary" size="sm" icon="filter" onClick={()=>setOpen(o=>!o)} className={activeKeys.length ? 'border-accent text-accent' : ''}>
        {tr('Filter','筛选')}{activeKeys.length>0 && <CountBadge n={activeKeys.length} tone="accent" />}
      </Button>
      {open && (<>
        <div className="fixed inset-0 z-10" onClick={()=>setOpen(false)} />
        <div className="absolute right-0 top-9 z-20 bg-panel border border-line rounded-[12px] p-3 shadow-[0_8px_28px_rgba(0,0,0,0.14)] flex flex-col gap-3" style={{ width, animation:'fadeIn 120ms ease' }}>
          {gs.map(g => (
            <div key={g.key}>
              <div className="text-[10.5px] uppercase tracking-wide text-ink3 mb-1.5">{g.label}</div>
              <div className="flex flex-wrap gap-1">
                {g.opts.map(([v,l]) => (
                  <FilterChip key={v} dense selected={(value[g.key]||'all')===v} onClick={()=>onChange({ ...value, [g.key]: v })}>{l}</FilterChip>
                ))}
              </div>
            </div>
          ))}
          {activeKeys.length>0 && <button onClick={reset} className="text-[12px] text-accent text-left hover:underline">{tr('Clear all','清除全部')}</button>}
        </div>
      </>)}
    </div>
  );
}

/* ---- canonical page header/toolbar — enforces placement across every admin module ----
   Row 1: title + count (left) · contextHint / secondary / primary action (right)
   Row 2: search (fixed 280) → categories → facets ……… ml-auto ……… filter dropdown */
function PageToolbar({ title, subtitle, titleMeta, count, contextHint, primaryAction, secondaryActions, leadingActions, search, categories, facets, filters }) {
  const hasRow2 = leadingActions || search || categories || facets || filters;
  return (
    <div className="shrink-0 border-b border-line px-6 pt-4 pb-3 bg-panel">
      <div className="flex items-center justify-between gap-3">
        <div className="flex items-baseline gap-2.5 min-w-0">
          <h1 className="text-[19px] font-semibold text-ink tracking-tight truncate shrink-0">{title}</h1>
          {count != null && <span className="text-[13px] text-ink3 tabular-nums shrink-0">{typeof count==='object'?count.n:count}</span>}
          {titleMeta && <span className="text-[13px] text-ink3 truncate min-w-0">{titleMeta}</span>}
        </div>
        <div className="flex items-center gap-2 shrink-0">
          {contextHint}
          {secondaryActions}
          {primaryAction}
        </div>
      </div>
      {subtitle && <p className="text-[13px] text-ink2 mt-1">{subtitle}</p>}
      {hasRow2 && (
        <div className="flex items-center gap-2 mt-3 flex-wrap">
          {leadingActions}
          {leadingActions && (search||filters||categories||facets) && <div className="w-px h-6 bg-line" />}
          {search && <SearchInput value={search.value} onChange={search.onChange} placeholder={search.placeholder} size="sm" className="w-[280px] shrink-0" />}
          {search && filters && <div className="w-px h-6 bg-line" />}
          {filters && <FilterMenu groups={filters.groups} value={filters.value} onChange={filters.onChange} />}
          {(search||filters) && categories && <div className="w-px h-6 bg-line" />}
          {categories && <CategoryChips value={categories.value} onChange={categories.onChange} options={categories.options} allLabel={categories.allLabel} />}
          {(search||filters||categories) && facets && <div className="w-px h-6 bg-line" />}
          {facets}
        </div>
      )}
    </div>
  );
}

/* ---- Markdown (component interaction notes) — render via marked, no editor ---- */
function renderMd(md) { if (typeof marked === 'undefined' || !md) return ''; try { return marked.parse(md, { breaks:true, gfm:true }); } catch (e) { return ''; } }
function MarkdownDoc({ md }) { if (!md) return null; return <div className="md-doc" dangerouslySetInnerHTML={{ __html: renderMd(md) }} />; }
function fileToDataUrl(file) { return new Promise((res) => { const r = new FileReader(); r.onload = () => res(r.result); r.onerror = () => res(''); r.readAsDataURL(file); }); }
/* Take a multi-file selection (a .md + its attachments) and inline every attachment as a data
   URL so the note is self-contained. Supports images/gif (inline), video (inline <video>), pdf
   (inline <iframe>), and office/other files (download chip). Handles Obsidian ![[file]] embeds
   and standard ![](file) / [text](file) links; unreferenced attachments append at the end. */
function fileKind(f) {
  const n = f.name.toLowerCase();
  if (/^image\//.test(f.type) || /\.(png|jpe?g|gif|svg|webp|avif)$/.test(n)) return 'image';
  if (/^video\//.test(f.type) || /\.(mp4|webm|mov|m4v|ogv)$/.test(n)) return 'video';
  if (/\.pdf$/.test(n)) return 'pdf';
  return 'file';
}
function mdEmbed(a) {
  a.used = true;
  if (a.kind === 'image') return `![${a.name}](${a.url})`;
  if (a.kind === 'video') return `\n<video controls preload="metadata" src="${a.url}"></video>\n`;
  if (a.kind === 'pdf') return `\n<iframe class="md-pdf" src="${a.url}"></iframe>\n`;
  return `<a class="md-file" href="${a.url}" download="${a.name}">${a.name}</a>`;
}
async function processMdFiles(fileList) {
  const files = [...(fileList || [])];
  const mdFile = files.find(f => /\.(md|markdown)$/i.test(f.name));
  if (!mdFile) return null;
  let md = await mdFile.text();
  const assets = {};
  // Object URLs (blob:) — unlike data: URLs they support range requests, so <video> playback
  // and <iframe> PDF preview work reliably. They live for the page session (content is in-memory).
  for (const f of files) { if (f === mdFile) continue; assets[f.name] = { name: f.name, kind: fileKind(f), url: URL.createObjectURL(f), used: false }; }
  const lookup = (raw) => { const key = decodeURIComponent(String(raw).split('/').pop().split('|')[0].trim()); return assets[key]; };
  md = md.replace(/!\[\[([^\]]+?)\]\]/g, (m, name) => { const a = lookup(name); return a ? mdEmbed(a) : m; });
  md = md.replace(/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/g, (m, alt, src) => { if (/^(https?:|data:)/.test(src)) return m; const a = lookup(src); return a ? mdEmbed(a) : m; });
  md = md.replace(/(^|[^!])\[([^\]]+)\]\(([^)\s]+)[^)]*\)/g, (m, pre, text, src) => { if (/^(https?:|data:|#)/.test(src)) return m; const a = lookup(src); return a ? pre + `<a class="md-file" href="${a.url}" download="${a.name}">${text}</a>` : m; });
  const extras = Object.values(assets).filter(a => !a.used);
  if (extras.length) md += '\n\n## ' + tr('Attachments', '附件') + '\n\n' + extras.map(a => mdEmbed(a)).join('\n\n');
  return md;
}

Object.assign(window, {
  cx, fmtBytes, STATUS_META, ACTION_META,
  Button, IconButton, StatusDot, StatusTag, CountBadge, SearchInput, Segmented,
  Field, TextInput, TextArea, TagInput, Drawer, RefChip, RefChain, EmptyState, ConfirmInline, Swatch, KindPreview,
  FilterChip, CategoryChips, FilterMenu, PageToolbar,
  renderMd, MarkdownDoc, processMdFiles,
});
