/* global React, Icon, IconBtn, useLiveQuery, copyText, db */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ============================================================
// 2FA codes — TOTP (RFC 6238) generator + key vault.
// Paste 2FA secret keys to get live, auto-rotating 6-digit
// codes, and save keys to a local vault so you never have to
// paste them again. Everything is computed in your browser
// with Web Crypto; nothing is sent anywhere.
// ============================================================

const B32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';

function normalizeSecret(s) {
  return (s || '').replace(/[\s-]/g, '').replace(/=+$/, '').toUpperCase();
}

// Base32 (RFC 4648) → Uint8Array. Returns null on invalid input.
function base32Decode(input) {
  const clean = normalizeSecret(input);
  if (!clean) return null;
  let bits = '';
  for (const ch of clean) {
    const idx = B32_ALPHABET.indexOf(ch);
    if (idx === -1) return null;
    bits += idx.toString(2).padStart(5, '0');
  }
  const bytes = [];
  for (let i = 0; i + 8 <= bits.length; i += 8) {
    bytes.push(parseInt(bits.slice(i, i + 8), 2));
  }
  if (!bytes.length) return null;
  return new Uint8Array(bytes);
}

// Compute a TOTP code for a given secret + time.
async function totp(secretBytes, { period = 30, digits = 6, t = Date.now() } = {}) {
  const counter = Math.floor(t / 1000 / period);
  const counterBytes = new Uint8Array(8);
  let c = counter;
  for (let i = 7; i >= 0; i--) { counterBytes[i] = c & 0xff; c = Math.floor(c / 256); }
  const key = await crypto.subtle.importKey(
    'raw', secretBytes, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']
  );
  const sig = new Uint8Array(await crypto.subtle.sign('HMAC', key, counterBytes));
  const offset = sig[sig.length - 1] & 0x0f;
  const bin =
    ((sig[offset] & 0x7f) << 24) |
    ((sig[offset + 1] & 0xff) << 16) |
    ((sig[offset + 2] & 0xff) << 8) |
    (sig[offset + 3] & 0xff);
  return (bin % 10 ** digits).toString().padStart(digits, '0');
}

// Parse a single input line into a code entry.
// Supports: raw base32 secret · "ID|Pass|2FA" pipe rows · otpauth:// URIs.
function parseLine(line) {
  const trimmed = (line || '').trim();
  if (!trimmed) return null;

  if (/^otpauth:\/\//i.test(trimmed)) {
    try {
      const url = new URL(trimmed);
      const secret = url.searchParams.get('secret') || '';
      const label = decodeURIComponent(url.pathname.replace(/^\/+/, '')) || 'otpauth';
      return {
        label,
        secret,
        period: parseInt(url.searchParams.get('period'), 10) || 30,
        digits: parseInt(url.searchParams.get('digits'), 10) || 6,
        raw: trimmed,
      };
    } catch (e) { /* fall through */ }
  }

  if (trimmed.includes('|')) {
    const parts = trimmed.split('|').map(s => s.trim()).filter(Boolean);
    return {
      label: parts[0] || '',
      secret: parts[parts.length - 1] || '',
      period: 30, digits: 6, raw: trimmed,
    };
  }

  return { label: '', secret: trimmed, period: 30, digits: 6, raw: trimmed };
}

function maskSecret(s) {
  const clean = normalizeSecret(s);
  if (clean.length <= 4) return clean;
  return clean.slice(0, 4) + '·'.repeat(Math.min(8, clean.length - 4));
}

const SAMPLE = `account@example.com|hunter2|JBSWY3DPEHPK3PXP
GitHub|-|NB2W45DFOIZA
otpauth://totp/Demo:me@site.io?secret=KVKFKRCPNZQUYMLXOVYDSQQ&issuer=Demo`;

