/**
 * Invitation Screen - Manages invitations (create, import, view, monitor).
 *
 * Provides:
 * - Import invitation by ID with multi-step import flow
 * - View active invitations with detailed information
 * - Monitor invitation updates via SSE
 * - Fill missing requirements
 * - Sign and complete invitations
 */

import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Box, Text } from 'ink';
import { InputDialog } from '../../components/Dialog.js';
import { ScrollableList, type ListItemData, type ListGroup } from '../../components/List.js';
import { useNavigation } from '../../hooks/useNavigation.js';
import { useAppContext, useStatus } from '../../hooks/useAppContext.js';
import { useBlockableInput, useIsInputCaptured } from '../../hooks/useInputLayer.js';
import { useInvitations } from '../../hooks/useInvitations.js';
import { useSatoshisConversion } from '../../hooks/useSatoshisConversion.js';
import { colors, logoSmall, formatSatoshis } from '../../theme.js';
import { copyToClipboard } from '../../utils/clipboard.js';
import type { Invitation } from '../../../services/invitation.js';
import type { XOInvitationCommit, XOInvitationVariableValue, XOTemplate } from '@xo-cash/types';

import {
  getInvitationState,
  getStateColorName,
  getInvitationInputs,
  getInvitationOutputs,
  getInvitationVariables,
  formatInvitationListItem,
  formatInvitationId,
} from '../../../utils/invitation-utils.js';

import { InvitationImportFlow } from './invitation-import/InvitationImportFlow.js';
import { compileCashAssemblyString } from '@xo-cash/engine';

/**
 * Map state color name to theme color.
 */
function getStateColor(state: string): string {
  const colorName = getStateColorName(state);
  switch (colorName) {
    case 'info':
      return colors.info as string;
    case 'warning':
      return colors.warning as string;
    case 'success':
      return colors.success as string;
    case 'error':
      return colors.error as string;
    case 'muted':
    default:
      return colors.textMuted as string;
  }
}

/**
 * Action menu items.
 */
const actionItems: ListItemData<string>[] = [
  { key: 'accept', label: 'Accept & Join', value: 'accept' },
  { key: 'fill', label: 'Fill Requirements', value: 'fill' },
  { key: 'sign', label: 'Sign Transaction', value: 'sign' },
  { key: 'broadcast', label: 'Broadcast Transaction', value: 'broadcast' },
  { key: 'copy', label: 'Copy Invitation ID', value: 'copy' },
];

/**
 * Invitation list item with invitation value or null for import action.
 */
type InvitationListItem = ListItemData<Invitation | null>;

/**
 * Groups for the invitation list.
 */
const invitationListGroups: ListGroup[] = [
  { id: 'actions' },
  { id: 'invitations', separator: true },
];

type OwnInvitationContext = {
  entityIdentifier: string | null;
  roleIdentifier: string | null;
};

function getRoleIdentifierFromCommits(commits: XOInvitationCommit[]): string | null {
  for (const commit of commits) {
    for (const input of commit.data.inputs ?? []) {
      if (input.roleIdentifier) return input.roleIdentifier;
    }

    for (const output of commit.data.outputs ?? []) {
      if (output.roleIdentifier) return output.roleIdentifier;
    }

    for (const variable of commit.data.variables ?? []) {
      if (variable.roleIdentifier) return variable.roleIdentifier;
    }
  }

  return null;
}

/**
 * Invitation Screen Component.
 */
