/// Totp standalone item type. Detail view shows the rotating code in a
/// signature block with a thin SVG countdown ring; form has a kind toggle
/// (TOTP vs Steam Guard) and a single secret input.

import { getState, setState, sendMessage, navigate, escapeHtml, popOutToTab } from '../../../shared/state';
import { renderFormHeader } from '../form-header';
import { REQUIRED_PILL_HTML } from '../../../shared/glyphs';
import type { Item, ItemId, ManifestEntry, Section, TotpKind, AttachmentRef } from '../../../shared/types';
import { base32Decode, base32Encode } from '../../../shared/base32';
import {
  renderRow, renderConcealedRow, renderSignatureBlock, wireFieldHandlers, renderSections,
  renderSectionsEditor, wireSectionsEditor,
} from '../fields';
import {
  renderAttachmentsDisclosure,
  wireAttachmentsDisclosure,
  teardownAttachmentsDisclosure,
} from '../attachments-disclosure';

// ----------------------------------------------------------------------
// Module-scope lifecycle state
// ----------------------------------------------------------------------

let totpTickerId: ReturnType<typeof setInterval> | null = null;
let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null;
let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null;
let sectionsExpanded = false;

function stopTotpTicker(): void {
  if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; }
}

/// Called by the dispatcher before each render. Stops the countdown ticker
/// AND removes the detail-view's keyboard handler so they don't leak.
export function teardown(): void {
  teardownAttachmentsDisclosure();
  stopTotpTicker();
  if (activeKeyHandler) {
    document.removeEventListener('keydown', activeKeyHandler);
    activeKeyHandler = null;
  }
  if (activeFormEscHandler) {
    document.removeEventListener('keydown', activeFormEscHandler);
    activeFormEscHandler = null;
  }
  sectionsExpanded = false;
}

// ----------------------------------------------------------------------
// Detail view
// ----------------------------------------------------------------------