// ============================================================
// Hook: keep a map of live TOTP codes for a list of entries.
// Each entry needs { key, secret, period, digits }. Recomputes
// only when a time window rolls over (or the entry set changes).
// ============================================================
function useLiveCodes(entries, nowSec) {
  const [codes, setCodes] = useState({});
  const windowKey = entries
    .map(e => e.key + ':' + normalizeSecret(e.secret) + ':' + Math.floor(nowSec / (e.period || 30)))
    .join('|');
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const next = {};
      for (const e of entries) {
        const bytes = base32Decode(e.secret);
        if (!bytes) { next[e.key] = { error: 'Invalid 2FA key' }; continue; }
        try {
          next[e.key] = { code: await totp(bytes, { period: e.period, digits: e.digits }) };
        } catch (err) {
          next[e.key] = { error: 'Could not generate' };
        }
      }
      if (!cancelled) setCodes(next);
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line
  }, [windowKey]);
  return codes;
}

// ============================================================
// Main tool
// ============================================================
function TotpTool() {
  const [input, setInput] = useState('');
  const [generated, setGenerated] = useState([]);   // committed parsed lines
  const [nowSec, setNowSec] = useState(Math.floor(Date.now() / 1000));
  const [copiedKey, setCopiedKey] = useState(null);

  const vault = useLiveQuery('totpKeys', { sort: ['createdAt', 'asc'] });
  const vaultSecrets = useMemo(
    () => new Set((vault || []).map(v => normalizeSecret(v.secret))),
    [vault]
  );

  // Draft input is intentionally NOT persisted — pasted keys live only in
  // memory for this session. Only keys explicitly saved to the vault persist.
  // Clear any draft left over from older versions that did persist it.
  useEffect(() => {
    db.kv.delete && db.kv.delete('totpInput');
  }, []);

  // 1-second clock
  useEffect(() => {
    const id = setInterval(() => setNowSec(Math.floor(Date.now() / 1000)), 1000);
    return () => clearInterval(id);
  }, []);

  function parseAll(text) {
    return (text || '').split('\n').map(parseLine).filter(Boolean);
  }

  // Entries with stable keys for the code hook
  const vaultEntries = useMemo(
    () => (vault || []).map(v => ({ ...v, key: 'v_' + v.id })),
    [vault]
  );
  const genEntries = useMemo(
    () => generated.map((e, i) => ({ ...e, key: 'g_' + i })),
    [generated]
  );
  const vaultCodes = useLiveCodes(vaultEntries, nowSec);
  const genCodes = useLiveCodes(genEntries, nowSec);

  const generate = () => setGenerated(parseAll(input));

  const copy = async (key, code) => {
    const ok = await copyText(code);
    if (ok) { setCopiedKey(key); setTimeout(() => setCopiedKey(null), 1400); }
  };

  const saveToVault = (e) => {
    const secret = normalizeSecret(e.secret);
    if (!secret || vaultSecrets.has(secret)) return;
    db.totpKeys.put({
      label: e.label || '',
      secret: e.secret.trim(),
      period: e.period || 30,
      digits: e.digits || 6,
    });
  };

  const saveAll = () => {
    genEntries.forEach(e => {
      if (!genCodes[e.key]?.error) saveToVault(e);
    });
  };

  const renameVault = async (item) => {
    const name = await window.lhDialog.prompt({
      title: 'Rename key',
      message: 'A label helps you recognize this account.',
      placeholder: 'e.g. GitHub · work email',
      initial: item.label || '',
      icon: 'edit',
      confirmLabel: 'Save',
    });
    if (name != null) db.totpKeys.put({ ...item, label: name.trim() });
  };

  const deleteVault = async (item) => {
    const ok = await window.lhDialog.confirm({
      title: 'Remove this key?',
      message: 'The saved 2FA key will be deleted from this browser. You can re-add it later if you still have the key.',
      confirmLabel: 'Remove',
      danger: true,
      icon: 'delete',
    });
    if (ok) db.totpKeys.delete(item.id);
  };

  const lineCount = input.split('\n').filter(l => l.trim()).length;
  const savableCount = genEntries.filter(
    e => !genCodes[e.key]?.error && !vaultSecrets.has(normalizeSecret(e.secret))
  ).length;

  return (
    <div className="totp-tool">
      <header className="totp-head">
        <div>
          <h1>2FA codes</h1>
          <p>Turn 2FA secret keys into live, auto-rotating one-time codes — and save them to a local vault so you never paste a key twice. Everything stays in this browser.</p>
        </div>
      </header>

      {/* Saved keys / vault */}
      <section className="totp-results">
        <div className="totp-results-head">
          <Icon name="shield_lock" className="totp-sec-icon" />
          <h2>Saved keys</h2>
          {vaultEntries.length > 0 && <span className="totp-count">{vaultEntries.length}</span>}
        </div>

        {vault == null ? null : vaultEntries.length === 0 ? (
          <div className="lh-empty totp-empty">
            <Icon name="shield_lock" />
            <div className="title">Your vault is empty</div>
            <div className="sub">Generate a code below, then hit <strong>Save</strong> to keep the key here for next time.</div>
          </div>
        ) : (
          <div className="totp-grid">
            {vaultEntries.map(e => {
              const period = e.period || 30;
              const remaining = period - (nowSec % period);
              return (
                <CodeCard
                  key={e.key}
                  entry={e}
                  result={vaultCodes[e.key] || {}}
                  remaining={remaining}
                  period={period}
                  copied={copiedKey === e.key}
                  onCopy={() => vaultCodes[e.key]?.code && copy(e.key, vaultCodes[e.key].code)}
                  onRename={() => renameVault(e)}
                  onDelete={() => deleteVault(e)}
                  saved
                />
              );
            })}
          </div>
        )}
      </section>

      {/* Input card */}
      <section className="totp-card totp-input-card">
        <div className="totp-card-title">
          <Icon name="vpn_key" />
          <span>Add keys</span>
          <div style={{ flex: 1 }} />
          <button className="lh-chip" onClick={() => { setInput(SAMPLE); setGenerated(parseAll(SAMPLE)); }}>
            <Icon name="science" />Sample
          </button>
          {input && (
            <button className="lh-chip" onClick={() => { setInput(''); setGenerated([]); }}>
              <Icon name="close" />Clear
            </button>
          )}
        </div>
        <textarea
          className="totp-input"
          placeholder={"Paste a base32 secret, an otpauth:// link, or a full account row.\nOne entry per line — each line produces one code."}
          value={input}
          onChange={e => setInput(e.target.value)}
          spellCheck={false}
        />
        <div className="totp-input-foot">
          <span className="totp-hint">
            Accepts a bare key, an <code>otpauth://</code> URI, or <code>id&nbsp;|&nbsp;pass&nbsp;|&nbsp;2fa</code> rows (the last field is the key). One code per line.
          </span>
          <button className="lh-btn primary" onClick={generate} disabled={!lineCount}>
            <Icon name="shield" />Get codes
          </button>
        </div>
      </section>

      {/* Generated (not-yet-saved) codes */}
      {genEntries.length > 0 && (
        <section className="totp-results">
          <div className="totp-results-head">
            <Icon name="bolt" className="totp-sec-icon" />
            <h2>Generated</h2>
            <span className="totp-count">{genEntries.length}</span>
            <div style={{ flex: 1 }} />
            {savableCount > 0 && (
              <button className="lh-btn ghost totp-saveall" onClick={saveAll}>
                <Icon name="bookmark_add" />Save all{savableCount > 1 ? ` (${savableCount})` : ''}
              </button>
            )}
          </div>
          <div className="totp-grid">
            {genEntries.map(e => {
              const period = e.period || 30;
              const remaining = period - (nowSec % period);
              const alreadySaved = vaultSecrets.has(normalizeSecret(e.secret));
              return (
                <CodeCard
                  key={e.key}
                  entry={e}
                  result={genCodes[e.key] || {}}
                  remaining={remaining}
                  period={period}
                  copied={copiedKey === e.key}
                  onCopy={() => genCodes[e.key]?.code && copy(e.key, genCodes[e.key].code)}
                  onSave={genCodes[e.key]?.error ? null : () => saveToVault(e)}
                  saved={alreadySaved}
                />
              );
            })}
          </div>
        </section>
      )}
    </div>
  );
}

