import React, { useCallback, useEffect, useRef, useState } from 'react';
import { sessionFilesApi } from '../api/client';

// ── Helpers ───────────────────────────────────────────────────────────────────

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

/**
 * Build a regex that matches an animal's identifier in a filename.
 * Tries both animal_id_string and animal_name (OR logic).
 *
 * Rules:
 *  - Match must not be preceded or followed by an alphanumeric char,
 *    so "2852" matches "rat_2852_session" but not "28520" or "X2852Y".
 *  - Multi-word names ("Rat A") allow any separator (space, dash, underscore)
 *    between words, so "Rat A" matches "rat_a_session" and "Rat-A-data".
 */
function buildAnimalRegex(animal) {
  const patterns = [];

  if (animal.animal_id_string) {
    patterns.push(escapeRegex(animal.animal_id_string));
  }

  if (animal.animal_name) {
    const words = animal.animal_name.trim().split(/\s+/).map(escapeRegex);
    // Single-word name: use as-is. Multi-word: allow space/dash/underscore between words.
    patterns.push(words.length === 1 ? words[0] : words.join('[\\s_\\-]+'));
  }

  if (patterns.length === 0) return null;
  return new RegExp(`(?<![a-zA-Z0-9])(${patterns.join('|')})(?![a-zA-Z0-9])`, 'i');
}

function classifyFile(filename) {
  const ext = filename.split('.').pop().toLowerCase();
  const MAP = { csv: 'CSV', tsv: 'TSV', h5: 'HDF5', hdf5: 'HDF5', nwb: 'NWB', bin: 'Binary', dat: 'Binary', raw: 'Binary', npy: 'NumPy', npz: 'NumPy', mat: 'MATLAB', pkl: 'Pickle' };
  return MAP[ext] ?? 'Binary';
}

function fmtBytes(n) {
  const num = typeof n === 'bigint' ? Number(n) : Number(n);
  if (num >= 1e9)  return `${(num / 1e9).toFixed(1)} GB`;
  if (num >= 1e6)  return `${(num / 1e6).toFixed(1)} MB`;
  if (num >= 1e3)  return `${(num / 1e3).toFixed(1)} KB`;
  return `${num} B`;
}

/**
 * Match an array of File objects against animals.
 * Returns {
 *   matched:   Map<animalId, Array<{ file, matchedIdentifier }>>
 *   unmatched: File[]
 * }
 * matchedIdentifier is the animal_id_string or animal_name value that triggered the match.
 */
function matchFilesToAnimals(files, animals) {
  // Build patterns; each entry carries both alternatives so we know which one matched.
  const entries = animals
    .map((animal) => {
      const alts = [];
      if (animal.animal_id_string) alts.push({ value: animal.animal_id_string, re: new RegExp(`(?<![a-zA-Z0-9])${escapeRegex(animal.animal_id_string)}(?![a-zA-Z0-9])`, 'i') });
      if (animal.animal_name) {
        const words = animal.animal_name.trim().split(/\s+/).map(escapeRegex);
        const pat   = words.length === 1 ? words[0] : words.join('[\\s_\\-]+');
        alts.push({ value: animal.animal_name, re: new RegExp(`(?<![a-zA-Z0-9])${pat}(?![a-zA-Z0-9])`, 'i') });
      }
      return { animal, alts };
    })
    .filter((e) => e.alts.length > 0);

  const matched  = new Map(animals.map((a) => [a.id, []]));
  const unmatched = [];

  for (const file of files) {
    let found = false;
    for (const { animal, alts } of entries) {
      for (const { value, re } of alts) {
        if (re.test(file.name)) {
          matched.get(animal.id).push({ file, matchedIdentifier: value });
          found = true;
          break;
        }
      }
      if (found) break;
    }
    if (!found) unmatched.push(file);
  }
  return { matched, unmatched };
}

// ── Sub-components ────────────────────────────────────────────────────────────