export async function renderDetail(app: HTMLElement, item: Item): Promise<void> {
  if (item.core.type !== 'totp') return;
  const c = item.core;
  const secretB32 = base32Encode(new Uint8Array(c.config.secret));
  const isSteam = c.config.kind === 'steam';

  const headerLine = c.issuer
    ? `${escapeHtml(c.issuer)}${c.label ? ` · ${escapeHtml(c.label)}` : ''}`
    : escapeHtml(item.title);

  // Countdown ring SVG. Stroke-dashoffset animates per tick (CSS transition
  // gives the smooth sweep between seconds).
  const ringSvg = `
    <svg width="32" height="32" viewBox="0 0 32 32" style="display:block;">
      <circle cx="16" cy="16" r="14" stroke="#30363d" stroke-width="2" fill="none"/>
      <circle id="totp-ring-arc" cx="16" cy="16" r="14" stroke="#d2ab43" stroke-width="2" fill="none"
              stroke-linecap="round" stroke-dasharray="87.96"
              transform="rotate(-90 16 16)" style="transition:stroke-dashoffset 1s linear;"/>
    </svg>
  `;

  const sigInner = `
    <div style="display:flex;justify-content:space-between;align-items:center;">
      <div>
        <div style="font-size:11px;color:#8b949e;letter-spacing:0.04em;">${headerLine}</div>
        <div id="totp-code" style="font-family:monospace;font-size:28px;letter-spacing:0.12em;color:#c9d1d9;margin-top:4px;">${isSteam ? '·····' : '······'}</div>
      </div>
      <div style="display:flex;flex-direction:column;align-items:center;gap:4px;">
        ${ringSvg}
        <span id="totp-countdown" style="font-size:10px;color:#8b949e;font-variant-numeric:tabular-nums;">…</span>
      </div>
    </div>
  `;

  app.innerHTML = `
    <div class="pad">
      <div style="margin-bottom:12px;">
        <div class="detail-title" style="margin-bottom:8px;">${escapeHtml(item.title)}</div>
        ${renderSignatureBlock({ accent: 'gold', children: sigInner })}
      </div>
      ${c.issuer ? renderRow({ label: 'issuer', value: c.issuer }) : ''}
      ${c.label  ? renderRow({ label: 'label',  value: c.label  }) : ''}
      ${renderRow({ label: 'kind', value: isSteam ? 'Steam Guard' : 'TOTP' })}
      ${renderConcealedRow({ id: 'totp-secret', label: 'secret', value: secretB32, monospace: true })}
      ${renderSections(item, 'totp')}
      ${renderAttachmentsDisclosure({ itemId: item.id, attachments: item.attachments, mode: 'view' })}
      <div class="form-actions" style="margin-top:14px;">
        <button class="btn" id="back-btn">back</button>
        <button class="btn" id="edit-btn">edit</button>
        <button class="btn danger" id="trash-btn">trash</button>
      </div>
    </div>
  `;

  wireFieldHandlers(app);
  wireAttachmentsDisclosure(app, { itemId: item.id, attachments: item.attachments, mode: 'view' });

  // Start the ticker — re-fetches code + countdown every second from the SW.
  startTotpTicker(item.id, c.config.period_seconds || 30);

  document.getElementById('back-btn')?.addEventListener('click', () => {
    teardown();
    navigate('list');
  });
  document.getElementById('edit-btn')?.addEventListener('click', () => {
    teardown();
    navigate('edit');
  });
  document.getElementById('trash-btn')?.addEventListener('click', async () => {
    if (!confirm(`Move "${item.title}" to trash?`)) return;
    teardown();
    const resp = await sendMessage({ type: 'delete_item', id: item.id });
    if (!resp.ok) { setState({ error: resp.error }); return; }
    const listResp = await sendMessage({ type: 'list_items' });
    if (listResp.ok) {
      const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
      navigate('list', { entries: data.items, selectedId: null, selectedItem: null });
    } else navigate('list');
  });

  const handler = async (e: KeyboardEvent) => {
    // Don't steal printable keystrokes from editable fields.
    const t = e.target;
    if (t instanceof HTMLElement) {
      const tag = t.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || t.isContentEditable) return;
    }
    switch (e.key) {
      case 'Escape': teardown(); navigate('list'); break;
      case 'e':      teardown(); navigate('edit'); break;
      case 't': {
        // Copy the currently displayed rotating code.
        const codeEl = document.getElementById('totp-code');
        const code = codeEl?.textContent?.trim();
        if (code && code !== '·····' && code !== '······') {
          try { await navigator.clipboard.writeText(code); } catch { /* swallow */ }
        }
        break;
      }
      case 'd':
        e.preventDefault();
        if (confirm(`Move "${item.title}" to trash?`)) {
          teardown();
          const resp = await sendMessage({ type: 'delete_item', id: item.id });
          if (!resp.ok) { setState({ error: resp.error }); return; }
          const listResp = await sendMessage({ type: 'list_items' });
          if (listResp.ok) {
            const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
            navigate('list', { entries: data.items, selectedId: null, selectedItem: null });
          } else navigate('list');
        }
        break;
    }
  };
  activeKeyHandler = handler;
  document.addEventListener('keydown', handler);
}

// ----------------------------------------------------------------------
// Countdown ticker
// ----------------------------------------------------------------------

function startTotpTicker(id: ItemId, period: number): void {
  stopTotpTicker();
  const circumference = 2 * Math.PI * 14;
  const tick = async () => {
    const r = await sendMessage({ type: 'get_totp', id });
    if (!r.ok) return;
    const { code, expires_at } = r.data as { code: string; expires_at: number };
    const codeEl = document.getElementById('totp-code');
    const cdEl = document.getElementById('totp-countdown');
    const ring = document.getElementById('totp-ring-arc') as SVGCircleElement | null;
    if (codeEl) codeEl.textContent = code;
    const remaining = Math.max(0, expires_at - Math.floor(Date.now() / 1000));
    if (cdEl) cdEl.textContent = `${remaining}s`;
    if (ring) {
      const offset = circumference * (1 - remaining / period);
      ring.style.strokeDashoffset = String(offset);
    }
  };
  void tick();
  totpTickerId = setInterval(() => void tick(), 1000);
}

// ----------------------------------------------------------------------
// Form (add / edit) with TOTP/Steam kind toggle
// ----------------------------------------------------------------------

let formKind: TotpKind = 'totp';

