import { useEffect, useMemo, useState } from "react";
import type { ApiSettings } from "../../types";
import {
  createRecipientImportMappingProfile,
  listRecipientImportMappingProfiles,
  updateRecipientImportMappingProfile,
  type RecipientImportMappingProfilePayload } from
"../../api/campaigns";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FileDropZone } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { PageTitle } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { usePlatformUiCapability, type FilesFileExplorerUiCapability } from "@govoplan/core-webui";
import LockedVersionNotice from "./components/LockedVersionNotice";
import VersionLine from "./components/VersionLine";
import { ToggleSwitch } from "@govoplan/core-webui";
import { EmailAddressInput } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import DataGrid, { DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "../../components/table/DataGrid";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
import { getBool } from "./utils/draftEditor";
import { getDraftFields } from "./utils/fieldDefinitions";
import { normalizeAttachmentBasePaths, type AttachmentBasePath } from "./utils/attachments";
import {
  applyRecipientMappingProfile,
  buildImportTable,
  buildImportTableFromRows,
  buildRecipientImportPreview,
  createRecipientImportProvenance,
  createRecipientMappingProfile as buildRecipientMappingProfile,
  defaultColumnMappings,
  importedRowsNeedAttachmentSource,
  matchRecipientMappingProfiles,
  materializeRecipientImport,
  recipientImportHeaderFingerprints,
  xlsxSheetsFromArrayBuffer,
  type CsvDelimiter,
  type ImportedAddress,
  type RecipientColumnKind,
  type RecipientColumnMapping,
  type RecipientImportMode,
  type RecipientImportPreview,
  type RecipientImportProvenance,
  type RecipientImportSourceType,
  type RecipientImportSpreadsheetSheet,
  type RecipientMappingProfile,
  type RecipientMappingProfileMatch } from
"./utils/bulkImport";
import {
  bulkLinkFilesToCampaign,
  type ImportFileLinkCapability,
  renderedImportPatterns,
  resolveImportedAttachmentLinks,
  type ImportFileLinkResolution } from
"./utils/fileLinking";
import {
  addressesFromValue,
  collectCampaignAddressSuggestions,
  type MailboxAddress } from
"@govoplan/core-webui";
import { insertAfter, moveArrayItem, i18nMessage } from "@govoplan/core-webui";
const recipientHeaderRows = [
{ key: "to", label: "i18n:govoplan-campaign.to.ae79ea1e", toggleKey: "allow_individual_to", toggleLabel: "i18n:govoplan-campaign.allow_individual_to.966cb33e", addLabel: "i18n:govoplan-campaign.add_recipient.a989d1f1", emptyText: "i18n:govoplan-campaign.no_global_recipients_configured.d4e20e92" },
{ key: "cc", label: "i18n:govoplan-campaign.cc.c5a976de", toggleKey: "allow_individual_cc", toggleLabel: "i18n:govoplan-campaign.allow_individual_cc.0457c0e2", addLabel: "i18n:govoplan-campaign.add_cc.bcb39ea3", emptyText: "i18n:govoplan-campaign.no_global_cc_recipients_configured.8afb23c1" },
{ key: "bcc", label: "i18n:govoplan-campaign.bcc.4c0145a3", toggleKey: "allow_individual_bcc", toggleLabel: "i18n:govoplan-campaign.allow_individual_bcc.a932d94b", addLabel: "i18n:govoplan-campaign.add_bcc.ae9feacc", emptyText: "i18n:govoplan-campaign.no_global_bcc_recipients_configured.86ef64ab" }] as
const;

type RecipientAddressKey = "to" | "cc" | "bcc";

type EntryAddressColumn = {
  key: RecipientAddressKey | "from" | "reply_to";
  label: string;
  allowMultiple: boolean;
  addLabel: string;
  emptyText: string;
  mergeKey?: "merge_to" | "merge_cc" | "merge_bcc" | "merge_reply_to";
};

export default function RecipientDataPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
  const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
  const [importOpen, setImportOpen] = useState(false);
  const [recipientProfilesPage, setRecipientProfilesPage] = useState(1);
  const [recipientProfilesPageSize, setRecipientProfilesPageSize] = useState(10);

  const version = data.currentVersion;
  const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
  const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
    settings,
    campaignId,
    version,
    locked,
    reload,
    setError,
    currentStep: "recipients",
    unsavedTitle: "i18n:govoplan-campaign.unsaved_recipient_changes.4184b4ed",
    unsavedMessage: "i18n:govoplan-campaign.recipient_address_data_has_unsaved_changes_save_.563c3b94"
  });
  const recipientsSection = asRecord(displayDraft.recipients);
  const entries = asRecord(displayDraft.entries);
  const attachments = asRecord(displayDraft.attachments);
  const entryDefaults = asRecord(entries.defaults);
  const inlineEntries = asArray(entries.inline).map(asRecord);
  const source = asRecord(entries.source);
  const fieldDefinitions = useMemo(() => getDraftFields(displayDraft), [displayDraft]);
  const importBasePaths = useMemo(() => normalizeAttachmentBasePaths(attachments.base_paths, attachments, true), [attachments]);
  const defaultImportBasePath = useMemo(() => importBasePaths.find((basePath) => basePath.allow_individual) ?? importBasePaths[0] ?? null, [importBasePaths]);
  const addressSuggestions = useMemo(() => collectCampaignAddressSuggestions(displayDraft), [displayDraft]);
  const defaultFrom = addressesFromValue(recipientsSection.from).slice(0, 1);
  const globalReplyTo = addressesFromValue(recipientsSection.reply_to);
  const globalRecipientValues: Record<string, MailboxAddress[]> = {
    to: addressesFromValue(recipientsSection.to),
    cc: addressesFromValue(recipientsSection.cc),
    bcc: addressesFromValue(recipientsSection.bcc)
  };
  const entryAddressColumns = useMemo<EntryAddressColumn[]>(() => {
    const columns: EntryAddressColumn[] = [];
    if (getBool(recipientsSection, "allow_individual_from")) {
      columns.push({ key: "from", label: "i18n:govoplan-campaign.from.3f66052a", allowMultiple: false, addLabel: "i18n:govoplan-campaign.set_from.11fe7396", emptyText: "i18n:govoplan-campaign.uses_global_from.4cf8e6ce" });
    }
    if (getBool(recipientsSection, "allow_individual_reply_to")) {
      columns.push({ key: "reply_to", label: "i18n:govoplan-campaign.reply_to.c1733667", allowMultiple: true, addLabel: "i18n:govoplan-campaign.add_reply_to.84b195f4", emptyText: "i18n:govoplan-campaign.uses_global_reply_to.72c8d931", mergeKey: "merge_reply_to" });
    }
    if (getBool(recipientsSection, "allow_individual_to")) {
      columns.push({ key: "to", label: "i18n:govoplan-campaign.to.ae79ea1e", allowMultiple: true, addLabel: "i18n:govoplan-campaign.add_to.768a148b", emptyText: "i18n:govoplan-campaign.no_recipient_address.1301a488", mergeKey: "merge_to" });
    }
    if (getBool(recipientsSection, "allow_individual_cc")) columns.push({ key: "cc", label: "i18n:govoplan-campaign.cc.c5a976de", allowMultiple: true, addLabel: "i18n:govoplan-campaign.add_cc.bcb39ea3", emptyText: "i18n:govoplan-campaign.no_cc.5463a081", mergeKey: "merge_cc" });
    if (getBool(recipientsSection, "allow_individual_bcc")) columns.push({ key: "bcc", label: "i18n:govoplan-campaign.bcc.4c0145a3", allowMultiple: true, addLabel: "i18n:govoplan-campaign.add_bcc.ae9feacc", emptyText: "i18n:govoplan-campaign.no_bcc.48cade21", mergeKey: "merge_bcc" });
    return columns;
  }, [recipientsSection]);


  function replaceInlineEntries(nextEntries: Record<string, unknown>[]) {
    patch(["entries", "inline"], nextEntries);
  }

  function addRecipient(afterIndex = inlineEntries.length - 1) {
    const nextIndex = inlineEntries.length + 1;
    const newEntry = {
      id: uniqueEntryId(inlineEntries, `recipient-${nextIndex}`),
      active: true,
      to: [],
      cc: [],
      bcc: [],
      name: "",
      email: "",
      from: [],
      reply_to: [],
      merge_to: false,
      merge_cc: true,
      merge_bcc: true,
      merge_reply_to: true
    };
    replaceInlineEntries(insertAfter(inlineEntries, afterIndex, newEntry));
  }

  function updateEntry(index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) {
    const nextEntries = inlineEntries.map((entry, currentIndex) => currentIndex === index ? updater(entry) : entry);
    replaceInlineEntries(nextEntries);
  }

  function updateEntryAddressList(index: number, key: EntryAddressColumn["key"], addresses: MailboxAddress[]) {
    updateEntry(index, (entry) => {
      if (key === "from") {
        return { ...entry, from: addresses.slice(0, 1) };
      }
      const nextEntry = { ...entry, [key]: addresses };
      if (key === "to") {
        const address = addresses[0] ?? { name: "", email: "" };
        return {
          ...nextEntry,
          name: address.name ?? "",
          email: address.email
        };
      }
      return nextEntry;
    });
  }

  function updateEntryMerge(index: number, mergeKey: NonNullable<EntryAddressColumn["mergeKey"]>, checked: boolean) {
    updateEntry(index, (entry) => {
      const next = { ...entry, [mergeKey]: checked };
      delete next[mergeKey.replace("merge_", "combine_")];
      return next;
    });
  }

  function removeEntry(index: number) {
    replaceInlineEntries(inlineEntries.filter((_, currentIndex) => currentIndex !== index));
  }

  function moveEntry(index: number, targetIndex: number) {
    if (locked || index === targetIndex) return;
    replaceInlineEntries(moveArrayItem(inlineEntries, index, targetIndex));
  }

  function applyRecipientImport(preview: RecipientImportPreview, mode: RecipientImportMode, provenance?: RecipientImportProvenance | null) {
    if (locked || !draft) return;
    const needsAttachmentSource = importedRowsNeedAttachmentSource(preview);
    const currentAttachments = asRecord(draft.attachments);
    let nextBasePaths = normalizeAttachmentBasePaths(currentAttachments.base_paths, currentAttachments, true);
    let attachmentBasePath: AttachmentBasePath | null = null;

    if (needsAttachmentSource) {
      if (nextBasePaths.length === 0) {
        nextBasePaths = [{ id: "base-path-campaign", name: "Campaign files", path: ".", allow_individual: true, unsent_warning: false }];
      }
      const selectedIndex = Math.max(0, nextBasePaths.findIndex((basePath) => basePath.allow_individual));
      nextBasePaths = nextBasePaths.map((basePath, index) => index === selectedIndex ? { ...basePath, allow_individual: true } : basePath);
      attachmentBasePath = nextBasePaths[selectedIndex] ?? null;
    }

    const importedDraft = materializeRecipientImport(draft, preview, { mode, attachmentBasePath, provenance });
    const nextDraft = needsAttachmentSource ?
    {
      ...importedDraft,
      attachments: {
        ...asRecord(importedDraft.attachments),
        base_paths: nextBasePaths,
        base_path: nextBasePaths[0]?.path || "."
      }
    } :
    importedDraft;
    setDraft(nextDraft);
    markDirty();
    setImportOpen(false);
  }


  return (
    <div className="content-pad workspace-data-page">
      <div className="page-heading split workspace-heading">
        <div>
          <PageTitle loading={loading}>i18n:govoplan-campaign.sender_recipients.922c6d24</PageTitle>
          <VersionLine version={version} versions={data.versions} status={saveState} />
        </div>
        <div className="button-row compact-actions">
          <Button onClick={reload} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
          <Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3</Button>
        </div>
      </div>

      {error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
      {localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
      {locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}

      <LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
        <>
          <Card title="i18n:govoplan-campaign.campaign_sender.b12e8ae2" collapsible>
            <div className="campaign-header-stack">
              <div className="campaign-header-grid">
                <FormField label="i18n:govoplan-campaign.default_from_address.b9ee6d77">
                  <EmailAddressInput
                    value={defaultFrom}
                    suggestions={addressSuggestions}
                    allowMultiple={false}
                    disabled={locked}
                    addLabel="i18n:govoplan-campaign.set_from.11fe7396"
                    emptyText="i18n:govoplan-campaign.no_global_from_address_configured.28141d2b"
                    onChange={(addresses: MailboxAddress[]) => patch(["recipients", "from"], addresses.slice(0, 1))} />
                  
                </FormField>
                <div className="campaign-header-toggle">
                  <ToggleSwitch
                    label="i18n:govoplan-campaign.allow_individual_senders.f85fda12"
                    checked={getBool(recipientsSection, "allow_individual_from")}
                    disabled={locked}
                    onChange={(checked) => patch(["recipients", "allow_individual_from"], checked)} />
                  
                </div>
              </div>

              <div className="campaign-header-grid">
                <FormField label="i18n:govoplan-campaign.global_reply_to_address.bec1a89b">
                  <EmailAddressInput
                    value={globalReplyTo}
                    suggestions={addressSuggestions}
                    allowMultiple
                    disabled={locked}
                    addLabel="i18n:govoplan-campaign.add_reply_to.84b195f4"
                    emptyText="i18n:govoplan-campaign.no_global_reply_to_address_configured.d62a3b9b"
                    onChange={(addresses: MailboxAddress[]) => patch(["recipients", "reply_to"], addresses)} />
                  
                </FormField>
                <div className="campaign-header-toggle">
                  <ToggleSwitch
                    label="i18n:govoplan-campaign.allow_individual_reply_to.028f8044"
                    checked={getBool(recipientsSection, "allow_individual_reply_to")}
                    disabled={locked}
                    onChange={(checked) => patch(["recipients", "allow_individual_reply_to"], checked)} />
                  
                </div>
              </div>
            </div>
          </Card>

          <Card title="i18n:govoplan-campaign.global_recipient_headers.e3d87f85" collapsible>
            <div className="campaign-header-stack">
              {recipientHeaderRows.map((row) =>
              <div className="campaign-header-grid" key={row.key}>
                  <FormField label={row.label}>
                    <EmailAddressInput
                    value={globalRecipientValues[row.key] ?? []}
                    suggestions={addressSuggestions}
                    allowMultiple
                    disabled={locked}
                    addLabel={row.addLabel}
                    emptyText={row.emptyText}
                    onChange={(addresses: MailboxAddress[]) => patch(["recipients", row.key], addresses)} />
                  
                  </FormField>
                  <div className="campaign-header-toggle">
                    <ToggleSwitch
                    label={row.toggleLabel}
                    checked={getBool(recipientsSection, row.toggleKey)}
                    disabled={locked}
                    onChange={(checked) => patch(["recipients", row.toggleKey], checked)} />
                  
                  </div>
                </div>
              )}
            </div>
          </Card>

          <Card title="i18n:govoplan-campaign.recipient_profiles.3dc9671c" actions={<Button disabled={locked} onClick={() => setImportOpen(true)}>i18n:govoplan-campaign.import.d6fbc9d2</Button>}>
            {inlineEntries.length === 0 && Boolean(source.type) &&
            <DismissibleAlert tone="info">i18n:govoplan-campaign.this_campaign_references_an_external_recipient_s.bdae9fb4</DismissibleAlert>
            }
            {!source.type &&
            <div className="admin-table-surface recipient-profiles-table-surface">
                <DataGrid
                id={`campaign-${campaignId}-recipient-profiles`}
                rows={inlineEntries}
                columns={recipientProfileColumns({ locked, entries: inlineEntries, entryDefaults, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntryMerge, updateEntry, addRecipient, moveEntry, removeEntry })}
                getRowKey={(entry, index) => String(entry.id || index)}
                emptyText="i18n:govoplan-campaign.no_recipient_profiles_are_stored_in_the_current_.478b8026"
                emptyAction={<DataGridEmptyAction onAdd={() => addRecipient(-1)} disabled={locked} label="i18n:govoplan-campaign.add_first_recipient.e540d52a" />}
                className="recipient-table-wrap recipient-address-table"
                pagination={{
                  page: recipientProfilesPage,
                  pageSize: recipientProfilesPageSize,
                  pageSizeOptions: [10, 25, 50, 100, 250],
                  onPageChange: setRecipientProfilesPage,
                  onPageSizeChange: (pageSize) => {
                    setRecipientProfilesPageSize(pageSize);
                    setRecipientProfilesPage(1);
                  }
                }} />
              
              </div>
            }
          </Card>
        </>
      </LoadingFrame>

      {importOpen &&
      <RecipientImportDialog
        settings={settings}
        campaignId={campaignId}
        existingEntries={inlineEntries}
        existingFields={fieldDefinitions}
        defaultAttachmentBasePath={defaultImportBasePath}
        onCancel={() => setImportOpen(false)}
        onImport={applyRecipientImport} />

      }
    </div>);

}

type RecipientImportDialogProps = {
  settings: ApiSettings;
  campaignId: string;
  existingEntries: Record<string, unknown>[];
  existingFields: ReturnType<typeof getDraftFields>;
  defaultAttachmentBasePath: AttachmentBasePath | null;
  onCancel: () => void;
  onImport: (preview: RecipientImportPreview, mode: RecipientImportMode, provenance?: RecipientImportProvenance | null) => void;
};

type RecipientImportStepId = "upload" | "parse" | "map" | "preview" | "files";

const allRecipientImportSteps: Array<{id: RecipientImportStepId;label: string;}> = [
{ id: "upload", label: "i18n:govoplan-campaign.upload.8bdf057f" },
{ id: "parse", label: "i18n:govoplan-campaign.parse.b7e45a36" },
{ id: "map", label: "i18n:govoplan-campaign.map.ab478f3e" },
{ id: "preview", label: "i18n:govoplan-campaign.preview.f1fbb2b4" },
{ id: "files", label: "i18n:govoplan-campaign.files.6ce6c512" }];


const recipientColumnKindOptions: Array<{value: RecipientColumnKind;label: string;}> = [
{ value: "ignore", label: "i18n:govoplan-campaign.ignore.98f55db5" },
{ value: "id", label: "i18n:govoplan-campaign.id.89f89c02" },
{ value: "active", label: "i18n:govoplan-campaign.active.a733b809" },
{ value: "name", label: "i18n:govoplan-campaign.display_name.c7874aaa" },
{ value: "from", label: "i18n:govoplan-campaign.from.3f66052a" },
{ value: "to", label: "i18n:govoplan-campaign.to.ae79ea1e" },
{ value: "cc", label: "i18n:govoplan-campaign.cc.c5a976de" },
{ value: "bcc", label: "i18n:govoplan-campaign.bcc.4c0145a3" },
{ value: "reply_to", label: "i18n:govoplan-campaign.reply_to.c1733667" },
{ value: "field", label: "i18n:govoplan-campaign.existing_field.681f2b09" },
{ value: "new_field", label: "i18n:govoplan-campaign.new_field.57fc8a2e" },
{ value: "attachment_pattern", label: "i18n:govoplan-campaign.attachment_pattern.59649690" }];


const AUTOMATIC_MAPPING_PROFILE_MIN_SCORE = 0.55;

const RECIPIENT_IMPORT_ENCODINGS = [
{ value: "utf-8", label: "i18n:govoplan-campaign.utf_8.663b90c8" },
{ value: "windows-1252", label: "i18n:govoplan-campaign.windows_1252.6944986d" },
{ value: "iso-8859-1", label: "i18n:govoplan-campaign.iso_8859_1.e4d77380" },
{ value: "utf-16le", label: "i18n:govoplan-campaign.utf_16_le.8d74294c" },
{ value: "utf-16be", label: "i18n:govoplan-campaign.utf_16_be.1de115c7" }];


export function RecipientImportDialog({ settings, campaignId, existingEntries, existingFields, defaultAttachmentBasePath, onCancel, onImport }: RecipientImportDialogProps) {
  const filesFileExplorer = usePlatformUiCapability<FilesFileExplorerUiCapability>("files.fileExplorer");
  const fileLinkCapability = useMemo<ImportFileLinkCapability | null>(() => {
    if (!filesFileExplorer?.listFiles || !filesFileExplorer.resolveFilePatterns || !filesFileExplorer.shareFilesWithTarget) return null;
    return {
      listFiles: filesFileExplorer.listFiles,
      resolveFilePatterns: filesFileExplorer.resolveFilePatterns,
      shareFilesWithTarget: filesFileExplorer.shareFilesWithTarget
    };
  }, [filesFileExplorer]);
  const recipientImportSteps = useMemo(
    () => fileLinkCapability ? allRecipientImportSteps : allRecipientImportSteps.filter((step) => step.id !== "files"),
    [fileLinkCapability]
  );
  const [activeStep, setActiveStep] = useState<RecipientImportStepId>("upload");
  const [csvText, setCsvText] = useState("");
  const [sourceType, setSourceType] = useState<RecipientImportSourceType>("text");
  const [filename, setFilename] = useState("");
  const [fileBuffer, setFileBuffer] = useState<ArrayBuffer | null>(null);
  const [encoding, setEncoding] = useState("utf-8");
  const [workbookSheets, setWorkbookSheets] = useState<RecipientImportSpreadsheetSheet[]>([]);
  const [selectedSheetName, setSelectedSheetName] = useState("");
  const [mode, setMode] = useState<RecipientImportMode>("append");
  const [delimiter, setDelimiter] = useState<CsvDelimiter>("auto");
  const [headerRows, setHeaderRows] = useState(1);
  const [quoted, setQuoted] = useState(true);
  const [valueSeparators, setValueSeparators] = useState(",;|");
  const [mappings, setMappings] = useState<RecipientColumnMapping[]>([]);
  const [mappingProfiles, setMappingProfiles] = useState<RecipientMappingProfile[]>([]);
  const [mappingManuallyChanged, setMappingManuallyChanged] = useState(false);
  const [mappingProfileError, setMappingProfileError] = useState("");
  const [mappingProfileNotice, setMappingProfileNotice] = useState("");
  const [fileError, setFileError] = useState("");
  const [fileLinkResolution, setFileLinkResolution] = useState<ImportFileLinkResolution | null>(null);
  const [fileLinkResolving, setFileLinkResolving] = useState(false);
  const [fileLinking, setFileLinking] = useState(false);
  const [fileLinkError, setFileLinkError] = useState("");
  const [fileLinkNotice, setFileLinkNotice] = useState("");
  const selectedSheet = useMemo(
    () => workbookSheets.find((sheet) => sheet.name === selectedSheetName) ?? workbookSheets[0] ?? null,
    [selectedSheetName, workbookSheets]
  );
  const hasContent = sourceType === "xlsx" ?
  Boolean(selectedSheet && selectedSheet.rows.some((row) => row.some((cell) => cell.trim()))) :
  csvText.trim().length > 0;
  const table = useMemo(
    () => {
      if (!hasContent) return null;
      if (sourceType === "xlsx") return selectedSheet ? buildImportTableFromRows(selectedSheet.rows, { headerRows }) : null;
      return buildImportTable(csvText, { delimiter, headerRows, quoted });
    },
    [csvText, delimiter, hasContent, headerRows, quoted, selectedSheet, sourceType]
  );
  const tableHeaderKey = table ? `${table.delimiter}:${table.firstDataRowNumber}:${table.headers.join("\u001f")}` : "";
  const mappingProfileMatches = useMemo(
    () => table ? matchRecipientMappingProfiles(table, mappingProfiles) : [],
    [mappingProfiles, table]
  );
  const parseReady = Boolean(table && table.dataRows.some((row) => row.some((cell) => cell.trim())));
  const preview = useMemo(
    () => table ? buildRecipientImportPreview(table, mappings, { existingFields, existingEntries, valueSeparators }) : null,
    [existingEntries, existingFields, mappings, table, valueSeparators]
  );
  const activeStepIndex = recipientImportSteps.findIndex((step) => step.id === activeStep);
  const nextStep = recipientImportSteps[activeStepIndex + 1]?.id ?? null;
  const previousStep = recipientImportSteps[activeStepIndex - 1]?.id ?? null;
  const isLastStep = activeStepIndex === recipientImportSteps.length - 1;
  const mappedColumnCount = mappings.filter((mapping) => mapping.kind !== "ignore").length;
  const patternRows = preview?.rows.filter((row) => row.patterns.length > 0).length ?? 0;
  const renderedPatterns = useMemo(() => preview ? renderedImportPatterns(preview) : [], [preview]);

  useEffect(() => {
    let cancelled = false;
    setMappingProfileError("");
    void listRecipientImportMappingProfiles(settings).
    then((profiles) => {
      if (!cancelled) setMappingProfiles(profiles);
    }).
    catch((err) => {
      if (!cancelled) setMappingProfileError(err instanceof Error ? err.message : String(err));
    });
    return () => {cancelled = true;};
  }, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);

  useEffect(() => {
    if (!fileBuffer || sourceType !== "csv") return;
    try {
      setCsvText(decodeImportText(fileBuffer, encoding));
      setFileError("");
    } catch (err) {
      setCsvText("");
      setFileError(err instanceof Error ? err.message : String(err));
    }
  }, [encoding, fileBuffer, sourceType]);

  useEffect(() => {
    setMappingManuallyChanged(false);
  }, [tableHeaderKey]);

  useEffect(() => {
    if (!table) {
      setMappings([]);
      setMappingProfileNotice("");
      return;
    }
    if (mappingManuallyChanged) return;
    const automaticMatch = mappingProfileMatches.find(isAutomaticMappingProfileMatch);
    if (automaticMatch) {
      setMappings(applyRecipientMappingProfile(table, automaticMatch.profile, existingFields));
      setValueSeparators(automaticMatch.profile.valueSeparators || ",;|");
      setMappingProfileNotice(mappingProfileMatchNotice(automaticMatch));
      return;
    }
    setMappings(defaultColumnMappings(table.headers, existingFields));
    setMappingProfileNotice("");
  }, [existingFields, mappingManuallyChanged, mappingProfileMatches, table, tableHeaderKey]);

  useEffect(() => {
    if (!hasContent) setActiveStep("upload");
  }, [hasContent]);

  useEffect(() => {
    if (activeStep === "files" && !fileLinkCapability) setActiveStep("preview");
  }, [activeStep, fileLinkCapability]);

  useEffect(() => {
    if (activeStep === "files" && fileLinkCapability) void refreshFileLinks();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeStep, defaultAttachmentBasePath?.id, defaultAttachmentBasePath?.path, defaultAttachmentBasePath?.source, fileLinkCapability, renderedPatterns.length]);

  async function readFile(file: File | undefined) {
    if (!file) return;
    setFilename(file.name);
    setFileError("");
    setWorkbookSheets([]);
    setSelectedSheetName("");
    try {
      const buffer = await file.arrayBuffer();
      if (isXlsxFile(file)) {
        const sheets = await xlsxSheetsFromArrayBuffer(buffer);
        if (sheets.length === 0) {
          throw new Error("i18n:govoplan-campaign.workbook_contains_no_readable_sheets.02c18603");
        }
        setSourceType("xlsx");
        setFileBuffer(null);
        setCsvText("");
        setWorkbookSheets(sheets);
        setSelectedSheetName(sheets[0]?.name ?? "");
      } else {
        setSourceType("csv");
        setFileBuffer(buffer);
        setCsvText(decodeImportText(buffer, encoding));
      }
      setActiveStep("parse");
    } catch (err) {
      setFileError(err instanceof Error ? err.message : String(err));
    }
  }

  function canOpenStep(stepId: RecipientImportStepId): boolean {
    if (stepId === "upload") return true;
    if (stepId === "parse") return hasContent;
    if (stepId === "map") return parseReady;
    if (stepId === "files" && !fileLinkCapability) return false;
    return parseReady && mappings.length > 0;
  }

  function stepStatus(stepId: RecipientImportStepId): string {
    if (stepId === "upload") return filename || (hasContent ? "i18n:govoplan-campaign.pasted_data.5b58080b" : "i18n:govoplan-campaign.waiting.33d30632");
    if (stepId === "parse") return table ? `${table.dataRows.length} rows` : "i18n:govoplan-campaign.set_parsing.be7b05fd";
    if (stepId === "map") return `${mappedColumnCount} mapped`;
    if (stepId === "preview") return preview ? `${preview.validCount} valid` : "i18n:govoplan-campaign.review.e29a79fe";
    if (!preview || preview.patternCount === 0) return "i18n:govoplan-campaign.no_patterns.5e1e46fa";
    if (fileLinkResolving) return "i18n:govoplan-campaign.checking.97876b83";
    if (fileLinkResolution) return i18nMessage("i18n:govoplan-campaign.value_to_link.50cdc659", { value0: fileLinkResolution.linkableFiles.length });
    if (fileLinkError) return "i18n:govoplan-campaign.needs_setup.522ebae4";
    return `${renderedPatterns.length} patterns`;
  }

  function goNext() {
    if (nextStep && canOpenStep(nextStep)) setActiveStep(nextStep);
  }

  async function refreshFileLinks() {
    setFileLinkNotice("");
    if (!fileLinkCapability) {
      setFileLinkResolution(null);
      setFileLinkError("");
      return;
    }
    if (!preview || preview.patternCount === 0) {
      setFileLinkResolution(null);
      setFileLinkError("");
      return;
    }
    if (!defaultAttachmentBasePath?.source) {
      setFileLinkResolution(null);
      setFileLinkError("i18n:govoplan-campaign.the_selected_attachment_source_is_not_connected_.9c528afa");
      return;
    }
    setFileLinkResolving(true);
    setFileLinkError("");
    try {
      setFileLinkResolution(await resolveImportedAttachmentLinks(fileLinkCapability, settings, campaignId, defaultAttachmentBasePath, preview));
    } catch (err) {
      setFileLinkResolution(null);
      setFileLinkError(err instanceof Error ? err.message : String(err));
    } finally {
      setFileLinkResolving(false);
    }
  }

  async function linkImportedFiles() {
    if (!fileLinkCapability || !fileLinkResolution || fileLinkResolution.linkableFiles.length === 0) return;
    setFileLinking(true);
    setFileLinkError("");
    setFileLinkNotice("");
    try {
      const linkedCount = await bulkLinkFilesToCampaign(fileLinkCapability, settings, campaignId, fileLinkResolution.linkableFiles);
      await refreshFileLinks();
      setFileLinkNotice(`Linked ${linkedCount} file${linkedCount === 1 ? "" : "s"} to this campaign.`);
    } catch (err) {
      setFileLinkError(err instanceof Error ? err.message : String(err));
    } finally {
      setFileLinking(false);
    }
  }

  function rememberCurrentMappingProfile() {
    if (!table || mappings.length === 0) return;
    const tableSnapshot = table;
    const mappingsSnapshot = mappings;
    const fallbackProfiles = mappingProfiles;
    const filenameSnapshot = filename;
    const parseOptions = { headerRows, quoted };
    const valueSeparatorsSnapshot = valueSeparators;

    void (async () => {
      try {
        const latestProfiles = await listRecipientImportMappingProfiles(settings).catch(() => fallbackProfiles);
        const reusableProfile = findReusableMappingProfile(tableSnapshot, latestProfiles);
        const draftProfile = buildRecipientMappingProfile({
          id: reusableProfile?.id,
          name: reusableProfile?.name ?? defaultMappingProfileName(filenameSnapshot, tableSnapshot),
          table: tableSnapshot,
          mappings: mappingsSnapshot,
          parseOptions,
          valueSeparators: valueSeparatorsSnapshot,
          createdAt: reusableProfile?.createdAt,
          updatedAt: new Date().toISOString()
        });
        const payload = mappingProfilePayload(draftProfile);
        await (reusableProfile ?
        updateRecipientImportMappingProfile(settings, reusableProfile.id, payload) :
        createRecipientImportMappingProfile(settings, payload));
      } catch {





        // Import acceptance must not be blocked by passive profile learning.
      }})();}function confirmRecipientImport(nextPreview: RecipientImportPreview) {rememberCurrentMappingProfile();
    const provenance = createRecipientImportProvenance({
      preview: nextPreview,
      mappings,
      mode,
      sourceType,
      filename,
      sheetName: sourceType === "xlsx" ? selectedSheet?.name ?? null : null,
      encoding: sourceType === "xlsx" ? null : encoding,
      delimiter: sourceType === "xlsx" ? null : formatDelimiter(nextPreview.table.delimiter),
      headerRows,
      quoted: sourceType === "xlsx" ? null : quoted,
      valueSeparators
    });
    onImport(nextPreview, mode, provenance);
  }

  function replaceColumnMapping(nextMapping: RecipientColumnMapping) {
    const columnCount = table?.headers.length ?? 0;
    setMappingManuallyChanged(true);
    setMappings((current) => {
      const byColumn = new Map(current.map((mapping) => [mapping.columnIndex, mapping]));
      byColumn.set(nextMapping.columnIndex, nextMapping);
      return Array.from({ length: columnCount }, (_value, columnIndex) => byColumn.get(columnIndex) ?? { columnIndex, kind: "ignore" });
    });
  }

  function changeColumnKind(columnIndex: number, kind: RecipientColumnKind) {
    const header = table?.headers[columnIndex] ?? `Column ${columnIndex + 1}`;
    const current = mappings.find((mapping) => mapping.columnIndex === columnIndex);
    const nextMapping: RecipientColumnMapping = { columnIndex, kind };
    if (kind === "field") nextMapping.fieldName = current?.fieldName || existingFields[0]?.name || "";
    if (kind === "new_field") nextMapping.newFieldName = current?.newFieldName || suggestImportFieldName(header);
    replaceColumnMapping(nextMapping);
  }

  const stepContent = activeStep === "upload" ?
  <>
      <div className="campaign-header-grid recipient-import-upload-grid">
        <FormField label="i18n:govoplan-campaign.recipient_file.693007d2">
          <FileDropZone
          accept=".csv,.tsv,.txt,.xlsx,text/csv,text/tab-separated-values,text/plain,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
          multiple={false}
          label="i18n:govoplan-campaign.drop_recipient_file_here.c7c2bddf"
          actionLabel="i18n:govoplan-campaign.or_click_to_select_file.0e72f25d"
          note={filename || "i18n:govoplan-campaign.csv_tsv_text_or_xlsx.5dcdac76"}
          onFiles={(files) => readFile(files[0])} />
        
        </FormField>
        <FormField label="i18n:govoplan-campaign.import_mode.7d161dff">
          <select value={mode} onChange={(event) => setMode(event.target.value === "replace" ? "replace" : "append")}>
            <option value="append">i18n:govoplan-campaign.append_to_current_profiles.0b434d6f</option>
            <option value="replace">i18n:govoplan-campaign.replace_current_profiles.3b3be5e2</option>
          </select>
        </FormField>
      </div>
      {fileError && <DismissibleAlert tone="danger" compact resetKey={fileError}>{fileError}</DismissibleAlert>}
      {sourceType === "xlsx" ?
    <DismissibleAlert tone="info" compact dismissible={false}>
          i18n:govoplan-campaign.workbook_loaded.cfaf40eb {workbookSheets.length} sheet{workbookSheets.length === 1 ? "" : "s"}.
        </DismissibleAlert> :

    <FormField label={filename ? i18nMessage("i18n:govoplan-campaign.raw_content_value.15e098b8", { value0: filename }) : "i18n:govoplan-campaign.raw_content.c687fb57"}>
          <textarea
        className="json-editor recipient-import-textarea"
        value={csvText}
        onChange={(event) => {
          setFileBuffer(null);
          setSourceType(filename ? "csv" : "text");
          setCsvText(event.target.value);
          if (!event.target.value.trim()) setFilename("");
        }}
        spellCheck={false} />
      
        </FormField>
    }
    </> :
  activeStep === "parse" ?
  <>
      <div className="campaign-header-grid">
        {sourceType === "xlsx" &&
      <FormField label="i18n:govoplan-campaign.sheet.53bc47a7">
            <select value={selectedSheet?.name ?? ""} onChange={(event) => setSelectedSheetName(event.target.value)}>
              {workbookSheets.map((sheet) =>
          <option key={sheet.name} value={sheet.name}>{sheet.name}</option>
          )}
            </select>
          </FormField>
      }
        <FormField label="i18n:govoplan-campaign.header_rows.9814b9e3">
          <input
          type="number"
          min={0}
          max={10}
          value={headerRows}
          onChange={(event) => setHeaderRows(Math.max(0, Number.parseInt(event.target.value, 10) || 0))} />
        
        </FormField>
        {sourceType !== "xlsx" &&
      <>
            <FormField label="i18n:govoplan-campaign.encoding.5821fec7">
              <select value={encoding} onChange={(event) => setEncoding(event.target.value)}>
                {RECIPIENT_IMPORT_ENCODINGS.map((option) =>
            <option key={option.value} value={option.value}>{option.label}</option>
            )}
              </select>
            </FormField>
            <FormField label="i18n:govoplan-campaign.separator.b4b289a7">
              <select value={delimiter} onChange={(event) => setDelimiter(event.target.value as CsvDelimiter)}>
                <option value="auto">i18n:govoplan-campaign.auto.c614ba7c</option>
                <option value=",">i18n:govoplan-campaign.comma.b9ee3dea</option>
                <option value=";">i18n:govoplan-campaign.semicolon.727ceca9</option>
                <option value={"\t"}>i18n:govoplan-campaign.tab.fe06eb64</option>
              </select>
            </FormField>
            <div className="campaign-header-toggle recipient-import-toggles">
              <ToggleSwitch label="i18n:govoplan-campaign.quoted_contents.f7fd335a" checked={quoted} onChange={setQuoted} />
            </div>
          </>
      }
      </div>
      {table &&
    <>
          <dl className="detail-list recipient-import-summary">
            <div><dt>{sourceType === "xlsx" ? "i18n:govoplan-campaign.sheet.53bc47a7" : "i18n:govoplan-campaign.separator.b4b289a7"}</dt><dd>{sourceType === "xlsx" ? selectedSheet?.name ?? "i18n:govoplan-campaign.workbook.b4418e62" : formatDelimiter(table.delimiter)}</dd></div>
            <div><dt>i18n:govoplan-campaign.header_rows.9814b9e3</dt><dd>{table.headerRows.length}</dd></div>
            <div><dt>i18n:govoplan-campaign.data_rows.3734724c</dt><dd>{table.dataRows.length}</dd></div>
            <div><dt>i18n:govoplan-campaign.columns.cf723c59</dt><dd>{table.headers.length}</dd></div>
          </dl>
          <RecipientImportRawTable tableRows={table.rows} headerRowCount={table.headerRows.length} columnCount={table.headers.length} />
        </>
    }
    </> :
  activeStep === "map" ?
  <>
      {mappingProfileError && <DismissibleAlert tone="warning" compact resetKey={mappingProfileError}>{mappingProfileError}</DismissibleAlert>}
      {mappingProfileNotice && <DismissibleAlert tone="info" compact resetKey={mappingProfileNotice}>{mappingProfileNotice}</DismissibleAlert>}
      <div className="campaign-header-grid recipient-import-map-controls">
        <FormField label="i18n:govoplan-campaign.multi_value_separators.6dd66f70">
          <input
          value={valueSeparators}
          onChange={(event) => {
            setMappingManuallyChanged(true);
            setValueSeparators(event.target.value);
          }} />
        
        </FormField>
        <FormField label="i18n:govoplan-campaign.mode.a7b93d21">
          <select value={mode} onChange={(event) => setMode(event.target.value === "replace" ? "replace" : "append")}>
            <option value="append">i18n:govoplan-campaign.append.6b3a6022</option>
            <option value="replace">i18n:govoplan-campaign.replace.a7cf7b25</option>
          </select>
        </FormField>
      </div>
      <div className="admin-table-surface recipient-import-preview-surface">
        <table className="recipient-import-preview-table recipient-import-mapping-table">
          <thead>
            <tr>
              <th>i18n:govoplan-campaign.column.65ba00e9</th>
              <th>i18n:govoplan-campaign.sample.58fabfa7</th>
              <th>i18n:govoplan-campaign.content.4f9be057</th>
              <th>i18n:govoplan-campaign.field.c326a466</th>
            </tr>
          </thead>
          <tbody>
            {table?.headers.map((header, columnIndex) => {
            const mapping = mappings.find((item) => item.columnIndex === columnIndex) ?? { columnIndex, kind: "ignore" as const };
            return (
              <tr key={`${columnIndex}-${header}`}>
                  <td><strong>{header}</strong></td>
                  <td className="mono-small">{sampleColumnValue(table.dataRows, columnIndex)}</td>
                  <td>
                    <select value={mapping.kind} onChange={(event) => changeColumnKind(columnIndex, event.target.value as RecipientColumnKind)}>
                      {recipientColumnKindOptions.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
                    </select>
                  </td>
                  <td>
                    {mapping.kind === "field" &&
                  <select
                    value={mapping.fieldName ?? ""}
                    onChange={(event) => replaceColumnMapping({ ...mapping, fieldName: event.target.value, newFieldName: undefined })}>
                    
                        <option value="">i18n:govoplan-campaign.select_field.bb7e63d5</option>
                        {existingFields.map((field) => <option key={field.name} value={field.name}>{field.label || field.name}</option>)}
                      </select>
                  }
                    {mapping.kind === "new_field" &&
                  <input
                    value={mapping.newFieldName ?? ""}
                    onChange={(event) => replaceColumnMapping({ ...mapping, newFieldName: event.target.value, fieldName: undefined })} />

                  }
                  </td>
                </tr>);

          })}
          </tbody>
        </table>
      </div>
    </> :
  activeStep === "preview" ?
  <>
      {preview &&
    <>
          <dl className="detail-list recipient-import-summary">
            <div><dt>i18n:govoplan-campaign.rows.52d0b352</dt><dd>{preview.validCount} i18n:govoplan-campaign.valid.210cece7 {preview.invalidCount} invalid</dd></div>
            <div><dt>i18n:govoplan-campaign.fields.e8b68527</dt><dd>{preview.fieldNamesToCreate.length} new</dd></div>
            <div><dt>i18n:govoplan-campaign.patterns.4d34f7a2</dt><dd>{preview.patternCount} from {patternRows} i18n:govoplan-campaign.row_s.61464061</dd></div>
            <div><dt>i18n:govoplan-campaign.source.6da13add</dt><dd>{preview.patternCount ? defaultAttachmentBasePath?.name ?? "i18n:govoplan-campaign.campaign_files.96e7004b" : "i18n:govoplan-campaign.no_patterns.5e1e46fa"}</dd></div>
          </dl>
          {preview.fieldNamesToCreate.length > 0 &&
      <p className="muted small-note">i18n:govoplan-campaign.new_fields.c61e20c0 {preview.fieldNamesToCreate.join(", ")}</p>
      }
          <div className="admin-table-surface recipient-import-preview-surface">
            <table className="recipient-import-preview-table">
              <thead>
                <tr>
                  <th>#</th>
                  <th>i18n:govoplan-campaign.to.ae79ea1e</th>
                  <th>i18n:govoplan-campaign.name.709a2322</th>
                  <th>i18n:govoplan-campaign.fields.e8b68527</th>
                  <th>i18n:govoplan-campaign.patterns.4d34f7a2</th>
                  <th>i18n:govoplan-campaign.status.bae7d5be</th>
                </tr>
              </thead>
              <tbody>
                {preview.rows.slice(0, 20).map((row) =>
            <tr key={row.rowNumber} className={row.issues.length ? "is-invalid" : undefined}>
                    <td>{row.rowNumber}</td>
                    <td>{formatAddressList(row.addresses.to)}</td>
                    <td>{row.name}</td>
                    <td>{Object.keys(row.fields).length}</td>
                    <td>{row.patterns.length}</td>
                    <td>{row.issues.length ? row.issues.join(", ") : "i18n:govoplan-campaign.ready.20c7c552"}</td>
                  </tr>
            )}
              </tbody>
            </table>
          </div>
        </>
    }
    </> :

  <RecipientImportFileLinkStep
    preview={preview}
    basePath={defaultAttachmentBasePath}
    resolution={fileLinkResolution}
    resolving={fileLinkResolving}
    linking={fileLinking}
    error={fileLinkError}
    notice={fileLinkNotice}
    onRefresh={() => void refreshFileLinks()}
    onLink={() => void linkImportedFiles()} />;



  return (
    <Dialog
      open
      title="i18n:govoplan-campaign.import_recipients.9d6dbe45"
      className="recipient-import-modal"
      bodyClassName="recipient-import-body"
      closeDisabled={fileLinking || fileLinkResolving}
      closeOnBackdrop={!fileLinking && !fileLinkResolving}
      onClose={onCancel}
      footer={
      <>
          <Button onClick={onCancel} disabled={fileLinking || fileLinkResolving}>i18n:govoplan-campaign.cancel.77dfd213</Button>
          {previousStep && <Button onClick={() => setActiveStep(previousStep)} disabled={fileLinking || fileLinkResolving}>i18n:govoplan-campaign.back.b52b36b7</Button>}
          {!isLastStep ?
        <Button variant="primary" disabled={!nextStep || !canOpenStep(nextStep)} onClick={goNext}>i18n:govoplan-campaign.next.bc981983</Button> :

        <Button variant="primary" disabled={!preview || preview.validCount === 0 || fileLinking || fileLinkResolving} onClick={() => preview && confirmRecipientImport(preview)}>i18n:govoplan-campaign.import_valid_rows.c3b2642b</Button>
        }
        </>
      }>
      
      <nav className="recipient-import-steps" aria-label="i18n:govoplan-campaign.recipient_import_steps.35dcf028">
        <div className="recipient-import-step-track">
          {recipientImportSteps.map((step, index) => {
            const stepIndex = recipientImportSteps.findIndex((item) => item.id === step.id);
            const stepState = stepIndex < activeStepIndex ? "is-complete" : step.id === activeStep ? "is-current" : "";
            return (
              <div className="recipient-import-step-group" key={step.id}>
                <button
                  type="button"
                  className={`recipient-import-step-item ${stepState}`.trim()}
                  disabled={!canOpenStep(step.id)}
                  aria-current={step.id === activeStep ? "step" : undefined}
                  onClick={() => setActiveStep(step.id)}>
                  
                  <span className="recipient-import-step-icon">{index + 1}</span>
                  <span className="recipient-import-step-copy">
                    <strong>{step.label}</strong>
                    <small>{stepStatus(step.id)}</small>
                  </span>
                </button>
                {index < recipientImportSteps.length - 1 && <span className="recipient-import-step-line" aria-hidden="true" />}
              </div>);

          })}
        </div>
      </nav>
      <section className="recipient-import-step-panel">
        {stepContent}
      </section>
    </Dialog>);

}

type RecipientImportFileLinkStepProps = {
  preview: RecipientImportPreview | null;
  basePath: AttachmentBasePath | null;
  resolution: ImportFileLinkResolution | null;
  resolving: boolean;
  linking: boolean;
  error: string;
  notice: string;
  onRefresh: () => void;
  onLink: () => void;
};

function RecipientImportFileLinkStep({ preview, basePath, resolution, resolving, linking, error, notice, onRefresh, onLink }: RecipientImportFileLinkStepProps) {
  const linkableIds = new Set(resolution?.linkableFiles.map((file) => file.id) ?? []);
  if (!preview || preview.patternCount === 0) {
    return <DismissibleAlert tone="info" dismissible={false}>i18n:govoplan-campaign.no_attachment_patterns_were_imported_there_are_n.c01c8266</DismissibleAlert>;
  }

  return (
    <div className="recipient-import-file-step">
      <div className="recipient-import-file-actions">
        <div>
          <strong>{basePath?.name ?? "i18n:govoplan-campaign.campaign_files.96e7004b"}</strong>
          <p className="muted small-note">i18n:govoplan-campaign.files_matched_by_imported_recipient_attachment_p.9b97aca3</p>
        </div>
        <div className="button-row compact-actions">
          <Button onClick={onRefresh} disabled={resolving || linking}>{resolving ? "i18n:govoplan-campaign.checking.820d6004" : "i18n:govoplan-campaign.refresh_matches.11e36411"}</Button>
          <Button variant="primary" onClick={onLink} disabled={resolving || linking || !resolution || resolution.linkableFiles.length === 0}>{linking ? "i18n:govoplan-campaign.linking.6f640897" : i18nMessage("i18n:govoplan-campaign.link_value_file_s.ca800d96", { value0: resolution?.linkableFiles.length ?? 0 })}</Button>
        </div>
      </div>

      {error && <DismissibleAlert tone="warning" compact resetKey={error}>{error}</DismissibleAlert>}
      {notice && <DismissibleAlert tone="success" compact resetKey={notice}>{notice}</DismissibleAlert>}
      {resolving && <DismissibleAlert tone="info" compact dismissible={false}>i18n:govoplan-campaign.resolving_imported_file_patterns.3aea140f</DismissibleAlert>}

      {resolution &&
      <>
          <dl className="detail-list recipient-import-summary">
            <div><dt>i18n:govoplan-campaign.patterns.4d34f7a2</dt><dd>{resolution.patterns.length}</dd></div>
            <div><dt>i18n:govoplan-campaign.matched_files.f79c63bb</dt><dd>{resolution.files.length}</dd></div>
            <div><dt>i18n:govoplan-campaign.already_linked.7d6c3dd5</dt><dd>{resolution.linkedFiles.length}</dd></div>
            <div><dt>i18n:govoplan-campaign.need_linking.a7617722</dt><dd>{resolution.linkableFiles.length}</dd></div>
          </dl>

          {resolution.unmatchedPatterns.length > 0 &&
        <div className="recipient-import-unmatched-patterns">
              <strong>i18n:govoplan-campaign.patterns_without_matches.172afdde</strong>
              <ul>
                {resolution.unmatchedPatterns.slice(0, 8).map((pattern) =>
            <li key={pattern.key}>i18n:govoplan-campaign.row.9bf7a8e8 {pattern.rowNumber}: <code>{pattern.renderedPattern}</code></li>
            )}
              </ul>
              {resolution.unmatchedPatterns.length > 8 && <p className="muted small-note">{resolution.unmatchedPatterns.length - 8} i18n:govoplan-campaign.more_unmatched_pattern_s.1dda9807</p>}
            </div>
        }

          <div className="admin-table-surface recipient-import-preview-surface">
            <table className="recipient-import-preview-table recipient-import-file-link-table">
              <thead>
                <tr>
                  <th>i18n:govoplan-campaign.file.2c3cafa4</th>
                  <th>i18n:govoplan-campaign.path.519e3913</th>
                  <th>i18n:govoplan-campaign.size.b7152342</th>
                  <th>i18n:govoplan-campaign.status.bae7d5be</th>
                </tr>
              </thead>
              <tbody>
                {resolution.files.length === 0 ?
              <tr><td colSpan={4}>i18n:govoplan-campaign.no_files_currently_match_the_imported_patterns.6b599e8b</td></tr> :
              resolution.files.map((file) =>
              <tr key={file.id}>
                    <td><strong>{file.filename}</strong></td>
                    <td>{file.display_path}</td>
                    <td>{formatImportBytes(file.size_bytes)}</td>
                    <td>{linkableIds.has(file.id) ? "i18n:govoplan-campaign.needs_linking.a0fc8341" : "i18n:govoplan-campaign.linked.a089f600"}</td>
                  </tr>
              )}
              </tbody>
            </table>
          </div>
        </>
      }
    </div>);

}

type RecipientImportRawTableProps = {
  tableRows: string[][];
  headerRowCount: number;
  columnCount: number;
};

function RecipientImportRawTable({ tableRows, headerRowCount, columnCount }: RecipientImportRawTableProps) {
  const visibleRows = tableRows.slice(0, 15);
  const safeColumnCount = Math.max(1, columnCount, ...visibleRows.map((row) => row.length));
  return (
    <div className="admin-table-surface recipient-import-preview-surface">
      <table className="recipient-import-preview-table recipient-import-raw-table">
        <thead>
          <tr>
            <th>#</th>
            {Array.from({ length: safeColumnCount }, (_value, index) => <th key={index}>{index + 1}</th>)}
          </tr>
        </thead>
        <tbody>
          {visibleRows.length === 0 ?
          <tr><td colSpan={safeColumnCount + 1}>i18n:govoplan-campaign.no_rows_parsed.a7ccc3de</td></tr> :
          visibleRows.map((row, rowIndex) =>
          <tr key={rowIndex} className={rowIndex < headerRowCount ? "is-header-row" : undefined}>
              <td>{rowIndex + 1}</td>
              {Array.from({ length: safeColumnCount }, (_value, columnIndex) => <td key={columnIndex}>{row[columnIndex] ?? ""}</td>)}
            </tr>
          )}
        </tbody>
      </table>
    </div>);

}

function formatDelimiter(delimiter: "," | ";" | "\t"): string {
  if (delimiter === "\t") return "i18n:govoplan-campaign.tab.fe06eb64";
  if (delimiter === ";") return "i18n:govoplan-campaign.semicolon.727ceca9";
  return "i18n:govoplan-campaign.comma.b9ee3dea";
}

function isXlsxFile(file: File): boolean {
  const name = file.name.toLowerCase();
  return name.endsWith(".xlsx") || file.type === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}

function decodeImportText(buffer: ArrayBuffer, encoding: string): string {
  try {
    return new TextDecoder(encoding).decode(buffer).replace(/^\uFEFF/, "");
  } catch (err) {
    throw new Error(`Could not decode file as ${encoding}: ${err instanceof Error ? err.message : String(err)}`);
  }
}

function mappingProfilePayload(profile: RecipientMappingProfile): RecipientImportMappingProfilePayload {
  return {
    name: profile.name,
    columnCount: profile.columnCount,
    headers: profile.headers,
    normalizedHeaders: profile.normalizedHeaders,
    orderedHeaderFingerprint: profile.orderedHeaderFingerprint,
    unorderedHeaderFingerprint: profile.unorderedHeaderFingerprint,
    delimiter: profile.delimiter,
    headerRows: profile.headerRows,
    quoted: profile.quoted,
    valueSeparators: profile.valueSeparators,
    mappings: profile.mappings
  };
}

function defaultMappingProfileName(filename: string, table: {headers: string[];}): string {
  const trimmedFilename = filename.trim().replace(/\.[^.]+$/, "");
  if (trimmedFilename) return trimmedFilename;
  const headerLabel = table.headers.slice(0, 3).map((header) => header.trim()).filter(Boolean).join(", ");
  return headerLabel ? i18nMessage("i18n:govoplan-campaign.mapping_value", { value0: headerLabel }) : "i18n:govoplan-campaign.recipient_import_mapping.5fe80f6c";
}

function formatMappingProfileMatch(match: RecipientMappingProfileMatch): string {
  if (match.mode === "ordered") return "i18n:govoplan-campaign.exact";
  if (match.mode === "unordered") return "i18n:govoplan-campaign.same_headers.e523bbe4";
  return `${Math.round(match.score * 100)}%`;
}

function isAutomaticMappingProfileMatch(match: RecipientMappingProfileMatch): boolean {
  return match.mode === "ordered" || match.mode === "unordered" || match.score >= AUTOMATIC_MAPPING_PROFILE_MIN_SCORE;
}

function findReusableMappingProfile(table: {headers: string[];}, profiles: RecipientMappingProfile[]): RecipientMappingProfile | null {
  const fingerprints = recipientImportHeaderFingerprints(table.headers);
  return profiles.find((profile) => profile.orderedHeaderFingerprint === fingerprints.orderedHeaderFingerprint) ??
  profiles.find((profile) => profile.unorderedHeaderFingerprint === fingerprints.unorderedHeaderFingerprint) ??
  null;
}

function mappingProfileMatchNotice(match: RecipientMappingProfileMatch): string {
  if (match.mode === "ordered") return i18nMessage("i18n:govoplan-campaign.using_mapping_from_previous_import_value.f86f53fd", { value0: match.profile.name });
  if (match.mode === "unordered") return i18nMessage("i18n:govoplan-campaign.using_mapping_from_previous_import_value_matched.08910120", { value0: match.profile.name });
  return i18nMessage("i18n:govoplan-campaign.using_mapping_derived_from_previous_import_value.008a66a3", { value0: match.profile.name, value1: formatMappingProfileMatch(match) });
}

function sampleColumnValue(rows: string[][], columnIndex: number): string {
  return rows.map((row) => String(row[columnIndex] ?? "").trim()).find(Boolean) ?? "";
}

function formatAddressList(addresses: ImportedAddress[]): string {
  return addresses.map((address) => address.name ? `${address.name} <${address.email}>` : address.email).join(", ");
}

function formatImportBytes(value?: number | null): string {
  if (!value) return "";
  if (value < 1024) return i18nMessage("i18n:govoplan-campaign.bytes_b", { value0: value });
  if (value < 1024 * 1024) return i18nMessage("i18n:govoplan-campaign.bytes_kb", { value0: (value / 1024).toFixed(1) });
  return i18nMessage("i18n:govoplan-campaign.bytes_mb", { value0: (value / 1024 / 1024).toFixed(1) });
}


function suggestImportFieldName(value: string): string {
  const cleaned = value.trim().replace(/^fields[._-]+/i, "").replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "");
  return cleaned || "field";
}

type RecipientProfileColumnContext = {
  locked: boolean;
  entries: Record<string, unknown>[];
  entryDefaults: Record<string, unknown>;
  entryAddressColumns: EntryAddressColumn[];
  addressSuggestions: MailboxAddress[];
  updateEntryAddressList: (index: number, key: EntryAddressColumn["key"], addresses: MailboxAddress[]) => void;
  updateEntryMerge: (index: number, mergeKey: NonNullable<EntryAddressColumn["mergeKey"]>, checked: boolean) => void;
  updateEntry: (index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) => void;
  addRecipient: (afterIndex?: number) => void;
  moveEntry: (index: number, targetIndex: number) => void;
  removeEntry: (index: number) => void;
};

function recipientProfileColumns({ locked, entries, entryDefaults, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntryMerge, updateEntry, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
  return [
  { id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", render: (_entry, index) => <span className="mono-small">{index + 1}</span>, value: (_entry, index) => index + 1 },
  ...entryAddressColumns.map((column): DataGridColumn<Record<string, unknown>> => ({
    id: column.key,
    header: column.label,
    width: column.key === "to" ? "minmax(260px, 1.2fr)" : 250,
    resizable: true,
    filterable: true,
    render: (entry, index) =>
    <div className="recipient-address-cell">
          <EmailAddressInput
        value={getEntryAddresses(entry, column.key)}
        suggestions={addressSuggestions}
        allowMultiple={column.allowMultiple}
        compact
        disabled={locked}
        addLabel={column.addLabel}
        emptyText={column.emptyText}
        onChange={(addresses) => updateEntryAddressList(index, column.key, addresses)} />
      
          {column.mergeKey &&
      <div className="recipient-address-merge">
              <ToggleSwitch
          label="i18n:govoplan-campaign.merge.ea8f0d02"
          checked={getEntryMerge(entry, entryDefaults, column.mergeKey)}
          disabled={locked}
          onChange={(checked) => updateEntryMerge(index, column.mergeKey!, checked)} />
        
              <span className="muted">i18n:govoplan-campaign.merge_with_global.ba230098 {column.label}</span>
            </div>
      }
        </div>,

    value: (entry) => getEntryAddresses(entry, column.key).map((address) => `${address.name ?? ""} ${address.email ?? ""}`).join(", ")
  })),
  { id: "active", header: "i18n:govoplan-campaign.active.a733b809", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "active", label: "i18n:govoplan-campaign.active.a733b809" }, { value: "inactive", label: "i18n:govoplan-campaign.inactive.09af574c" }] }, render: (entry, index) => <ToggleSwitch label="i18n:govoplan-campaign.active.a733b809" checked={entry.active !== false} disabled={locked} onChange={(checked) => updateEntry(index, (current) => ({ ...current, active: checked }))} />, value: (entry) => entry.active !== false ? "active" : "inactive" },
  {
    id: "actions",
    header: "i18n:govoplan-campaign.actions.c3cd636a",
    width: 180,
    sticky: "end",
    render: (_entry, index) =>
    <DataGridRowActions
      disabled={locked}
      onAddBelow={() => addRecipient(index)}
      onRemove={() => removeEntry(index)}
      onMoveUp={index > 0 ? () => moveEntry(index, index - 1) : undefined}
      onMoveDown={index < entries.length - 1 ? () => moveEntry(index, index + 1) : undefined}
      addLabel="i18n:govoplan-campaign.add_recipient_below.52fb47d8"
      removeLabel="i18n:govoplan-campaign.remove_recipient.d1bc9f53"
      moveUpLabel="i18n:govoplan-campaign.move_recipient_up.90c6bccc"
      moveDownLabel="i18n:govoplan-campaign.move_recipient_down.ead5466c" />


  }];

}