function FileRow({ file, matchedIdentifier, onRemove }) {
  return (
    <div className="flex items-center justify-between py-1 gap-2 text-xs">
      <span className="font-mono text-gray-700 truncate min-w-0 flex-1">{file.name}</span>
      {matchedIdentifier && (
        <span className="text-blue-400 shrink-0 font-mono" title="matched by">↳ {matchedIdentifier}</span>
      )}
      <span className="text-gray-400 shrink-0">{fmtBytes(file.size)}</span>
      <span className="text-gray-400 shrink-0 w-12 text-right">{classifyFile(file.name)}</span>
      {onRemove && (
        <button type="button" onClick={onRemove}
          className="text-red-300 hover:text-red-500 transition-colors shrink-0 ml-1">✕</button>
      )}
    </div>
  );
}

function SavedFileRow({ file, onDelete, deleting }) {
  const size = typeof file.file_size === 'string' ? BigInt(file.file_size) : file.file_size;
  return (
    <div className="flex items-center justify-between py-1 gap-2 text-xs">
      <span className="font-mono text-gray-600 truncate min-w-0 flex-1">{file.filename}</span>
      <span className="text-gray-400 shrink-0">{fmtBytes(size)}</span>
      <span className="text-gray-400 shrink-0 w-12 text-right">{file.file_type ?? '?'}</span>
      <button type="button" onClick={onDelete} disabled={deleting}
        className="text-red-300 hover:text-red-500 transition-colors shrink-0 ml-1 disabled:opacity-40">✕</button>
    </div>
  );
}

// ── Main component ────────────────────────────────────────────────────────────

/**
 * FileDropZone
 *
 * Props:
 *   rows      — array of { animal, status } from ExperimentDayView
 *               (only rows with a status are shown; status.id is required to save files)
 *   date      — YYYY-MM-DD string, shown in naming hint
 */