export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Item | null): void {
  const state = getState();
  const title = existing?.title ?? '';
  const c = (existing?.core.type === 'totp') ? existing.core : null;
  formKind = c?.config.kind === 'steam' ? 'steam' : 'totp';
  const secretB32 = c?.config.secret ? base32Encode(new Uint8Array(c.config.secret)) : '';

  const sectionsDraft: Section[] = existing
    ? JSON.parse(JSON.stringify(existing.sections)) as Section[]
    : [];
  let attachmentsDraft: AttachmentRef[] = existing?.attachments ?? [];

  const renderInner = (): string => `
    <div class="pad">
      ${renderFormHeader({ titleText: mode === 'add' ? 'new totp' : 'edit totp' })}
      ${state.error ? `<div class="error">${escapeHtml(state.error)}</div>` : ''}
      <div class="form-group"><label class="label" for="f-title">title ${REQUIRED_PILL_HTML}</label>
        <input id="f-title" type="text" value="${escapeHtml(title)}" placeholder="GitHub"></div>
      <div class="form-group"><label class="label">kind</label>
        <div class="inline-row">
          <button type="button" id="kind-totp" class="btn ${formKind === 'totp' ? 'btn-primary' : ''}" style="flex:1;">TOTP</button>
          <button type="button" id="kind-steam" class="btn ${formKind === 'steam' ? 'btn-primary' : ''}" style="flex:1;">Steam Guard</button>
        </div>
        <p class="muted" style="font-size:11px;margin-top:4px;" id="kind-blurb">${formKind === 'steam' ? 'Steam Mobile Authenticator (5-char alphanumeric)' : 'Standard time-based codes (6 digits)'}</p>
      </div>
      <div class="form-group"><label class="label" for="f-secret">secret (base32) ${REQUIRED_PILL_HTML}</label>
        <input id="f-secret" type="text" value="${escapeHtml(secretB32)}" placeholder="JBSWY3DPEHPK3PXP"></div>
      <div class="form-group"><label class="label" for="f-issuer">issuer</label>
        <input id="f-issuer" type="text" value="${escapeHtml(c?.issuer ?? '')}" placeholder="GitHub"></div>
      <div class="form-group"><label class="label" for="f-label">label</label>
        <input id="f-label" type="text" value="${escapeHtml(c?.label ?? '')}" placeholder="alice@github.com"></div>
      ${renderSectionsEditor(sectionsDraft, sectionsExpanded)}
      ${renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' })}
      <div class="form-actions">
        <button class="btn" id="cancel-btn">cancel</button>
        <button class="btn btn-primary" id="save-btn">save</button>
      </div>
    </div>
  `;

  app.innerHTML = renderInner();

  // In-place re-render on kind toggle. Preserves current input values so
  // the user doesn't lose what they've typed.
  const reRender = (): void => {
    const titleVal  = (document.getElementById('f-title')  as HTMLInputElement).value;
    const secretVal = (document.getElementById('f-secret') as HTMLInputElement).value;
    const issuerVal = (document.getElementById('f-issuer') as HTMLInputElement).value;
    const labelVal  = (document.getElementById('f-label')  as HTMLInputElement).value;
    // Preserve the disclosure's live expanded state across kind-toggle re-render.
    const currentDisclosure = app.querySelector('.disclosure');
    if (currentDisclosure) {
      sectionsExpanded = currentDisclosure.getAttribute('data-expanded') === 'true';
    }
    app.innerHTML = renderInner();
    (document.getElementById('f-title')  as HTMLInputElement).value = titleVal;
    (document.getElementById('f-secret') as HTMLInputElement).value = secretVal;
    (document.getElementById('f-issuer') as HTMLInputElement).value = issuerVal;
    (document.getElementById('f-label')  as HTMLInputElement).value = labelVal;
    wireKindToggle();
    wireFormButtons(mode, existing, sectionsDraft, attachmentsDraft);
    wireSectionsEditor(app, sectionsDraft, sectionsRerender);
    wireDisclosure();
  };

  // Rerender only the sections editor in place (used by structural section
  // mutations — add/remove). Reuses the form-wide reRender for simplicity
  // since kind toggle already re-mounts the full inner DOM; here we just
  // need to preserve sectionsExpanded and swap the disclosure block.
  const sectionsRerender = (): void => {
    const disclosure = app.querySelector('.disclosure');
    if (!disclosure) return;
    sectionsExpanded = disclosure.getAttribute('data-expanded') === 'true';
    disclosure.outerHTML = renderSectionsEditor(sectionsDraft, sectionsExpanded);
    wireSectionsEditor(app, sectionsDraft, sectionsRerender);
  };

  const wireDisclosure = (): void => {
    wireAttachmentsDisclosure(app, {
      itemId: existing?.id ?? '',
      attachments: attachmentsDraft,
      mode: 'edit',
      onChange: (next) => {
        attachmentsDraft = next;
        const disc = app.querySelector('.attachments-disclosure');
        if (disc) {
          disc.outerHTML = renderAttachmentsDisclosure({
            itemId: existing?.id ?? '',
            attachments: attachmentsDraft,
            mode: 'edit',
          });
          wireDisclosure();
        }
      },
    });
  };

  const wireKindToggle = (): void => {
    document.getElementById('kind-totp')?.addEventListener('click', () => {
      formKind = 'totp';
      reRender();
    });
    document.getElementById('kind-steam')?.addEventListener('click', () => {
      formKind = 'steam';
      reRender();
    });
  };

  wireKindToggle();
  wireFormButtons(mode, existing, sectionsDraft, attachmentsDraft);
  wireSectionsEditor(app, sectionsDraft, sectionsRerender);
  wireDisclosure();

  const escHandler = (e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      setState({ error: null });
      navigate(mode === 'edit' ? 'detail' : 'list');
    }
  };
  activeFormEscHandler = escHandler;
  document.addEventListener('keydown', escHandler);

  (document.getElementById('f-title') as HTMLInputElement | null)?.focus();
}

