'use client';

import { useCallback, useEffect, useRef, useState } from 'react';

import { sourceLabel } from '../lib/classify';
import { editFilterCss, editTransformCss } from '../lib/edits';
import { formatBytes, formatDate, formatDuration, formatTime } from '../lib/format';
import { blobToWavBase64, startRecording, wavBase64ToBlob, type Recorder } from '../lib/audioCapture';
import { Icon } from '../icons';
import { useAIProvider } from './aiContext';
import { useGallery, useGalleryStoreApi } from '../store/context';
import type { GalleryUser, GeoLocation, MediaItem } from '../types';

export function InfoPanel() {
  const api = useGalleryStoreApi();
  const open = useGallery((s) => s.infoOpen);
  const media = useGallery((s) => s.media);
  const lightboxId = useGallery((s) => s.lightboxId);
  const selection = useGallery((s) => s.selection);

  if (!open) return null;

  // Target: the lightbox item, else the single selected item.
  const targetId = lightboxId ?? (selection.size === 1 ? [...selection][0] : undefined);
  const item = targetId ? (media.find((m) => m.id === targetId) ?? null) : null;

  const ext = item?.name.includes('.') ? item.name.split('.').pop()!.toUpperCase() : '—';
  const mp = item ? ((item.width * item.height) / 1_000_000).toFixed(1) : '0';

  return (
    <aside className="apg-info" aria-label="Info">
      <div className="apg-info__head">
        <span style={{ fontWeight: 700, fontSize: 15 }}>Info</span>
        <button
          type="button"
          className="apg-iconbtn"
          aria-label="Close info"
          onClick={() => api.getState().setInfoOpen(false)}
        >
          <Icon name="close" size={18} />
        </button>
      </div>

      {!item ? (
        <div className="apg-info__empty">Select a single photo to see its details.</div>
      ) : (
        <div className="apg-info__body">
          {item.kind === 'video' ? (
            // Videos get a real, playable preview at the top of the panel — the same
            // affordance images have, not a frozen poster frame.
            <video
              className="apg-info__thumb apg-info__thumb--video"
              src={item.src}
              poster={item.poster ?? item.thumbnail}
              controls
              playsInline
              preload="metadata"
            />
          ) : (
            <img className="apg-info__thumb" src={item.thumbnail ?? item.src} alt={item.name} />
          )}
          <div className="apg-info__name">{item.name}</div>
          <div className="apg-info__sub">
            {formatDate(item.takenAt)} · {formatTime(item.takenAt)}
          </div>

          {/* Editable caption (single line) + note (multi-line) — persisted via updateMedia. */}
          <EditableField
            key={`cap-${item.id}`}
            label="Caption"
            value={item.caption}
            placeholder="Add a caption…"
            onSave={(v) => api.getState().updateMedia(item.id, { caption: v })}
          />
          <EditableField
            key={`note-${item.id}`}
            label="Note"
            value={item.note}
            placeholder="Add a note…"
            multiline
            onSave={(v) => api.getState().updateMedia(item.id, { note: v })}
          />

          {item.uploadedBy ? <UploadedBy user={item.uploadedBy} /> : null}

          {item.kind === 'image' && !item.analyzedAt ? (
            <div className="apg-info__analyzing">
              <span className="apg-info__spinner" aria-hidden />
              Analyzing image…
            </div>
          ) : null}

          <Row label="Kind" value={item.kind === 'video' ? 'Video' : 'Photo'} />
          <Row label="Source" value={sourceLabel(item.source)} />
          <Row label="Format" value={`${item.mime} (${ext})`} />
          <Row label="Dimensions" value={`${item.width} × ${item.height} · ${mp} MP`} />
          <Row label="Size" value={formatBytes(item.bytes)} />
          {item.duration ? <Row label="Duration" value={formatDuration(item.duration)} /> : null}
          {item.exif?.Make || item.exif?.Model ? (
            <Row label="Camera" value={`${item.exif.Make ?? ''} ${item.exif.Model ?? ''}`.trim()} />
          ) : null}
          {item.exif?.FNumber ? (
            <Row
              label="Exposure"
              value={[
                item.exif.FNumber ? `ƒ${item.exif.FNumber}` : '',
                item.exif.ISO ? `ISO ${item.exif.ISO}` : '',
                item.exif.FocalLength ? `${item.exif.FocalLength}mm` : '',
              ]
                .filter(Boolean)
                .join(' · ')}
            />
          ) : null}
          {item.exif?.LensModel ? <Row label="Lens" value={String(item.exif.LensModel)} /> : null}
          {item.exif?.Orientation ? (
            <Row label="Orientation" value={orientationLabel(item.exif.Orientation)} />
          ) : null}

          {item.objectLabels.length ? (
            <Chips
              label="Objects"
              items={item.objectLabels}
              onClick={(o) => {
                const s = api.getState();
                s.setObjectFocus(o);
                s.closeLightbox();
                s.setInfoOpen(false);
              }}
            />
          ) : null}
          {item.tags.length ? (
            <Chips
              label="Tags"
              items={item.tags}
              onClick={(t) => {
                const s = api.getState();
                s.setTagFocus(t);
                s.closeLightbox();
                s.setInfoOpen(false);
              }}
            />
          ) : null}

          {item.location ? (
            <>
              <Row
                label="Location"
                value={
                  item.location.formatted ??
                  item.location.place ??
                  `${item.location.lat.toFixed(4)}, ${item.location.lng.toFixed(4)}`
                }
              />
              <MiniMap
                lat={item.location.lat}
                lng={item.location.lng}
                onOpen={() => api.getState().focusMap({ lat: item.location!.lat, lng: item.location!.lng })}
              />
              <AddressBlock
                item={item}
                onOpenMap={() => api.getState().focusMap({ lat: item.location!.lat, lng: item.location!.lng })}
              />
              <div style={{ fontSize: 11, color: 'var(--apg-text-tertiary)', marginTop: 4 }}>
                Click the map or address to open it in full.
              </div>
            </>
          ) : null}

          <Versions item={item} />
          <Comments item={item} />
        </div>
      )}
    </aside>
  );
}

