// @design-system: domain/IsraeliAddressField

/**
 * IsraeliAddressField - structured Israeli address picker.
 *
 * Data sources:
 *  - Cities: Israel Open Data API (data.gov.il)
 *  - Streets: filtered by selected city (data.gov.il)
 *  - Geocoding: Nominatim OpenStreetMap
 *  - Map preview: OpenStreetMap iframe
 *
 * @example
 * <IsraeliAddressField value={address} onChange={setAddress} />
 */

'use client';

import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useT } from '@/lib/i18n/react';
import { Label } from '@/components/ui/primitives/Label';
import { Input } from '@/components/ui/primitives/Input';
import { FormField } from '@/components/ui/primitives/FormField';
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/overlays/Popover';
import { cn } from '@/lib/cn';
import { captureCaught } from '@/lib/observability';
import {
  type GovCity,
  type GovStreet,
  govCityCode,
  govCityName,
  govStreetCode,
  govStreetName,
} from './govTypes';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface IsraeliAddressValue {
  cityCode: string;
  cityName: string;
  streetCode: string;
  streetName: string;
  houseNumber: string;
  apt: string;
  lat: number | null;
  lng: number | null;
}

export interface IsraeliAddressFieldProps {
  value: IsraeliAddressValue | null;
  onChange: (v: IsraeliAddressValue) => void;
}

// ─── Address API helpers (proxied via /api/address/* to avoid CORS) ───────────

async function fetchCities(): Promise<{ code: string; name: string }[]> {
  const res = await fetch('/api/address/cities');
  const json = (await res.json()) as { result?: { records?: GovCity[] } };
  return (json.result?.records ?? [])
    .map((r) => ({ code: govCityCode(r).trim(), name: govCityName(r).trim() }))
    .filter((c) => c.name)
    .sort((a, b) => a.name.localeCompare(b.name, 'he'));
}

async function fetchStreets(cityCode: string): Promise<{ code: string; name: string }[]> {
  const res = await fetch(`/api/address/streets?cityCode=${encodeURIComponent(cityCode)}`);
  const json = (await res.json()) as { result?: { records?: GovStreet[] } };
  return (json.result?.records ?? [])
    .map((r) => ({ code: govStreetCode(r).trim(), name: govStreetName(r).trim() }))
    .filter((s) => s.name)
    .sort((a, b) => a.name.localeCompare(b.name, 'he'));
}

async function geocode(
  cityName: string,
  streetName: string,
  houseNumber: string,
): Promise<{ lat: number; lng: number } | null> {
  try {
    const url = `/api/address/geocode?city=${encodeURIComponent(cityName)}&street=${encodeURIComponent(streetName)}&house=${encodeURIComponent(houseNumber)}`;
    const res = await fetch(url, { headers: { Accept: 'application/json' } });
    if (!res.ok) return null;
    const data = (await res.json()) as { ok?: boolean; lat?: number; lng?: number };
    if (!data.ok || typeof data.lat !== 'number' || typeof data.lng !== 'number') return null;
    return { lat: data.lat, lng: data.lng };
  } catch (err) {
    captureCaught(err, {
      scope: 'components.ui.domain.IsraeliAddressField.IsraeliAddressField',
      severity: 'warning',
    });
    return null;
  }
}

// ─── AddressCombobox ─────────────────────────────────────────────────────────

interface AddressComboboxProps {
  id: string;
  items: { code: string; name: string }[];
  value: { code: string; name: string } | null;
  onChange: (code: string) => void;
  placeholder: string;
  disabled?: boolean;
  loading?: boolean;
}