export function InvitationScreen(): React.ReactElement {
  const { navigate, data: navData } = useNavigation();
  const { appService, showError, showInfo } = useAppContext();
  const { setStatus } = useStatus();

  const invitations = useInvitations();
  const { currencyCode, formattedFiatPerBchRate, formatSatoshisToFiat } =
    useSatoshisConversion();

  // ── UI state ─────────────────────────────────────────────────────────────
  const [selectedIndex, setSelectedIndex] = useState(0);
  const [selectedActionIndex, setSelectedActionIndex] = useState(0);
  const [focusedPanel, setFocusedPanel] = useState<'list' | 'actions'>('list');
  const [isLoading, setIsLoading] = useState(false);

  // ── Import state ─────────────────────────────────────────────────────────
  // Two phases: first the ID input dialog, then the multi-step import flow.
  const [showIdDialog, setShowIdDialog] = useState(false);
  const [importingId, setImportingId] = useState<string | null>(null);
  const [pendingImportedInvitationId, setPendingImportedInvitationId] = useState<string | null>(null);

  // ── Template cache ───────────────────────────────────────────────────────
  const [templateCache, setTemplateCache] = useState<Map<string, XOTemplate>>(new Map());
  const [selectedTemplate, setSelectedTemplate] = useState<XOTemplate | null>(null);
  const [ownInvitationContext, setOwnInvitationContext] = useState<OwnInvitationContext>({
    entityIdentifier: null,
    roleIdentifier: null,
  });

  // Check if we should open import dialog on mount
  const initialMode = navData.mode as string | undefined;

  useEffect(() => {
    if (initialMode === 'import') {
      setShowIdDialog(true);
    }
  }, [initialMode]);

  /**
   * Load templates for all invitations (for list display).
   */
  useEffect(() => {
    if (!appService) return;

    invitations.forEach(inv => {
      const templateId = inv.data.templateIdentifier;
      if (!templateCache.has(templateId)) {
        appService.engine.getTemplate(templateId).then(template => {
          if (template) {
            setTemplateCache(prev => new Map(prev).set(templateId, template));
          }
        });
      }
    });
  }, [invitations, appService, templateCache]);

  /**
   * Build list items for ScrollableList.
   */
  const listItems = useMemo((): InvitationListItem[] => {
    const importItem: InvitationListItem = {
      key: 'import',
      label: '+ Import Invitation',
      value: null,
      group: 'actions',
      color: 'info',
    };

    const invitationItems: InvitationListItem[] = invitations.map(inv => {
      const template = templateCache.get(inv.data.templateIdentifier);
      const formatted = formatInvitationListItem(inv, template);

      return {
        key: inv.data.invitationIdentifier,
        label: formatted.label,
        value: inv,
        group: 'invitations',
        color: formatted.statusColor,
        hidden: !formatted.isValid,
      };
    });

    return [importItem, ...invitationItems];
  }, [invitations.length, templateCache]);

  const selectedItem = listItems[selectedIndex];
  const selectedInvitation = selectedItem?.value ?? null;

  /**
   * Load template for selected invitation.
   */
  useEffect(() => {
    if (!selectedInvitation || !appService) {
      setSelectedTemplate(null);
      return;
    }

    appService.engine.getTemplate(selectedInvitation.data.templateIdentifier)
      .then(template => setSelectedTemplate(template ?? null));
  }, [selectedInvitation, appService]);

  /**
   * Load the current engine entity's commits for the selected invitation.
   */
  useEffect(() => {
    if (!selectedInvitation || !appService) {
      setOwnInvitationContext({
        entityIdentifier: null,
        roleIdentifier: null,
      });
      return;
    }

    let isCurrent = true;

    appService.engine.findOwnCommits(selectedInvitation.data.invitationIdentifier)
      .then((ownCommits) => {
        if (!isCurrent) return;

        setOwnInvitationContext({
          entityIdentifier: ownCommits[0]?.entityIdentifier ?? null,
          roleIdentifier: getRoleIdentifierFromCommits(ownCommits),
        });
      })
      .catch(() => {
        if (!isCurrent) return;

        setOwnInvitationContext({
          entityIdentifier: null,
          roleIdentifier: null,
        });
      });

    return () => {
      isCurrent = false;
    };
  }, [selectedInvitation, appService]);

  // ── Import flow callbacks ──────────────────────────────────────────────

  /**
   * ID dialog submitted — transition to the multi-step import flow.
   */
  const handleImportIdSubmit = useCallback((invitationId: string) => {
    if (!invitationId.trim()) {
      setShowIdDialog(false);
      return;
    }
    setShowIdDialog(false);
    setImportingId(invitationId.trim());
  }, []);

  /**
   * Import flow closed (completed or cancelled).
   */
  const handleImportFlowClose = useCallback((importedInvitationId?: string) => {
    if (importedInvitationId) {
      setPendingImportedInvitationId(importedInvitationId);
    }
    setImportingId(null);
  }, []);

  /**
   * Once imported invitation is visible in the list, select and focus it.
   */
  useEffect(() => {
    if (!pendingImportedInvitationId) return;

    const importedIndex = listItems.findIndex((item) => {
      return item.value?.data.invitationIdentifier === pendingImportedInvitationId;
    });

    if (importedIndex >= 0) {
      setSelectedIndex(importedIndex);
      setFocusedPanel('list');
      setPendingImportedInvitationId(null);
    }
  }, [pendingImportedInvitationId, listItems]);

  // ── Action handlers ────────────────────────────────────────────────────

  const acceptInvitation = useCallback(async () => {
    if (!selectedInvitation) {
      showError('No invitation selected');
      return;
    }

    try {
      setIsLoading(true);
      setStatus('Accepting invitation...');

      await selectedInvitation.accept();
      showInfo('Invitation accepted! You are now a participant.\n\nNext step: Use "Fill Requirements" to add your UTXOs.');
      setStatus('Ready');
    } catch (error) {
      const errorMsg = error instanceof Error ? error.message : String(error);
      if (errorMsg.toLowerCase().includes('already') || errorMsg.toLowerCase().includes('participant')) {
        showInfo('You have already accepted this invitation.\n\nNext step: Use "Fill Requirements" to add your UTXOs.');
      } else {
        showError(`Failed to accept: ${errorMsg}`);
      }
    } finally {
      setIsLoading(false);
    }
  }, [selectedInvitation, showInfo, showError, setStatus]);

  const signInvitation = useCallback(async () => {
    if (!selectedInvitation) {
      showError('No invitation selected');
      return;
    }

    try {
      setIsLoading(true);
      setStatus('Signing invitation...');

      await selectedInvitation.sign();
      showInfo('Invitation signed!');
      setStatus('Ready');
    } catch (error) {
      showError(`Failed to sign: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setIsLoading(false);
    }
  }, [selectedInvitation, showInfo, showError, setStatus]);

  /**
   * Broadcast transaction.
   */
  const broadcastTransaction = useCallback(async () => {
    if (!selectedInvitation) return;

    setIsLoading(true);
    setStatus('Broadcasting transaction...');

    try {
      await selectedInvitation.broadcast();
      showInfo(
        `Transaction Broadcast Successful!\n\n` +
        `The transaction has been submitted to the network.`
      );
      setStatus('Ready');
    } catch (error) {
      showError(`Failed to broadcast: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setIsLoading(false);
      setStatus('Ready');
    }
  }, [selectedInvitation, showInfo, showError, setStatus]);

  const copyId = useCallback(async () => {
    if (!selectedInvitation) {
      showError('No invitation selected');
      return;
    }

    try {
      await copyToClipboard(selectedInvitation.data.invitationIdentifier);
      showInfo(`Copied!\n\n${selectedInvitation.data.invitationIdentifier}`);
    } catch (error) {
      showError(`Failed to copy: ${error instanceof Error ? error.message : String(error)}`);
    }
  }, [selectedInvitation, showInfo, showError]);

  const fillRequirements = useCallback(async () => {
    if (!selectedInvitation) {
      showError('No invitation selected');
      return;
    }

    try {
      setIsLoading(true);

      setStatus('Checking available roles...');
      const roles = await selectedInvitation.getAvailableRoles();

      if (roles.length === 0) {
        showInfo('You are already participating in this invitation. Checking if inputs are needed...');
      } else {
        const roleToTake = roles[0];
        showInfo(`Accepting invitation as role: ${roleToTake}`);
        setStatus(`Accepting as ${roleToTake}...`);

        try {
          await selectedInvitation.accept();
        } catch (e) {
          showError(`Failed to accept role: ${e instanceof Error ? e.message : String(e)}`);
          setStatus('Ready');
          return;
        }
      }

      setStatus('Analyzing invitation...');

      let requiredAmount = 0n;
      const commits = selectedInvitation.data.commits || [];
      for (const commit of commits) {
        const variables = commit.data?.variables || [];
        for (const variable of variables) {
          if (variable.variableIdentifier?.toLowerCase().includes('satoshi')) {
            requiredAmount = BigInt(variable.value?.toString() || '0');
            break;
          }
        }
        if (requiredAmount > 0n) break;
      }

      const fee = 500n;
      const dust = 546n;
      const totalNeeded = requiredAmount + fee + dust;

      const utxos = await selectedInvitation.findSuitableResources({
        templateIdentifier: selectedInvitation.data.templateIdentifier,
        outputIdentifier: 'receiveOutput',
      });

      if (utxos.length === 0) {
        showError('No suitable UTXOs found. Make sure your wallet has funds.');
        setStatus('Ready');
        return;
      }

      setStatus('Selecting UTXOs...');

      const selectedUtxos: Array<{
        outpointTransactionHash: string;
        outpointIndex: number;
        valueSatoshis: bigint;
      }> = [];
      let accumulated = 0n;
      const seenLockingBytecodes = new Set<string>();

      for (const utxo of utxos) {
        const lockingBytecodeHex = utxo.scriptHash
          ? typeof utxo.scriptHash === 'string'
            ? utxo.scriptHash
            : Buffer.from(utxo.scriptHash).toString('hex')
          : undefined;

        if (lockingBytecodeHex && seenLockingBytecodes.has(lockingBytecodeHex)) continue;
        if (lockingBytecodeHex) seenLockingBytecodes.add(lockingBytecodeHex);

        selectedUtxos.push({
          outpointTransactionHash: utxo.outpointTransactionHash,
          outpointIndex: utxo.outpointIndex,
          valueSatoshis: BigInt(utxo.valueSatoshis),
        });
        accumulated += BigInt(utxo.valueSatoshis);

        if (accumulated >= totalNeeded) break;
      }

      if (accumulated < totalNeeded) {
        showError(`Insufficient funds. Need ${formatSatoshis(totalNeeded)}, have ${formatSatoshis(accumulated)}`);
        setStatus('Ready');
        return;
      }

      const changeAmount = accumulated - requiredAmount - fee;

      setStatus('Adding inputs...');
      await selectedInvitation.addInputs(
        selectedUtxos.map(u => ({
          outpointTransactionHash: new Uint8Array(Buffer.from(u.outpointTransactionHash, 'hex')),
          outpointIndex: u.outpointIndex,
        }))
      );

      if (changeAmount >= dust) {
        setStatus('Adding change output...');
        await selectedInvitation.addOutputs([{
          valueSatoshis: changeAmount,
        }]);
      }

      showInfo(
        `Requirements filled!\n\n` +
        `• Selected ${selectedUtxos.length} UTXO(s)\n` +
        `• Total: ${formatSatoshis(accumulated)}\n` +
        `• Required: ${formatSatoshis(requiredAmount)}\n` +
        `• Fee: ${formatSatoshis(fee)}\n` +
        `• Change: ${formatSatoshis(changeAmount)}\n\n` +
        `Now use "Sign Transaction" to complete.`
      );
      setStatus('Ready');
    } catch (error) {
      showError(`Failed to fill requirements: ${error instanceof Error ? error.message : String(error)}`);
      setStatus('Ready');
    } finally {
      setIsLoading(false);
    }
  }, [selectedInvitation, showInfo, showError, setStatus]);

  const handleAction = useCallback((action: string) => {
    switch (action) {
      case 'copy':
        copyId();
        break;
      case 'accept':
        acceptInvitation();
        break;
      case 'fill':
        fillRequirements();
        break;
      case 'sign':
        signInvitation();
        break;
      case 'broadcast':
        broadcastTransaction();
        break;
    }
  }, [selectedInvitation, copyId, acceptInvitation, fillRequirements, signInvitation, broadcastTransaction, navigate]);

  const handleListItemActivate = useCallback((item: InvitationListItem, _index: number) => {
    if (item.key === 'import') {
      setShowIdDialog(true);
    }
  }, []);

  const handleActionItemActivate = useCallback((item: ListItemData<string>, _index: number) => {
    if (item.value) {
      handleAction(item.value);
    }
  }, [handleAction]);

  // ── Keyboard navigation ──────────────────────────────────────────────────
  // Automatically blocked when any dialog/overlay is capturing input.
  const isCaptured = useIsInputCaptured();

  useBlockableInput((input, key) => {
    if (key.tab) {
      setFocusedPanel(prev => prev === 'list' ? 'actions' : 'list');
      return;
    }

    if (input === 'c' && selectedInvitation) {
      copyId();
    }

    if (input === 'i') {
      setShowIdDialog(true);
    }
  });

  // ── Render helpers ───────────────────────────────────────────────────────

  const renderInvitationListItem = useCallback((
    item: InvitationListItem,
    isSelected: boolean,
    isFocused: boolean
  ): React.ReactNode => {
    if (item.key === 'import') {
      return (
        <Text
          color={isFocused ? colors.focus : colors.info}
          bold={isSelected}
        >
          {isFocused ? '▸ ' : '  '}
          {item.label}
        </Text>
      );
    }

    const inv = item.value;
    if (!inv) return null;

    const state = getInvitationState(inv);
    const template = templateCache.get(inv.data.templateIdentifier);
    const templateName = template?.name ?? 'Unknown';

    return (
      <Text
        color={isFocused ? colors.focus : colors.text}
        bold={isSelected}
      >
        {isFocused ? '▸ ' : '  '}
        <Text color={getStateColor(state)}>[{state}]</Text>
        {' '}{templateName}-{inv.data.actionIdentifier} ({formatInvitationId(inv.data.invitationIdentifier, 8)})
      </Text>
    );
  }, [templateCache]);

  const renderDetails = () => {
    if (!selectedInvitation) {
      return <Text color={colors.textMuted}>Select an invitation to view details</Text>;
    }

    const state = getInvitationState(selectedInvitation);
    const action = selectedTemplate?.actions?.[selectedInvitation.data.actionIdentifier];
    const inputs = getInvitationInputs(selectedInvitation);
    const outputs = getInvitationOutputs(selectedInvitation);
    const variables = getInvitationVariables(selectedInvitation);
    const userEntityId = ownInvitationContext.entityIdentifier;
    const userRole = ownInvitationContext.roleIdentifier;
    const roleInfoRaw = userRole && selectedTemplate?.roles?.[userRole];
    const roleInfo = roleInfoRaw && typeof roleInfoRaw === 'object' ? roleInfoRaw : null;

    const getFiatSuffix = (satoshis: bigint): string => {
      const fiatValue = formatSatoshisToFiat(satoshis);
      return fiatValue ? ` (~${fiatValue})` : '';
    };

    const parseNumberishToBigInt = (value: unknown): bigint | null => {
      if (typeof value === 'bigint') {
        return value;
      }

      const asString = String(value).trim();
      if (!/^[-]?\d+$/.test(asString)) {
        return null;
      }

      try {
        return BigInt(asString);
      } catch {
        return null;
      }
    };

    const isSatoshisVariable = (variableIdentifier: string): boolean => {
      const templateVariable = selectedTemplate?.variables?.[variableIdentifier];
      const templateType = templateVariable?.type?.toLowerCase();
      const templateHint = templateVariable?.hint?.toLowerCase();
      const identifier = variableIdentifier.toLowerCase();

      if (templateHint?.includes('satoshi')) {
        return true;
      }

      return (
        templateType === 'integer' &&
        (identifier.includes('satoshi') || identifier.includes('amount'))
      );
    };

    return (
      <Box flexDirection="column">
        {/* Type & Status */}
        <Box flexDirection="row" marginBottom={1}>
          <Box width="50%">
            <Box flexDirection="column">
              <Text color={colors.primary} bold>Type: </Text>
              <Text color={colors.text}>{selectedTemplate?.name ?? 'Unknown Template'}</Text>
              <Text color={colors.textMuted} dimColor>
                {selectedTemplate?.description ?? 'No description available'}
              </Text>
            </Box>
          </Box>
          <Box width="50%">
            <Box flexDirection="column">
              <Text color={colors.primary} bold>Status: </Text>
              <Text color={getStateColor(state)}>{state}</Text>
              <Text color={colors.textMuted}>
                Action: {action?.name ?? selectedInvitation.data.actionIdentifier}
              </Text>
              {formattedFiatPerBchRate && (
                <Text color={colors.textMuted}>
                  1 BCH = {formattedFiatPerBchRate}
                </Text>
              )}
              {action?.description && (
                <Text color={colors.textMuted} dimColor>{action.description}</Text>
              )}
            </Box>
          </Box>
        </Box>

        {/* Your Role */}
        {userRole && (
          <Box marginBottom={1} flexDirection="column">
            <Text color={colors.primary} bold>Your Role: </Text>
            <Text color={colors.success}>{roleInfo?.name ?? userRole}</Text>
            {roleInfo?.description && (
              <Text color={colors.textMuted} dimColor>{roleInfo.description}</Text>
            )}
          </Box>
        )}

        {/* Inputs & Outputs */}
        <Box flexDirection="row" marginBottom={1}>
          <Box width="50%" flexDirection="column">
            <Text color={colors.primary} bold>Inputs ({inputs.length}):</Text>
            {inputs.length === 0 ? (
              <Text color={colors.textMuted}>  No inputs yet</Text>
            ) : (
              inputs.map((input, idx) => {
                const isUserInput = input.entityIdentifier === userEntityId;
                const inputTemplate = selectedTemplate?.inputs?.[input.inputIdentifier ?? ''];
                const inputSatoshis = (
                  'valueSatoshis' in input && input.valueSatoshis !== undefined
                )
                  ? parseNumberishToBigInt(input.valueSatoshis)
                  : null;
                return (
                  <Text
                    key={`input-${idx}`}
                    color={isUserInput ? colors.success : colors.text}
                  >
                    {/* Indicator for whether this is the user's input */}
                    {'  '}{isUserInput ? '• ' : '○ '}
                    
                    {/* TODO: Why doesnt this stuff work? It just cant resolve inputs? */}
                    {/* Input name */}
                    {inputTemplate?.name ?? input.inputIdentifier ?? `Input ${idx}`}
                    
                    {/* Input role */}
                    {input.roleIdentifier && ` (${input.roleIdentifier})`}
                    
                    {/* Input value */}
                    {inputSatoshis !== null && ` ${formatSatoshis(inputSatoshis)}${getFiatSuffix(inputSatoshis)}`}
                  </Text>
                );
              })
            )}
          </Box>

          <Box width="50%" flexDirection="column">
            <Text color={colors.primary} bold>Outputs ({outputs.length}):</Text>
            {outputs.length === 0 ? (
              <Text color={colors.textMuted}>  No outputs yet</Text>
            ) : (
              outputs.map((output, idx) => {
                const isUserOutput = output.entityIdentifier === userEntityId;
                const outputTemplate = selectedTemplate?.outputs?.[output.outputIdentifier ?? ''];
                const outputSatoshis = output.valueSatoshis !== undefined
                  ? parseNumberishToBigInt(output.valueSatoshis)
                  : null;
                return (
                  <Text
                    key={`output-${idx}`}
                    color={isUserOutput ? colors.success : colors.text}
                  >
                    {/* Indicator for whether this is the user's output */}
                    {'  '}{isUserOutput ? '• ' : '○ '}

                    {/* Output name */}
                    {outputTemplate?.name ?? output.outputIdentifier ?? `Output ${idx}`}
                    
                    {/* Output description */}
                    {outputTemplate?.description && ' - ' + compileCashAssemblyString({
                      cashAssemblyText: outputTemplate?.description,
                      variables: variables.reduce((acc, variable) => {
                        acc[variable.variableIdentifier] = variable.value as XOInvitationVariableValue;

                        return acc;
                      }, {} as Record<string, XOInvitationVariableValue>)
                    })}
                    
                    {/* Output value */}
                    {outputSatoshis !== null && ` (${formatSatoshis(outputSatoshis)}${getFiatSuffix(outputSatoshis)})`}
                  </Text>
                );
              })
            )}
          </Box>
        </Box>

        {/* Variables */}
        <Box flexDirection="column">
          <Text color={colors.primary} bold>Variables ({variables.length}):</Text>
          {variables.length === 0 ? (
            <Text color={colors.textMuted}>  No variables set</Text>
          ) : (
            variables.map((variable, idx) => {
              const isUserVariable = variable.entityIdentifier === userEntityId;
              const varTemplate = selectedTemplate?.variables?.[variable.variableIdentifier];
              const displayValue = typeof variable.value === 'bigint'
                ? variable.value.toString()
                : String(variable.value);
              const parsedVariableSatoshis = isSatoshisVariable(variable.variableIdentifier)
                ? parseNumberishToBigInt(variable.value)
                : null;
              return (
                <Text
                  key={`var-${idx}`}
                  color={isUserVariable ? colors.success : colors.text}
                >
                  {'  '}{isUserVariable ? '• ' : '○ '}
                  {varTemplate?.name ?? variable.variableIdentifier}: {displayValue}
                  {parsedVariableSatoshis !== null &&
                    ` (${formatSatoshis(parsedVariableSatoshis)}${getFiatSuffix(parsedVariableSatoshis)})`}
                  {varTemplate?.description && (
                    <Text color={colors.textMuted} dimColor> - {varTemplate.description}</Text>
                  )}
                </Text>
              );
            })
          )}
        </Box>

        <Box marginTop={1}>
          <Text color={colors.textMuted} dimColor>c: Copy ID</Text>
        </Box>
      </Box>
    );
  };

  // ── Main render ──────────────────────────────────────────────────────────

  return (
    <Box flexDirection="column" flexGrow={1}>
      {/* Header */}
      <Box borderStyle="single" borderColor={colors.secondary} paddingX={1}>
        <Text color={colors.primary} bold>{logoSmall} - Invitations</Text>
      </Box>

      {/* Top row: List + Actions */}
      <Box flexDirection="row" marginTop={1} height={12}>
        {/* Left column: Invitation list */}
        <Box flexDirection="column" width="70%" paddingRight={1}>
          <Box
            borderStyle="single"
            borderColor={focusedPanel === 'list' ? colors.focus : colors.primary}
            flexDirection="column"
            paddingX={1}
            flexGrow={1}
          >
            <Text color={colors.primary} bold> Invitations </Text>
            <ScrollableList
              items={listItems}
              selectedIndex={selectedIndex}
              onSelect={setSelectedIndex}
              onActivate={handleListItemActivate}
              focus={focusedPanel === 'list' && !isCaptured}
              maxVisible={6}
              groups={invitationListGroups}
              emptyMessage="No invitations yet"
              renderItem={renderInvitationListItem}
            />
          </Box>
        </Box>

        {/* Right column: Actions */}
        <Box flexDirection="column" width="30%" paddingLeft={1}>
          <Box
            borderStyle="single"
            borderColor={focusedPanel === 'actions' ? colors.focus : colors.border}
            flexDirection="column"
            paddingX={1}
            flexGrow={1}
          >
            <Text color={colors.primary} bold> Actions </Text>
            <ScrollableList
              items={actionItems}
              selectedIndex={selectedActionIndex}
              onSelect={setSelectedActionIndex}
              onActivate={handleActionItemActivate}
              focus={focusedPanel === 'actions' && !isCaptured}
              emptyMessage="No actions"
            />
          </Box>
        </Box>
      </Box>

      {/* Bottom row: Details */}
      <Box flexDirection="column" marginTop={1} flexGrow={1}>
        <Box
          borderStyle="single"
          borderColor={colors.border}
          flexDirection="column"
          paddingX={1}
          flexGrow={1}
        >
          <Text color={colors.primary} bold> Details </Text>
          <Box marginTop={1} flexDirection="column">
            {renderDetails()}
          </Box>
        </Box>
      </Box>

      {/* Help text */}
      <Box marginTop={1}>
        <Text color={colors.textMuted} dimColor>
          Tab: Switch panel • ↑↓: Navigate • Enter: Select • i: Import • c: Copy ID • Esc: Back
        </Text>
      </Box>

      {/* Import ID dialog */}
      {showIdDialog && (
        <Box
          position="absolute"
          flexDirection="column"
          alignItems="center"
          justifyContent="center"
          width="100%"
          height="100%"
        >
          <InputDialog
            title="Import Invitation"
            prompt="Enter Invitation ID:"
            placeholder="Paste invitation ID..."
            onSubmit={handleImportIdSubmit}
            onCancel={() => setShowIdDialog(false)}
            isActive={true}
          />
        </Box>
      )}

      {/* Multi-step import flow */}
      {importingId && appService && (
        <InvitationImportFlow
          invitationId={importingId}
          mode="screen"
          appService={appService}
          onClose={handleImportFlowClose}
          showError={showError}
          showInfo={showInfo}
          setStatus={setStatus}
        />
      )}
    </Box>
  );
}