/** Version history + audit log for a photo/video (v1 = original). */
function Versions({ item }: { item: MediaItem }) {
  const api = useGalleryStoreApi();
  const [openId, setOpenId] = useState<string | null>(null);
  const versions = item.versions ?? [];
  // Newest first; the last entry is the current one.
  const ordered = [...versions].reverse();
  const currentId = versions.length ? versions[versions.length - 1]!.id : null;

  return (
    <div className="apg-info__section">
      <div className="apg-info__section-head">
        <Icon name="clock" size={15} />
        <span>Version history</span>
        <span className="apg-info__count">{versions.length || 1}</span>
      </div>
      {versions.length === 0 ? (
        <div className="apg-info__hint">
          Only the original exists. Edits create new versions — the original is never overwritten.
        </div>
      ) : (
        <ol className="apg-versions">
          {ordered.map((v) => {
            const isOpen = openId === v.id;
            const isCurrent = v.id === currentId;
            const isOriginal = v.version === 1;
            return (
              <li key={v.id} className="apg-version">
                <button
                  type="button"
                  className={`apg-version__row${isCurrent ? ' is-current' : ''}`}
                  onClick={() => setOpenId(isOpen ? null : v.id)}
                  aria-expanded={isOpen}
                >
                  <span className="apg-version__thumb-wrap">
                    <img
                      className="apg-version__thumb"
                      src={v.thumbnail ?? v.src ?? item.thumbnail ?? item.src}
                      alt=""
                      style={{
                        filter: editFilterCss(v.edits) || undefined,
                        transform: editTransformCss(v.edits) || undefined,
                      }}
                    />
                  </span>
                  <span className="apg-version__meta">
                    <span className="apg-version__title">
                      {isOriginal ? 'Original' : `Version ${v.version}`}
                      {isCurrent ? <span className="apg-version__badge">Current</span> : null}
                    </span>
                    <span className="apg-version__time">
                      {formatDate(v.createdAt)} · {formatTime(v.createdAt)}
                    </span>
                    {v.author ? (
                      <span className="apg-version__author">
                        {v.authorAvatar ? (
                          <img className="apg-version__author-avatar" src={v.authorAvatar} alt="" />
                        ) : (
                          <span className="apg-version__author-avatar apg-version__author-avatar--initial" aria-hidden>
                            {v.author.slice(0, 1).toUpperCase()}
                          </span>
                        )}
                        <span className="apg-version__author-name">{v.author}</span>
                      </span>
                    ) : null}
                  </span>
                  <Icon name={isOpen ? 'chevron-down' : 'chevron-right'} size={14} />
                </button>
                {isOpen ? (
                  <div className="apg-version__detail">
                    <div className="apg-version__changes-label">What changed</div>
                    <ul className="apg-version__changes">
                      {v.changes.map((c, i) => (
                        <li key={i}>{c}</li>
                      ))}
                    </ul>
                    {!isCurrent ? (
                      <button
                        type="button"
                        className="apg-btn apg-btn--small"
                        onClick={() => api.getState().restoreVersion(item.id, v.id)}
                      >
                        Restore this version
                      </button>
                    ) : null}
                  </div>
                ) : null}
              </li>
            );
          })}
        </ol>
      )}
    </div>
  );
}