function wireFormButtons(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): void {
  document.getElementById('cancel-btn')?.addEventListener('click', () => {
    setState({ error: null });
    navigate(mode === 'edit' ? 'detail' : 'list');
  });
  document.getElementById('popout-btn')?.addEventListener('click', popOutToTab);
  document.getElementById('save-btn')?.addEventListener('click', async () => {
    await saveTotp(mode, existing, sectionsDraft, attachmentsDraft);
  });
}

async function saveTotp(mode: 'add' | 'edit', existing: Item | null, sectionsDraft: Section[], attachmentsDraft: AttachmentRef[]): Promise<void> {
  const state = getState();
  const title = (document.getElementById('f-title') as HTMLInputElement).value.trim();
  if (!title) { setState({ error: 'Title is required' }); return; }

  const secretStr = (document.getElementById('f-secret') as HTMLInputElement).value.trim();
  if (!secretStr) { setState({ error: 'Secret is required' }); return; }

  let secretBytes: Uint8Array;
  try {
    secretBytes = base32Decode(secretStr);
  } catch (err) {
    setState({ error: `Invalid TOTP secret: ${err instanceof Error ? err.message : String(err)}` });
    return;
  }
  if (secretBytes.length === 0) { setState({ error: 'Secret decoded to zero bytes' }); return; }

  const get = (id: string) => (document.getElementById(id) as HTMLInputElement).value.trim();

  const isSteam = formKind === 'steam';
  const core = {
    type: 'totp' as const,
    config: {
      secret: Array.from(secretBytes),
      algorithm: 'sha1' as const,
      digits: isSteam ? 5 : 6,
      period_seconds: 30,
      kind: (isSteam ? 'steam' : 'totp') as TotpKind,
    },
    issuer: get('f-issuer') || undefined,
    label:  get('f-label')  || undefined,
  };

  const now = Math.floor(Date.now() / 1000);
  const item: Item = {
    id: existing?.id ?? '',
    title, type: 'totp',
    tags: existing?.tags ?? [],
    favorite: existing?.favorite ?? false,
    group: existing?.group, notes: existing?.notes,
    created: existing?.created ?? now,
    modified: now, trashed_at: undefined,
    core,
    sections: sectionsDraft,
    attachments: attachmentsDraft,
    field_history: existing?.field_history ?? {},
  };

  setState({ loading: true, error: null });
  const resp = mode === 'add'
    ? await sendMessage({ type: 'add_item', item })
    : await sendMessage({ type: 'update_item', id: state.selectedId!, item });
  if (resp.ok) {
    const listResp = await sendMessage({ type: 'list_items' });
    if (listResp.ok) {
      const data = listResp.data as { items: Array<[ItemId, ManifestEntry]> };
      navigate('list', { entries: data.items, selectedId: null, selectedItem: null });
    } else navigate('list');
  } else {
    setState({ loading: false, error: resp.error });
  }
}