function getEntryMerge(entry: Record<string, unknown>, defaults: Record<string, unknown>, mergeKey: NonNullable<EntryAddressColumn["mergeKey"]>): boolean {
  const legacyKey = mergeKey.replace("merge_", "combine_");
  if (typeof entry[mergeKey] === "boolean") return entry[mergeKey] as boolean;
  if (typeof entry[legacyKey] === "boolean") return entry[legacyKey] as boolean;
  if (typeof defaults[mergeKey] === "boolean") return defaults[mergeKey] as boolean;
  if (typeof defaults[legacyKey] === "boolean") return defaults[legacyKey] as boolean;
  return true;
}

function getEntryAddresses(entry: Record<string, unknown>, key: EntryAddressColumn["key"]): MailboxAddress[] {
  if (key === "to") {
    const explicit = addressesFromValue(entry.to);
    return explicit.length ? explicit : fallbackRecipientAddress(entry);
  }
  if (key === "from") return addressesFromValue(entry.from).slice(0, 1);
  if (key === "reply_to") return addressesFromValue(entry.reply_to);
  return addressesFromValue(entry[key]);
}

function fallbackRecipientAddress(entry: Record<string, unknown>): MailboxAddress[] {
  const direct = addressesFromValue(entry.recipient)[0] ?? addressesFromValue(entry)[0];
  return direct?.email ? [direct] : [];
}

function uniqueEntryId(entries: Record<string, unknown>[], preferred: string): string {
  const existing = new Set(entries.map((entry) => String(entry.id || "")).filter(Boolean));
  if (!existing.has(preferred)) return preferred;
  let counter = entries.length + 1;
  let candidate = `recipient-${counter}`;
  while (existing.has(candidate)) {
    counter += 1;
    candidate = `recipient-${counter}`;
  }
  return candidate;
}