/** Threaded comments on a photo/video. */
function Comments({ item }: { item: MediaItem }) {
  const api = useGalleryStoreApi();
  const currentUser = useGallery((s) => s.config.currentUser);
  const comments = item.comments ?? [];
  const [text, setText] = useState('');
  // Without a host-provided user we keep the legacy free-text author field
  // (remembered in localStorage). With one, identity comes from the host.
  const [author, setAuthor] = useState(() => {
    if (typeof window === 'undefined') return 'You';
    return window.localStorage.getItem('apg:comment-author') || 'You';
  });

  const provider = useAIProvider();
  const canVoice = Boolean(provider?.transcribeAudio);
  const canDenoise = Boolean(provider?.denoiseAudio);
  const [recording, setRecording] = useState(false);
  const [denoise, setDenoise] = useState(false);
  const [voiceStatus, setVoiceStatus] = useState<string | null>(null);
  const recorderRef = useRef<Recorder | null>(null);

  const startVoice = async () => {
    setVoiceStatus(null);
    try {
      recorderRef.current = await startRecording();
      setRecording(true);
    } catch (e) {
      setVoiceStatus(e instanceof Error ? e.message : 'Microphone unavailable.');
    }
  };

  const stopVoice = async () => {
    const rec = recorderRef.current;
    recorderRef.current = null;
    setRecording(false);
    if (!rec || !provider?.transcribeAudio) return;
    try {
      const blob = await rec.stop();
      let wav16: string;
      if (denoise && provider.denoiseAudio) {
        setVoiceStatus('Reducing noise…');
        const wav48 = await blobToWavBase64(blob, 48000);
        const cleaned = await provider.denoiseAudio(wav48);
        wav16 = await blobToWavBase64(wavBase64ToBlob(cleaned), 16000);
      } else {
        wav16 = await blobToWavBase64(blob, 16000);
      }
      setVoiceStatus('Transcribing…');
      const spoken = (await provider.transcribeAudio(wav16)).trim();
      if (spoken) setText((prev) => (prev ? `${prev} ${spoken}` : spoken));
      setVoiceStatus(null);
    } catch (e) {
      setVoiceStatus(e instanceof Error ? e.message : 'Could not transcribe audio.');
    }
  };

  const post = () => {
    const t = text.trim();
    if (!t) return;
    if (currentUser) {
      // The store stamps authorId/author/avatar from config.currentUser.
      api.getState().addComment(item.id, t);
      setText('');
      return;
    }
    const name = author.trim() || 'You';
    try {
      window.localStorage.setItem('apg:comment-author', name);
    } catch {
      /* ignore */
    }
    api.getState().addComment(item.id, t, name);
    setText('');
  };

  return (
    <div className="apg-info__section">
      <div className="apg-info__section-head">
        <Icon name="chat" size={15} />
        <span>Comments</span>
        <span className="apg-info__count">{comments.length}</span>
      </div>

      {comments.length === 0 ? (
        <div className="apg-info__hint">No comments yet. Start the conversation below.</div>
      ) : (
        <ul className="apg-comments">
          {comments.map((c) => {
            // With a configured user, only their OWN comments are deletable
            // (mirrors the server rule enforced on authorId).
            const canDelete = !currentUser || c.authorId === currentUser.id;
            return (
              <li key={c.id} className="apg-comment">
                {c.authorAvatar ? (
                  <img className="apg-comment__avatar-img" src={c.authorAvatar} alt="" />
                ) : (
                  <div className="apg-comment__avatar" aria-hidden>
                    {(c.author ?? 'You').slice(0, 1).toUpperCase()}
                  </div>
                )}
                <div className="apg-comment__body">
                  <div className="apg-comment__meta">
                    <span className="apg-comment__author">{c.author ?? 'You'}</span>
                    <span className="apg-comment__time">
                      {formatDate(c.createdAt)} · {formatTime(c.createdAt)}
                    </span>
                  </div>
                  <div className="apg-comment__text">{c.text}</div>
                </div>
                {canDelete ? (
                  <button
                    type="button"
                    className="apg-comment__delete"
                    aria-label="Delete comment"
                    onClick={() => api.getState().deleteComment(item.id, c.id)}
                  >
                    <Icon name="close" size={13} />
                  </button>
                ) : null}
              </li>
            );
          })}
        </ul>
      )}

      <div className="apg-comment-form">
        {currentUser ? (
          <div className="apg-comment-form__identity">
            {currentUser.avatarUrl ? (
              <img className="apg-comment__avatar-img" src={currentUser.avatarUrl} alt="" />
            ) : (
              <div className="apg-comment__avatar" aria-hidden>
                {(currentUser.name || '?').slice(0, 1).toUpperCase()}
              </div>
            )}
            <span className="apg-comment__author">{currentUser.name}</span>
          </div>
        ) : (
          <input
            className="apg-comment-form__author"
            value={author}
            onChange={(e) => setAuthor(e.target.value)}
            placeholder="Your name"
            aria-label="Your name"
            maxLength={40}
          />
        )}
        <textarea
          className="apg-comment-form__input"
          value={text}
          onChange={(e) => setText(e.target.value)}
          onKeyDown={(e) => {
            if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') post();
          }}
          placeholder="Add a comment…"
          aria-label="Add a comment"
          rows={2}
          maxLength={2000}
        />
        {canVoice ? (
          <div className="apg-voice">
            <button
              type="button"
              className={`apg-btn apg-btn--small apg-voice__mic${recording ? ' apg-voice__mic--rec' : ''}`}
              onClick={recording ? stopVoice : startVoice}
              aria-label={recording ? 'Stop recording' : 'Record a voice comment'}
              title={recording ? 'Stop & transcribe' : 'Speak your comment'}
            >
              <Icon name={recording ? 'check' : 'mic'} size={14} />
              {recording ? 'Stop' : 'Speak'}
            </button>
            {canDenoise ? (
              <label
                className="apg-voice__denoise"
                title="Clean up background noise before transcribing"
              >
                <input
                  type="checkbox"
                  checked={denoise}
                  onChange={(e) => setDenoise(e.target.checked)}
                />
                Reduce noise
              </label>
            ) : null}
            <span className="apg-voice__status" aria-live="polite">
              {recording ? '● Listening…' : (voiceStatus ?? '')}
            </span>
          </div>
        ) : null}
        <button
          type="button"
          className="apg-btn apg-btn--small apg-comment-form__post"
          onClick={post}
          disabled={!text.trim()}
        >
          Post
        </button>
      </div>
    </div>
  );
}