function AddressCombobox({
  id,
  items,
  value,
  onChange,
  placeholder,
  disabled,
  loading,
}: AddressComboboxProps) {
  const t = useT('vendor_business_card');
  const listId = `${id}-list`;

  const [open, setOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [prevValueCode, setPrevValueCode] = useState<string | undefined>(value?.code);
  // dirty: user has typed since last focus/reset — determines whether to filter
  const [dirty, setDirty] = useState(false);
  const [inputValue, setInputValue] = useState(value?.name ?? '');

  // Sync input when value.code changes externally (e.g., city change clears street).
  // React's recommended "getDerivedStateFromProps" equivalent — called during render,
  // not in an effect, to avoid cascading-render lint violation.
  if (prevValueCode !== value?.code) {
    setPrevValueCode(value?.code);
    setInputValue(value?.name ?? '');
    setDirty(false);
  }

  const filtered = useMemo(() => {
    if (!dirty || !inputValue.trim()) return items;
    const ql = inputValue.trim().toLowerCase();
    return items.filter((item) => item.name.toLowerCase().includes(ql));
  }, [items, inputValue, dirty]);

  const selectItem = (item: { code: string; name: string }) => {
    setInputValue(item.name);
    setDirty(false);
    setOpen(false);
    setActiveIndex(-1);
    onChange(item.code);
  };

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
    setDirty(true);
    setActiveIndex(-1);
    if (!open) setOpen(true);
  };

  const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
    setDirty(false);
    setOpen(true);
    e.target.select();
  };

  const handleBlur = () => {
    setInputValue(value?.name ?? '');
    setDirty(false);
    setOpen(false);
    setActiveIndex(-1);
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (!open) {
      if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
        e.preventDefault();
        setOpen(true);
      }
      return;
    }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setActiveIndex((i) => (filtered.length === 0 ? -1 : (i + 1) % filtered.length));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setActiveIndex((i) => (filtered.length === 0 ? -1 : i <= 0 ? filtered.length - 1 : i - 1));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      const item = filtered[activeIndex];
      if (activeIndex >= 0 && item) selectItem(item);
    } else if (e.key === 'Escape') {
      e.preventDefault();
      setInputValue(value?.name ?? '');
      setDirty(false);
      setOpen(false);
      setActiveIndex(-1);
    }
  };

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverAnchor asChild>
        <Input
          id={id}
          role="combobox"
          aria-expanded={open}
          aria-haspopup="listbox"
          aria-autocomplete="list"
          aria-controls={listId}
          aria-activedescendant={activeIndex >= 0 ? `${id}-option-${activeIndex}` : undefined}
          value={inputValue}
          onChange={handleInputChange}
          onFocus={handleFocus}
          onBlur={handleBlur}
          onKeyDown={handleKeyDown}
          placeholder={loading ? '...' : placeholder}
          disabled={disabled || loading}
          autoComplete="off"
        />
      </PopoverAnchor>
      {open && (
        <PopoverContent
          align="start"
          sideOffset={4}
          onOpenAutoFocus={(e) => e.preventDefault()}
          className="max-h-60 w-[var(--radix-popover-trigger-width)] overflow-y-auto p-0"
        >
          {filtered.length === 0 ? (
            <div role="status" className="text-text-secondary px-3 py-2 text-sm">
              {t('address_no_results')}
            </div>
          ) : (
            <ul id={listId} role="listbox" className="py-1">
              {filtered.map((item, i) => (
                <li
                  key={item.code}
                  id={`${id}-option-${i}`}
                  role="option"
                  aria-selected={value?.code === item.code}
                  className={cn(
                    'cursor-pointer px-3 py-2 text-sm',
                    i === activeIndex
                      ? 'bg-brand-primary-50 text-brand-primary-900'
                      : 'hover:bg-neutral-50',
                    value?.code === item.code && 'font-semibold',
                  )}
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => selectItem(item)}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter' || e.key === ' ') {
                      e.preventDefault();
                      selectItem(item);
                    }
                  }}
                >
                  {item.name}
                </li>
              ))}
            </ul>
          )}
        </PopoverContent>
      )}
    </Popover>
  );
}

// ─── Component ────────────────────────────────────────────────────────────────