export default function FileDropZone({ rows, date }) {
  const [isDragging, setIsDragging] = useState(false);
  const [dropped, setDropped]       = useState(null);   // { matched: Map, unmatched: File[] } | null
  const [saving, setSaving]         = useState(false);
  const [saveError, setSaveError]   = useState(null);
  const [savedByStatus, setSavedByStatus] = useState({}); // statusId → SavedFile[]
  const [loadingIds, setLoadingIds]       = useState(new Set());
  const [deletingId, setDeletingId]       = useState(null);
  const dragCounter = useRef(0);

  const statusRows = rows.filter((r) => r.status);
  const animals    = statusRows.map((r) => r.animal);

  // Load existing saved files for all statuses in this day view
  useEffect(() => {
    if (statusRows.length === 0) return;
    const ids = statusRows.map((r) => r.status.id);
    setLoadingIds(new Set(ids));
    Promise.all(
      ids.map((sid) => sessionFilesApi.list(sid).then((files) => ({ sid, files })))
    ).then((results) => {
      const map = {};
      results.forEach(({ sid, files }) => { map[sid] = files; });
      setSavedByStatus(map);
      setLoadingIds(new Set());
    }).catch(() => setLoadingIds(new Set()));
  }, [statusRows.map((r) => r.status?.id).join(',')]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Drag handlers ──────────────────────────────────────────────────────────

  const onDragEnter = useCallback((e) => {
    e.preventDefault();
    dragCounter.current += 1;
    if (dragCounter.current === 1) setIsDragging(true);
  }, []);

  const onDragLeave = useCallback((e) => {
    e.preventDefault();
    dragCounter.current -= 1;
    if (dragCounter.current === 0) setIsDragging(false);
  }, []);

  const onDragOver = useCallback((e) => { e.preventDefault(); }, []);

  const onDrop = useCallback((e) => {
    e.preventDefault();
    dragCounter.current = 0;
    setIsDragging(false);
    const files = Array.from(e.dataTransfer.files);
    if (files.length === 0) return;
    setDropped(matchFilesToAnimals(files, animals));
    setSaveError(null);
  }, [animals]);

  function removeMatchedFile(animalId, fileIdx) {
    setDropped((prev) => {
      const next = new Map(prev.matched);
      const arr = [...next.get(animalId)];
      arr.splice(fileIdx, 1);
      next.set(animalId, arr);
      return { matched: next, unmatched: prev.unmatched };
    });
  }

  function removeUnmatched(idx) {
    setDropped((prev) => {
      const u = [...prev.unmatched];
      u.splice(idx, 1);
      return { matched: prev.matched, unmatched: u };
    });
  }

  // ── Save ───────────────────────────────────────────────────────────────────

  async function save() {
    if (!dropped) return;
    setSaving(true);
    setSaveError(null);
    try {
      const results = await Promise.all(
        statusRows.map(async ({ animal, status }) => {
          const entries = dropped.matched.get(animal.id) ?? [];
          if (entries.length === 0) return { statusId: status.id, saved: [] };
          const payload = entries.map(({ file, matchedIdentifier }) => ({
            filename:              file.name,
            file_size:             file.size,
            file_type:             classifyFile(file.name),
            last_modified:         file.lastModified ?? null,
            matched_identifier:    matchedIdentifier,
            animal_id_string_snap: animal.animal_id_string ?? null,
            animal_name_snap:      animal.animal_name      ?? null,
          }));
          const saved = await sessionFilesApi.create(status.id, payload);
          return { statusId: status.id, saved };
        })
      );
      setSavedByStatus((prev) => {
        const next = { ...prev };
        results.forEach(({ statusId, saved }) => {
          next[statusId] = [...(next[statusId] ?? []), ...saved];
        });
        return next;
      });
      setDropped(null);
    } catch (err) {
      setSaveError(err.message ?? 'Failed to save file metadata');
    } finally {
      setSaving(false);
    }
  }

  // ── Delete ─────────────────────────────────────────────────────────────────

  async function deleteFile(fileId, statusId) {
    setDeletingId(fileId);
    try {
      await sessionFilesApi.delete(fileId);
      setSavedByStatus((prev) => ({
        ...prev,
        [statusId]: prev[statusId].filter((f) => f.id !== fileId),
      }));
    } catch {
      // silently ignore — UI stays consistent, user can retry
    } finally {
      setDeletingId(null);
    }
  }

  // ── Derived counts ─────────────────────────────────────────────────────────

  const totalDropped       = dropped ? [...dropped.matched.values()].reduce((n, a) => n + a.length, 0) : 0;  // entries are { file, matchedIdentifier }
  const totalUnmatched     = dropped?.unmatched.length ?? 0;
  const totalSaved         = Object.values(savedByStatus).reduce((n, a) => n + a.length, 0);
  const hasSaved           = totalSaved > 0;
  const hasMatchedDropped  = totalDropped > 0;

  // Hint string for naming convention
  const sampleId = animals[0]?.animal_id_string || animals[0]?.animal_name || '<id>';
  const namingHint = `${date}_${sampleId}_session1.csv`;

  return (
    <div className="mt-8">
      <h3 className="text-sm font-semibold text-gray-700 mb-3">Session File Registry</h3>
      <p className="text-xs text-gray-400 mb-4">
        Drop all session files here. Filenames are matched to subjects by ID —
        only metadata (name, size) is recorded; files are never uploaded.
        <br />
        Naming convention: <span className="font-mono text-gray-500">{namingHint}</span>
      </p>

      {/* Drop zone */}
      <div
        onDragEnter={onDragEnter}
        onDragLeave={onDragLeave}
        onDragOver={onDragOver}
        onDrop={onDrop}
        data-testid="drop-zone"
        className={[
          'border-2 border-dashed rounded-xl px-6 py-10 text-center transition-all cursor-default select-none',
          isDragging
            ? 'border-blue-400 bg-blue-50'
            : 'border-gray-200 bg-gray-50 hover:border-gray-300',
        ].join(' ')}
      >
        {isDragging ? (
          <p className="text-blue-600 font-medium text-sm">Release to register files</p>
        ) : (
          <>
            <p className="text-gray-400 text-sm">Drag &amp; drop session files here</p>
            <p className="text-gray-300 text-xs mt-1">All subjects at once · files are not uploaded</p>
          </>
        )}
      </div>

      {/* Dropped files preview */}
      {dropped && (
        <div className="mt-4 space-y-4" data-testid="drop-preview">

          {/* Per-subject matched files */}
          {statusRows.map(({ animal, status }) => {
            const entries = dropped.matched.get(animal.id) ?? [];
            if (entries.length === 0) return null;
            return (
              <div key={animal.id} className="bg-white border border-gray-200 rounded-xl p-4">
                <div className="flex items-center justify-between mb-2">
                  <span className="text-sm font-semibold text-gray-800">
                    {animal.animal_name}
                    {animal.animal_id_string && (
                      <span className="ml-1.5 text-xs font-normal text-gray-400">{animal.animal_id_string}</span>
                    )}
                  </span>
                  <span className="text-xs text-gray-400">{entries.length} file{entries.length !== 1 ? 's' : ''}</span>
                </div>
                <div className="divide-y divide-gray-50">
                  {entries.map(({ file, matchedIdentifier }, i) => (
                    <FileRow key={file.name + i} file={file} matchedIdentifier={matchedIdentifier} onRemove={() => removeMatchedFile(animal.id, i)} />
                  ))}
                </div>
              </div>
            );
          })}

          {/* Subjects with no matched files */}
          {statusRows.some(({ animal }) => (dropped.matched.get(animal.id) ?? []).length === 0) && (
            <div className="bg-amber-50 border border-amber-100 rounded-xl p-4">
              <p className="text-xs font-semibold text-amber-700 mb-1">No files matched for:</p>
              {statusRows
                .filter(({ animal }) => (dropped.matched.get(animal.id) ?? []).length === 0)
                .map(({ animal }) => (
                  <span key={animal.id} className="inline-block text-xs text-amber-600 bg-amber-100 rounded px-2 py-0.5 mr-1 mb-1 font-mono">
                    {animal.animal_id_string || animal.animal_name}
                  </span>
                ))}
            </div>
          )}

          {/* Unmatched files */}
          {dropped.unmatched.length > 0 && (
            <div className="bg-gray-50 border border-dashed border-gray-200 rounded-xl p-4">
              <p className="text-xs font-semibold text-gray-500 mb-2">
                Unmatched ({dropped.unmatched.length}) — no subject ID found in filename
              </p>
              <div className="divide-y divide-gray-100">
                {dropped.unmatched.map((f, i) => (
                  <FileRow key={f.name + i} file={f} onRemove={() => removeUnmatched(i)} />
                ))}
              </div>
            </div>
          )}

          {saveError && (
            <p className="text-xs text-red-500 px-1">{saveError}</p>
          )}

          {/* Action row */}
          <div className="flex items-center gap-3 pt-1">
            {hasMatchedDropped && (
              <button
                type="button"
                onClick={save}
                disabled={saving}
                data-testid="save-btn"
                className="px-4 py-2 rounded-lg text-sm font-medium bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 transition-colors"
              >
                {saving ? 'Saving…' : `Register ${totalDropped} file${totalDropped !== 1 ? 's' : ''}`}
              </button>
            )}
            <button
              type="button"
              onClick={() => { setDropped(null); setSaveError(null); }}
              className="text-sm text-gray-400 hover:text-gray-600"
            >
              Clear
            </button>
          </div>
        </div>
      )}

      {/* Saved files */}
      {hasSaved && (
        <div className="mt-6 space-y-3" data-testid="saved-files">
          <p className="text-xs font-semibold text-gray-500 uppercase tracking-wide">
            Registered files ({totalSaved})
          </p>
          {statusRows.map(({ animal, status }) => {
            const files = savedByStatus[status.id] ?? [];
            if (files.length === 0) return null;
            return (
              <div key={animal.id} className="bg-white border border-gray-200 rounded-xl p-4">
                <div className="flex items-center justify-between mb-2">
                  <span className="text-sm font-semibold text-gray-800">
                    {animal.animal_name}
                    {animal.animal_id_string && (
                      <span className="ml-1.5 text-xs font-normal text-gray-400">{animal.animal_id_string}</span>
                    )}
                  </span>
                  <span className="text-xs text-gray-400">{files.length} file{files.length !== 1 ? 's' : ''}</span>
                </div>
                <div className="divide-y divide-gray-50">
                  {files.map((f) => (
                    <SavedFileRow
                      key={f.id}
                      file={f}
                      onDelete={() => deleteFile(f.id, status.id)}
                      deleting={deletingId === f.id}
                    />
                  ))}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}