function Row({ label, value }: { label: string; value: string }) {
  return (
    <div className="apg-info__row">
      <span className="apg-info__row-label">{label}</span>
      <span className="apg-info__row-value">{value}</span>
    </div>
  );
}

// EXIF Orientation codes 1–8 → human text (construction shots are often sideways).
const ORIENTATION_LABELS: Record<number, string> = {
  1: 'Normal',
  2: 'Mirrored horizontal',
  3: 'Rotated 180°',
  4: 'Mirrored vertical',
  5: 'Mirrored + 90° CCW',
  6: 'Rotated 90° CW',
  7: 'Mirrored + 90° CW',
  8: 'Rotated 90° CCW',
};
function orientationLabel(v: string | number): string {
  const n = typeof v === 'number' ? v : parseInt(String(v), 10);
  return ORIENTATION_LABELS[n] ?? String(v);
}

function Chips({
  label,
  items,
  onClick,
}: {
  label: string;
  items: string[];
  onClick?: (item: string) => void;
}) {
  return (
    <div className="apg-info__chips">
      <span className="apg-info__row-label">{label}</span>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 4 }}>
        {items.map((t) => (
          <button
            key={t}
            type="button"
            className="apg-chip"
            onClick={onClick ? () => onClick(t) : undefined}
            style={{ cursor: onClick ? 'pointer' : 'default', textTransform: 'capitalize' }}
          >
            {t}
          </button>
        ))}
      </div>
    </div>
  );
}

// Cross-instance guards so re-opening a panel (or a second panel) never double-
// fetches the same item: ids currently in flight, and ids that already failed
// (so the auto-fetch doesn't loop — the user must press Retry).
const geocodeInFlight = new Set<string>();
const geocodeFailed = new Set<string>();

/** Shape of Nominatim's `address` object (addressdetails=1) — all fields optional. */
interface NominatimAddress {
  road?: string;
  neighbourhood?: string;
  suburb?: string;
  city?: string;
  town?: string;
  village?: string;
  county?: string;
  state?: string;
  postcode?: string;
  country?: string;
  country_code?: string;
}