// ============================================================
// One code card with a countdown ring
// ============================================================
function CodeCard({ entry, result, remaining, period, copied, onCopy, onSave, onRename, onDelete, saved }) {
  const R = 13;
  const CIRC = 2 * Math.PI * R;
  const frac = remaining / period;
  const urgent = remaining <= 5;
  const label = entry.label || maskSecret(entry.secret) || 'Unnamed key';

  // Hide the code by default; reveal only briefly on demand.
  const REVEAL_MS = 30000;
  const [revealed, setRevealed] = useState(false);
  const revealTimer = useRef(null);
  useEffect(() => () => revealTimer.current && clearTimeout(revealTimer.current), []);
  const reveal = () => {
    setRevealed(true);
    if (revealTimer.current) clearTimeout(revealTimer.current);
    revealTimer.current = setTimeout(() => setRevealed(false), REVEAL_MS);
  };
  const hide = () => {
    setRevealed(false);
    if (revealTimer.current) clearTimeout(revealTimer.current);
  };

  const digits = result.code
    ? (result.code.length === 6
        ? result.code.slice(0, 3) + ' ' + result.code.slice(3)
        : result.code)
    : null;
  const masked = result.code
    ? (result.code.length === 6 ? '••• •••' : '•'.repeat(result.code.length))
    : null;
  const display = revealed ? digits : masked;

  return (
    <div className={`totp-code-card ${result.error ? 'is-error' : ''} ${urgent && !result.error ? 'is-urgent' : ''}`}>
      <div className="totp-code-top">
        <div className="totp-code-info">
          <div className="totp-code-label" title={label}>{label}</div>
          <div className="totp-code-key" title={entry.raw || entry.secret}>
            <Icon name="vpn_key" />{maskSecret(entry.secret)}
          </div>
        </div>
        <div className="totp-code-actions">
          {!result.error && (
            <IconBtn
              name={revealed ? 'visibility_off' : 'visibility'}
              title={revealed ? 'Hide code' : 'Reveal for 30s'}
              onClick={revealed ? hide : reveal}
            />
          )}
          {onSave && !saved && (
            <IconBtn name="bookmark_add" title="Save to vault" onClick={onSave} />
          )}
          {saved && onSave && (
            <span className="totp-saved-tag" title="In your vault"><Icon name="check_circle" />Saved</span>
          )}
          {onRename && <IconBtn name="edit" title="Rename" onClick={onRename} />}
          {onDelete && <IconBtn name="delete" tone="danger" title="Remove" onClick={onDelete} />}
        </div>
      </div>

      <div className="totp-code-bottom">
        {result.error ? (
          <div className="totp-code-err">
            <Icon name="error" />{result.error}
          </div>
        ) : (
          <button className="totp-code-value" onClick={onCopy} title="Click to copy">
            <span className={`totp-code-digits ${revealed ? '' : 'is-hidden'}`}>{display || '— — —'}</span>
            <span className="totp-code-copy">
              <Icon name={copied ? 'check' : 'content_copy'} />
              {copied ? 'Copied' : 'Copy'}
            </span>
          </button>
        )}

        {!result.error && (
          <div className={`totp-ring ${urgent ? 'is-urgent' : ''}`} title={`${remaining}s left`}>
            <svg viewBox="0 0 32 32" width="32" height="32">
              <circle className="totp-ring-track" cx="16" cy="16" r={R} />
              <circle
                className="totp-ring-fill"
                cx="16" cy="16" r={R}
                strokeDasharray={CIRC}
                strokeDashoffset={CIRC * (1 - frac)}
                transform="rotate(-90 16 16)"
              />
            </svg>
            <span className="totp-ring-num">{remaining}</span>
          </div>
        )}
      </div>
    </div>
  );
}

window.TotpTool = TotpTool;