export function IsraeliAddressField({ value, onChange }: IsraeliAddressFieldProps) {
  const t = useT('vendor_business_card');

  const [cities, setCities] = useState<{ code: string; name: string }[]>([]);
  const [streets, setStreets] = useState<{ code: string; name: string }[]>([]);
  const [isLoadingCities, setIsLoadingCities] = useState(true);
  const [isLoadingStreets, setIsLoadingStreets] = useState(false);
  const [isGeocoding, setIsGeocoding] = useState(false);
  const [geocodeWarning, setGeocodeWarning] = useState(false);

  // Local controlled state (mirrors the value prop for internal use)
  const [local, setLocal] = useState<IsraeliAddressValue>(
    value ?? {
      cityCode: '',
      cityName: '',
      streetCode: '',
      streetName: '',
      houseNumber: '',
      apt: '',
      lat: null,
      lng: null,
    },
  );

  // Ref to cancel in-flight street fetch when city changes
  const streetFetchRef = useRef<{ cancelled: boolean } | null>(null);
  const mountedRef = useRef(false);
  const deferredUpdateRef = useRef(0);

  useEffect(() => {
    mountedRef.current = true;
    return () => {
      mountedRef.current = false;
      if (streetFetchRef.current) streetFetchRef.current.cancelled = true;
    };
  }, []);

  // ── Sync local when parent passes a new non-null value (e.g. after fetch resolves).
  // Render-time derived state check (getDerivedStateFromProps pattern) — avoids effect setState.
  const [prevValue, setPrevValue] = useState(value);
  if (value && value !== prevValue) {
    setPrevValue(value);
    setLocal(value);
  }

  // Load cities on mount
  useEffect(() => {
    let cancelled = false;
    fetchCities()
      .then((list) => {
        if (!cancelled && mountedRef.current) setCities(list);
      })
      .catch((err) => {
        captureCaught(err, {
          scope: 'components.ui.domain.IsraeliAddressField.IsraeliAddressField',
          severity: 'info',
        });
      })
      .finally(() => {
        if (!cancelled && mountedRef.current) setIsLoadingCities(false);
      });
    return () => {
      cancelled = true;
    };
  }, []);

  // ── Effect 2: Resolve cityCode from cityName after cities list loads (legacy DB rows)
  // Also kicks off street fetch so Effect 3 can resolve streetCode.
  useEffect(() => {
    if (!cities.length) return;
    if (!local.cityName || local.cityCode) return; // already resolved or nothing to resolve
    const match = cities.find((c) => c.name === local.cityName);
    if (!match) return;
    const updated: IsraeliAddressValue = { ...local, cityCode: match.code };
    const updateId = ++deferredUpdateRef.current;

    if (streetFetchRef.current) streetFetchRef.current.cancelled = true;
    const handle = { cancelled: false };
    streetFetchRef.current = handle;

    queueMicrotask(() => {
      if (!mountedRef.current || handle.cancelled || updateId !== deferredUpdateRef.current) return;
      setLocal(updated);
      onChange(updated);
    });

    queueMicrotask(() => {
      if (!mountedRef.current || handle.cancelled) return;
      setIsLoadingStreets(true);
    });
    fetchStreets(match.code)
      .then((list) => {
        if (!handle.cancelled) setStreets(list);
      })
      .catch((err) => {
        captureCaught(err, {
          scope: 'components.ui.domain.IsraeliAddressField.IsraeliAddressField',
          severity: 'info',
        });
      })
      .finally(() => {
        if (!handle.cancelled) setIsLoadingStreets(false);
      });
  }, [cities, local, onChange]);

  // ── Effect 3: Resolve streetCode from streetName after streets list loads (legacy DB rows)
  useEffect(() => {
    if (!streets.length) return;
    if (!local.streetName || local.streetCode) return; // already resolved or nothing to resolve
    const match = streets.find((s) => s.name === local.streetName);
    if (!match) return;
    const updated: IsraeliAddressValue = { ...local, streetCode: match.code };
    const updateId = ++deferredUpdateRef.current;

    queueMicrotask(() => {
      if (!mountedRef.current || updateId !== deferredUpdateRef.current) return;
      setLocal(updated);
      onChange(updated);
    });
  }, [local, onChange, streets]);

  const tryGeocode = useCallback(
    async (updated: IsraeliAddressValue) => {
      if (!updated.cityName || !updated.streetName || !updated.houseNumber) return;
      setIsGeocoding(true);
      setGeocodeWarning(false);
      const coords = await geocode(updated.cityName, updated.streetName, updated.houseNumber);
      setIsGeocoding(false);
      const final: IsraeliAddressValue = {
        ...updated,
        lat: coords?.lat ?? null,
        lng: coords?.lng ?? null,
      };
      if (!coords) setGeocodeWarning(true);
      setLocal(final);
      onChange(final);
    },
    [onChange],
  );

  const handleCityChange = (cityCode: string) => {
    // Cancel any in-flight street fetch
    if (streetFetchRef.current) streetFetchRef.current.cancelled = true;

    const cityName = cities.find((c) => c.code === cityCode)?.name ?? '';
    const updated: IsraeliAddressValue = {
      ...local,
      cityCode,
      cityName,
      streetCode: '',
      streetName: '',
      lat: null,
      lng: null,
    };
    setLocal(updated);
    onChange(updated);
    setStreets([]);

    if (!cityCode) return;
    const handle = { cancelled: false };
    streetFetchRef.current = handle;
    setIsLoadingStreets(true);
    fetchStreets(cityCode)
      .then((list) => {
        if (!handle.cancelled) setStreets(list);
      })
      .catch((err) => {
        captureCaught(err, {
          scope: 'components.ui.domain.IsraeliAddressField.IsraeliAddressField',
          severity: 'info',
        });
      })
      .finally(() => {
        if (!handle.cancelled) setIsLoadingStreets(false);
      });
  };

  const handleStreetChange = (streetCode: string) => {
    const streetName = streets.find((s) => s.code === streetCode)?.name ?? '';
    const updated: IsraeliAddressValue = { ...local, streetCode, streetName, lat: null, lng: null };
    setLocal(updated);
    onChange(updated);
  };

  const handleHouseNumberBlur = () => {
    if (local.houseNumber) void tryGeocode(local);
  };

  const mapUrl =
    local.lat && local.lng
      ? `https://www.openstreetmap.org/export/embed.html?bbox=${local.lng - 0.002},${local.lat - 0.002},${local.lng + 0.002},${local.lat + 0.002}&layer=mapnik&marker=${local.lat},${local.lng}`
      : null;

  return (
    <section aria-labelledby="address-section-heading" className="flex flex-col gap-3">
      <Label id="address-section-heading" className="block text-sm font-semibold">
        {t('address_section')}
      </Label>

      {/* City */}
      <FormField htmlFor="addr-city" label={t('address_city')}>
        <AddressCombobox
          id="addr-city"
          items={cities}
          value={local.cityCode ? { code: local.cityCode, name: local.cityName } : null}
          onChange={handleCityChange}
          placeholder={t('address_city')}
          loading={isLoadingCities}
        />
      </FormField>

      {/* Street */}
      <FormField htmlFor="addr-street" label={t('address_street')}>
        <AddressCombobox
          id="addr-street"
          items={streets}
          value={local.streetCode ? { code: local.streetCode, name: local.streetName } : null}
          onChange={handleStreetChange}
          placeholder={!local.cityCode ? t('address_street_pick_city_first') : t('address_street')}
          disabled={!local.cityCode}
          loading={isLoadingStreets}
        />
      </FormField>

      {/* House number + apt - side by side */}
      <div className="grid grid-cols-2 gap-3">
        <FormField htmlFor="addr-house" label={t('address_house_number')}>
          <Input
            id="addr-house"
            type="text"
            dir="ltr"
            value={local.houseNumber}
            onChange={(e) => setLocal((p) => ({ ...p, houseNumber: e.target.value }))}
            onBlur={handleHouseNumberBlur}
          />
        </FormField>
        <FormField htmlFor="addr-apt" label={t('address_apt')}>
          <Input
            id="addr-apt"
            type="text"
            value={local.apt}
            onChange={(e) => {
              const updated = { ...local, apt: e.target.value };
              setLocal(updated);
              onChange(updated);
            }}
          />
        </FormField>
      </div>

      {/* Geocoding indicator */}
      {isGeocoding && (
        <p role="status" className="text-text-secondary text-xs">
          {t('address_geocoding')}
        </p>
      )}
      {geocodeWarning && !isGeocoding && (
        <p role="alert" className="text-warning-600 text-xs">
          {t('address_not_verified')}
        </p>
      )}

      {/* OSM map preview */}
      {mapUrl && (
        <div className="h-60 overflow-hidden rounded-lg">
          <iframe
            src={mapUrl}
            title={t('map_iframe_title')}
            dir="ltr"
            width="100%"
            height="240"
            loading="lazy"
            className="border-0"
            aria-label={t('map_iframe_aria_label')}
          />
        </div>
      )}
    </section>
  );
}