/**
 * Reverse-geocodes an item's GPS coords to a full postal address via free
 * OpenStreetMap Nominatim, then PERSISTS the parsed fields onto the item
 * (`updateMedia`) so it saves through the adapter and survives a reload. Runs
 * automatically the first time a located item's Info panel opens (only when
 * `location.formatted` isn't already cached); manual Retry on error.
 *
 * NOTE: a hardened host CSP must allow `nominatim.openstreetmap.org` in
 * `connect-src` for this fetch to succeed.
 */
function AddressBlock({ item, onOpenMap }: { item: MediaItem; onOpenMap: () => void }) {
  const api = useGalleryStoreApi();
  const loc = item.location!;
  const [status, setStatus] = useState<'idle' | 'loading' | 'error'>('idle');

  const lookup = useCallback(async () => {
    // One request per id at a time (etiquette + no double-fetch across panels).
    if (geocodeInFlight.has(item.id)) return;
    geocodeInFlight.add(item.id);
    setStatus('loading');
    try {
      const res = await fetch(
        `https://nominatim.openstreetmap.org/reverse?lat=${loc.lat}&lon=${loc.lng}&format=json&zoom=18&addressdetails=1`,
        { headers: { Accept: 'application/json' } },
      );
      const data = (await res.json()) as { display_name?: string; address?: NominatimAddress };
      const a = data.address ?? {};
      if (data.display_name) {
        const parsed: Partial<GeoLocation> = {
          road: a.road,
          neighbourhood: a.neighbourhood,
          suburb: a.suburb,
          city: a.city ?? a.town ?? a.village,
          county: a.county,
          state: a.state,
          postcode: a.postcode,
          country: a.country,
          countryCode: a.country_code ? a.country_code.toUpperCase() : undefined,
          formatted: data.display_name,
        };
        // Persist through the adapter (→ be-crm) so it survives reload + isn't refetched.
        api.getState().updateMedia(item.id, { location: { ...loc, ...parsed } });
        geocodeFailed.delete(item.id);
        setStatus('idle');
      } else {
        geocodeFailed.add(item.id);
        setStatus('error');
      }
    } catch {
      geocodeFailed.add(item.id);
      setStatus('error');
    } finally {
      geocodeInFlight.delete(item.id);
    }
  }, [api, item.id, loc]);

  // Auto-fetch once when a located item without a cached address opens. The
  // `!loc.formatted` guard (and the failed-set) prevent a re-fetch loop: once
  // persisted, the item re-renders WITH `formatted` and this no-ops.
  useEffect(() => {
    if (!loc.formatted && !geocodeFailed.has(item.id) && !geocodeInFlight.has(item.id)) {
      void lookup();
    }
  }, [item.id, loc.formatted, lookup]);

  const retry = () => {
    geocodeFailed.delete(item.id);
    void lookup();
  };

  if (loc.formatted) {
    const cells: Array<[string, string | undefined]> = [
      ['City', loc.city],
      ['State', loc.state],
      ['Postcode', loc.postcode],
      ['Country', loc.country],
    ];
    return (
      <div className="apg-address">
        <button type="button" className="apg-address__line" onClick={onOpenMap} title="Open in Map">
          <Icon name="map" size={14} />
          <span>{loc.formatted}</span>
        </button>
        <dl className="apg-address__grid">
          {cells
            .filter(([, v]) => Boolean(v))
            .map(([k, v]) => (
              <div key={k} className="apg-address__cell">
                <dt>{k}</dt>
                <dd>{v}</dd>
              </div>
            ))}
        </dl>
        <div className="apg-info__coords">
          {loc.lat.toFixed(5)}, {loc.lng.toFixed(5)}
        </div>
      </div>
    );
  }

  return (
    <div className="apg-address">
      <div className="apg-info__coords">
        {loc.lat.toFixed(5)}, {loc.lng.toFixed(5)}
      </div>
      {status === 'loading' ? (
        <div className="apg-info__hint" aria-live="polite">
          Looking up address…
        </div>
      ) : status === 'error' ? (
        <button type="button" className="apg-btn apg-btn--small" onClick={retry} style={{ marginTop: 6 }}>
          Retry address
        </button>
      ) : null}
    </div>
  );
}

/** "Uploaded by" identity block: avatar + name + (muted) email. */
function UploadedBy({ user }: { user: GalleryUser }) {
  return (
    <div className="apg-info__uploader">
      <span className="apg-info__uploader-label">Uploaded by</span>
      <div className="apg-info__uploader-row">
        {user.avatarUrl ? (
          <img className="apg-info__uploader-avatar" src={user.avatarUrl} alt="" />
        ) : (
          <div className="apg-info__uploader-avatar apg-info__uploader-avatar--initial" aria-hidden>
            {(user.name || '?').slice(0, 1).toUpperCase()}
          </div>
        )}
        <div className="apg-info__uploader-meta">
          <span className="apg-info__uploader-name">{user.name}</span>
          {user.email ? <span className="apg-info__uploader-email">{user.email}</span> : null}
        </div>
      </div>
    </div>
  );
}

/**
 * Click-to-edit metadata field (caption / note). Saves on blur or Enter (Cmd/Ctrl+
 * Enter for the multi-line note) via the supplied `onSave`, which persists through
 * `updateMedia`. Shows an unobtrusive "edited" hint once a value is present.
 */
function EditableField({
  label,
  value,
  placeholder,
  multiline,
  onSave,
}: {
  label: string;
  value?: string;
  placeholder: string;
  multiline?: boolean;
  onSave: (value: string) => void;
}) {
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(value ?? '');

  // Re-sync the draft when the underlying value changes and we're not editing.
  useEffect(() => {
    if (!editing) setDraft(value ?? '');
  }, [value, editing]);

  const commit = () => {
    setEditing(false);
    const next = draft.trim();
    if (next !== (value ?? '').trim()) onSave(next);
  };

  const hasValue = Boolean((value ?? '').trim());

  if (editing) {
    const shared = {
      className: 'apg-editable__input',
      value: draft,
      autoFocus: true,
      placeholder,
      onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => setDraft(e.target.value),
      onBlur: commit,
    };
    return (
      <div className="apg-editable apg-editable--editing">
        <span className="apg-editable__label">{label}</span>
        {multiline ? (
          <textarea
            {...shared}
            rows={3}
            maxLength={4000}
            onKeyDown={(e) => {
              if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') commit();
              else if (e.key === 'Escape') {
                setDraft(value ?? '');
                setEditing(false);
              }
            }}
          />
        ) : (
          <input
            {...shared}
            maxLength={280}
            onKeyDown={(e) => {
              if (e.key === 'Enter') commit();
              else if (e.key === 'Escape') {
                setDraft(value ?? '');
                setEditing(false);
              }
            }}
          />
        )}
      </div>
    );
  }

  return (
    <button type="button" className="apg-editable" onClick={() => setEditing(true)}>
      <span className="apg-editable__label">
        {label}
        {hasValue ? <span className="apg-editable__edited">edited</span> : null}
        <Icon name="pencil" size={12} />
      </span>
      <span className={['apg-editable__value', hasValue ? '' : 'apg-editable__value--empty'].filter(Boolean).join(' ')}>
        {hasValue ? value : placeholder}
      </span>
    </button>
  );
}

/** Small non-interactive Leaflet map for the location preview (click to open full Map). */
function MiniMap({ lat, lng, onOpen }: { lat: number; lng: number; onOpen?: () => void }) {
  const ref = useRef<HTMLDivElement>(null);
  const mapRef = useRef<any>(null);

  useEffect(() => {
    let cancelled = false;
    void import('leaflet').then((mod) => {
      const L = (mod as any).default ?? mod;
      if (cancelled || !ref.current || mapRef.current) return;
      const map = L.map(ref.current, {
        zoomControl: false,
        attributionControl: false,
        dragging: false,
        scrollWheelZoom: false,
        doubleClickZoom: false,
        boxZoom: false,
        keyboard: false,
        tap: false,
      }).setView([lat, lng], 11);
      mapRef.current = map;
      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
      L.circleMarker([lat, lng], {
        radius: 7,
        color: '#fff',
        weight: 2,
        fillColor: '#0a84ff',
        fillOpacity: 1,
      }).addTo(map);
    });
    return () => {
      cancelled = true;
      if (mapRef.current) {
        mapRef.current.remove();
        mapRef.current = null;
      }
    };
  }, [lat, lng]);

  return (
    <div style={{ position: 'relative' }}>
      <div ref={ref} className="apg-info__map" />
      {onOpen ? (
        <button
          type="button"
          aria-label="Open in Map"
          onClick={onOpen}
          style={{
            position: 'absolute',
            inset: 0,
            background: 'transparent',
            border: 'none',
            cursor: 'pointer',
            zIndex: 500,
          }}
        />
      ) : null}
    </div>
  );
}
