import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import type { FeatureMeta, FeatureFilters } from '../types';
import { trackEvent } from '../lib/analytics';
import {
  SCHOOL_FILTER_NAME,
  createSchoolFilterKey,
  getDefaultSchoolFeatureName,
  getSchoolBackendFeatureName,
  getSchoolFilterKeyId,
  normalizeSchoolFilters,
} from '../lib/school-filter';
import {
  SPECIFIC_CRIMES_FILTER_NAME,
  createSpecificCrimeFilterKey,
  getDefaultSpecificCrimeFeatureName,
  getSpecificCrimeFeatureName,
  getSpecificCrimeFilterKeyId,
  normalizeSpecificCrimeFilters,
} from '../lib/crime-filter';
import {
  ELECTION_VOTE_SHARE_FILTER_NAME,
  createElectionVoteShareFilterKey,
  getDefaultElectionVoteShareFeatureName,
  getElectionVoteShareFeatureName,
  getElectionVoteShareFilterKeyId,
  normalizeElectionVoteShareFilters,
} from '../lib/election-filter';
import {
  ETHNICITIES_FILTER_NAME,
  createEthnicityFilterKey,
  getDefaultEthnicityFeatureName,
  getEthnicityFeatureName,
  getEthnicityFilterKeyId,
  normalizeEthnicityFilters,
} from '../lib/ethnicity-filter';
import {
  POI_FILTER_NAMES,
  createPoiFilterKey,
  getDefaultPoiFilterFeatureName,
  getPoiDistanceFeatureName,
  getPoiDistanceFilterKeyId,
  normalizePoiDistanceFilters,
  usesFixedPoiDistanceScale,
  type PoiFilterName,
} from '../lib/poi-distance-filter';

interface UseFiltersOptions {
  initialFilters: FeatureFilters;
  features: FeatureMeta[];
  /** Max simultaneous filters for the current user; `null`/`undefined` = unlimited. */
  filterLimit?: number | null;
  /** Called when an add is blocked because `filterLimit` is already reached. */
  onFilterLimitReached?: () => void;
}

function normalizeFilters(filters: FeatureFilters): FeatureFilters {
  return normalizePoiDistanceFilters(
    normalizeEthnicityFilters(
      normalizeElectionVoteShareFilters(
        normalizeSpecificCrimeFilters(normalizeSchoolFilters(filters))
      )
    )
  );
}

function getBackendFeatureName(name: string): string {
  return (
    getSchoolBackendFeatureName(name) ??
    getSpecificCrimeFeatureName(name) ??
    getElectionVoteShareFeatureName(name) ??
    getEthnicityFeatureName(name) ??
    getPoiDistanceFeatureName(name) ??
    name
  );
}

function dropUnknownFilters(filters: FeatureFilters, features: FeatureMeta[]): FeatureFilters {
  if (features.length === 0) return filters;

  const knownFeatures = new Set(features.map((feature) => feature.name));
  let changed = false;
  const next: FeatureFilters = {};

  for (const [name, value] of Object.entries(filters)) {
    if (knownFeatures.has(getBackendFeatureName(name))) {
      next[name] = value;
    } else {
      changed = true;
    }
  }

  return changed ? next : filters;
}

function getNextNumericKeyId(
  filters: FeatureFilters,
  getId: (name: string) => string | null
): number {
  let max = -1;
  for (const name of Object.keys(filters)) {
    const id = getId(name);
    if (id == null) continue;
    const numeric = Number(id);
    if (Number.isInteger(numeric) && numeric >= 0) {
      max = Math.max(max, numeric);
    }
  }
  return max + 1;
}

export function useFilters({
  initialFilters,
  features,
  filterLimit,
  onFilterLimitReached,
}: UseFiltersOptions) {
  const initialFiltersRef = useRef<FeatureFilters | null>(null);
  if (!initialFiltersRef.current) {
    initialFiltersRef.current = normalizeFilters(initialFilters);
  }

  const [filters, setFilters] = useState<FeatureFilters>(() => initialFiltersRef.current!);
  // Mirrors used by handleAddFilter's limit check so its identity stays stable.
  const filtersRef = useRef(filters);
  filtersRef.current = filters;
  const filterLimitRef = useRef(filterLimit);
  filterLimitRef.current = filterLimit;
  const onFilterLimitReachedRef = useRef(onFilterLimitReached);
  onFilterLimitReachedRef.current = onFilterLimitReached;
  const [activeFeature, setActiveFeature] = useState<string | null>(null);
  const [dragValue, setDragValue] = useState<[number, number] | null>(null);
  const [pinnedFeature, setPinnedFeature] = useState<string | null>(null);
  const pendingDragRef = useRef<string | null>(null);
  const dragActiveRef = useRef<string | null>(null);
  const dragValueRef = useRef<[number, number] | null>(null);
  const undoStackRef = useRef<FeatureFilters[]>([]);
  const schoolFilterIdRef = useRef(
    getNextNumericKeyId(initialFiltersRef.current!, getSchoolFilterKeyId)
  );
  const specificCrimeFilterIdRef = useRef(
    getNextNumericKeyId(initialFiltersRef.current!, getSpecificCrimeFilterKeyId)
  );
  const electionVoteShareFilterIdRef = useRef(
    getNextNumericKeyId(initialFiltersRef.current!, getElectionVoteShareFilterKeyId)
  );
  const ethnicityFilterIdRef = useRef(
    getNextNumericKeyId(initialFiltersRef.current!, getEthnicityFilterKeyId)
  );
  const poiDistanceFilterIdRef = useRef(
    getNextNumericKeyId(initialFiltersRef.current!, getPoiDistanceFilterKeyId)
  );

  const enabledFeatures = useMemo(() => new Set(Object.keys(filters)), [filters]);

  const viewFeature = activeFeature || pinnedFeature;
  const viewSource: 'drag' | 'eye' | null = activeFeature ? 'drag' : pinnedFeature ? 'eye' : null;

  const filterRange = useMemo((): [number, number] | null => {
    if (!viewFeature) return null;
    if (activeFeature && dragValue) return dragValue;
    const filterVal = filters[viewFeature];
    if (filterVal && typeof filterVal[0] === 'number') return filterVal as [number, number];
    return null;
  }, [viewFeature, activeFeature, dragValue, filters]);

  useEffect(() => {
    if (features.length === 0) return;

    const knownFeatures = new Set(features.map((feature) => feature.name));
    setFilters((prev) => dropUnknownFilters(prev, features));
    setPinnedFeature((prev) => {
      if (!prev) return prev;
      return knownFeatures.has(getBackendFeatureName(prev)) ? prev : null;
    });
    setActiveFeature((prev) => {
      if (!prev) return prev;
      return knownFeatures.has(getBackendFeatureName(prev)) ? prev : null;
    });
  }, [features]);

  const handleAddFilter = useCallback(
    (name: string) => {
      const meta = features.find((f) => f.name === name);
      if (
        name !== SCHOOL_FILTER_NAME &&
        name !== SPECIFIC_CRIMES_FILTER_NAME &&
        name !== ELECTION_VOTE_SHARE_FILTER_NAME &&
        name !== ETHNICITIES_FILTER_NAME &&
        !POI_FILTER_NAMES.includes(name as PoiFilterName) &&
        !meta
      ) {
        return;
      }

      // Demo (unlicensed) users are capped at `filterLimit` simultaneous filters.
      // Re-applying an existing regular feature replaces it (no new key), so it's
      // always allowed; the special/generated filters always add a new key.
      const limit = filterLimitRef.current;
      if (limit != null) {
        const current = filtersRef.current;
        const addsNewKey =
          name === SCHOOL_FILTER_NAME ||
          name === SPECIFIC_CRIMES_FILTER_NAME ||
          name === ELECTION_VOTE_SHARE_FILTER_NAME ||
          name === ETHNICITIES_FILTER_NAME ||
          POI_FILTER_NAMES.includes(name as PoiFilterName) ||
          !(name in current);
        if (addsNewKey && Object.keys(current).length >= limit) {
          onFilterLimitReachedRef.current?.();
          return;
        }
      }

      trackEvent('Filter Add', { feature: name });
      setFilters((prev) => {
        undoStackRef.current.push(prev);
        if (undoStackRef.current.length > 50) undoStackRef.current.shift();
        if (name === SCHOOL_FILTER_NAME) {
          const schoolKey = createSchoolFilterKey('primary', 'good', schoolFilterIdRef.current++);
          const defaultSchoolFeatureName = getDefaultSchoolFeatureName(features);
          const defaultSchoolFeature = defaultSchoolFeatureName
            ? features.find((feature) => feature.name === defaultSchoolFeatureName)
            : undefined;
          return {
            ...prev,
            [schoolKey]: [
              defaultSchoolFeature?.histogram?.min ?? defaultSchoolFeature?.min ?? 0,
              defaultSchoolFeature?.histogram?.max ?? defaultSchoolFeature?.max ?? 10,
            ],
          };
        }
        if (name === SPECIFIC_CRIMES_FILTER_NAME) {
          const defaultCrimeFeatureName = getDefaultSpecificCrimeFeatureName(features);
          const defaultCrimeFeature = defaultCrimeFeatureName
            ? features.find((feature) => feature.name === defaultCrimeFeatureName)
            : undefined;
          if (!defaultCrimeFeatureName) return prev;

          return {
            ...prev,
            [createSpecificCrimeFilterKey(
              defaultCrimeFeatureName,
              specificCrimeFilterIdRef.current++
            )]: [
              defaultCrimeFeature?.histogram?.min ?? defaultCrimeFeature?.min ?? 0,
              defaultCrimeFeature?.histogram?.max ?? defaultCrimeFeature?.max ?? 100,
            ],
          };
        }
        if (name === ELECTION_VOTE_SHARE_FILTER_NAME) {
          const defaultVoteShareFeatureName = getDefaultElectionVoteShareFeatureName(features);
          const defaultVoteShareFeature = defaultVoteShareFeatureName
            ? features.find((feature) => feature.name === defaultVoteShareFeatureName)
            : undefined;
          if (!defaultVoteShareFeatureName) return prev;

          return {
            ...prev,
            [createElectionVoteShareFilterKey(
              defaultVoteShareFeatureName,
              electionVoteShareFilterIdRef.current++
            )]: [
              defaultVoteShareFeature?.histogram?.min ?? defaultVoteShareFeature?.min ?? 0,
              defaultVoteShareFeature?.histogram?.max ?? defaultVoteShareFeature?.max ?? 100,
            ],
          };
        }
        if (name === ETHNICITIES_FILTER_NAME) {
          const defaultEthnicityFeatureName = getDefaultEthnicityFeatureName(features);
          const defaultEthnicityFeature = defaultEthnicityFeatureName
            ? features.find((feature) => feature.name === defaultEthnicityFeatureName)
            : undefined;
          if (!defaultEthnicityFeatureName) return prev;

          return {
            ...prev,
            [createEthnicityFilterKey(defaultEthnicityFeatureName, ethnicityFilterIdRef.current++)]:
              [
                defaultEthnicityFeature?.histogram?.min ?? defaultEthnicityFeature?.min ?? 0,
                defaultEthnicityFeature?.histogram?.max ?? defaultEthnicityFeature?.max ?? 100,
              ],
          };
        }
        if (POI_FILTER_NAMES.includes(name as PoiFilterName)) {
          const poiFilterName = name as PoiFilterName;
          const defaultPoiFeatureName = getDefaultPoiFilterFeatureName(features, poiFilterName);
          const defaultPoiFeature = defaultPoiFeatureName
            ? features.find((feature) => feature.name === defaultPoiFeatureName)
            : undefined;
          if (!defaultPoiFeatureName) return prev;
          const fixedDistanceScale = usesFixedPoiDistanceScale(defaultPoiFeature);

          return {
            ...prev,
            [createPoiFilterKey(
              poiFilterName,
              defaultPoiFeatureName,
              poiDistanceFilterIdRef.current++
            )]: [
              fixedDistanceScale
                ? (defaultPoiFeature?.min ?? 0)
                : (defaultPoiFeature?.histogram?.min ?? defaultPoiFeature?.min ?? 0),
              fixedDistanceScale
                ? (defaultPoiFeature?.max ?? 5)
                : (defaultPoiFeature?.histogram?.max ?? defaultPoiFeature?.max ?? 5),
            ],
          };
        }
        if (!meta) return prev;
        if (meta.type === 'enum' && meta.values) {
          return { ...prev, [name]: [...meta.values!] };
        } else if (meta.type === 'numeric' && meta.histogram) {
          return { ...prev, [name]: [meta.histogram!.min, meta.histogram!.max] };
        } else if (meta.min != null && meta.max != null) {
          return { ...prev, [name]: [meta.min!, meta.max!] };
        }
        return prev;
      });
    },
    [features]
  );

  const handleUndo = useCallback(() => {
    const prev = undoStackRef.current.pop();
    if (prev) setFilters(prev);
  }, []);

  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
        const target = e.target as HTMLElement;
        if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
          return;
        e.preventDefault();
        handleUndo();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [handleUndo]);

  const handleFilterChange = useCallback((name: string, value: [number, number] | string[]) => {
    setFilters((prev) => {
      if (Array.isArray(value) && value.length === 0) {
        const next = { ...prev };
        delete next[name];
        return normalizeFilters(next);
      }

      const schoolKeyId = getSchoolFilterKeyId(name);
      if (schoolKeyId != null) {
        let replaced = false;
        const next: FeatureFilters = {};
        for (const [existingName, existingValue] of Object.entries(prev)) {
          if (getSchoolFilterKeyId(existingName) === schoolKeyId) {
            if (!replaced) {
              next[name] = value;
              replaced = true;
            }
            continue;
          }
          next[existingName] = existingValue;
        }
        if (replaced) return normalizeFilters(next);
      }

      const specificCrimeKeyId = getSpecificCrimeFilterKeyId(name);
      if (specificCrimeKeyId != null) {
        let replaced = false;
        const next: FeatureFilters = {};
        for (const [existingName, existingValue] of Object.entries(prev)) {
          if (getSpecificCrimeFilterKeyId(existingName) === specificCrimeKeyId) {
            if (!replaced) {
              next[name] = value;
              replaced = true;
            }
            continue;
          }
          next[existingName] = existingValue;
        }
        if (replaced) return normalizeFilters(next);
      }

      const ethnicityKeyId = getEthnicityFilterKeyId(name);
      if (ethnicityKeyId != null) {
        let replaced = false;
        const next: FeatureFilters = {};
        for (const [existingName, existingValue] of Object.entries(prev)) {
          if (getEthnicityFilterKeyId(existingName) === ethnicityKeyId) {
            if (!replaced) {
              next[name] = value;
              replaced = true;
            }
            continue;
          }
          next[existingName] = existingValue;
        }
        if (replaced) return normalizeFilters(next);
      }

      const electionVoteShareKeyId = getElectionVoteShareFilterKeyId(name);
      if (electionVoteShareKeyId != null) {
        let replaced = false;
        const next: FeatureFilters = {};
        for (const [existingName, existingValue] of Object.entries(prev)) {
          if (getElectionVoteShareFilterKeyId(existingName) === electionVoteShareKeyId) {
            if (!replaced) {
              next[name] = value;
              replaced = true;
            }
            continue;
          }
          next[existingName] = existingValue;
        }
        if (replaced) return normalizeFilters(next);
      }

      const poiDistanceKeyId = getPoiDistanceFilterKeyId(name);
      if (poiDistanceKeyId != null) {
        let replaced = false;
        const next: FeatureFilters = {};
        for (const [existingName, existingValue] of Object.entries(prev)) {
          if (getPoiDistanceFilterKeyId(existingName) === poiDistanceKeyId) {
            if (!replaced) {
              next[name] = value;
              replaced = true;
            }
            continue;
          }
          next[existingName] = existingValue;
        }
        if (replaced) return normalizeFilters(next);
      }

      return normalizeFilters({ ...prev, [name]: value });
    });
  }, []);

  const handleRemoveFilter = useCallback((name: string) => {
    trackEvent('Filter Remove', { feature: name });
    setFilters((prev) => {
      const next = { ...prev };
      delete next[name];
      return next;
    });
    setPinnedFeature((prev) => (prev === name ? null : prev));
  }, []);

  const handleDragStart = useCallback(
    (name: string, initialValue?: [number, number]) => {
      const meta = features.find((f) => f.name === name);
      if (meta?.type === 'enum') return;
      pendingDragRef.current = name;
      setDragValue(initialValue ?? null);
      dragValueRef.current = initialValue ?? null;
      setActiveFeature(name);
    },
    [features]
  );

  const handleDragChange = useCallback((value: [number, number]) => {
    if (pendingDragRef.current) {
      setActiveFeature(pendingDragRef.current);
      dragActiveRef.current = pendingDragRef.current;
      pendingDragRef.current = null;
    }
    setDragValue(value);
    dragValueRef.current = value;
  }, []);

  const handleDragEnd = useCallback(() => {
    if (pendingDragRef.current) {
      // Click without drag — no filter value was changed, just clear preview state.
      pendingDragRef.current = null;
      setActiveFeature(null);
      setDragValue(null);
      dragValueRef.current = null;
      return;
    }
    const af = dragActiveRef.current;
    const dv = dragValueRef.current;
    if (af && dv) {
      setFilters((prev) => normalizeFilters({ ...prev, [af]: dv }));
    }
    setActiveFeature(null);
    setDragValue(null);
    dragActiveRef.current = null;
    dragValueRef.current = null;
  }, []);

  /** End drag without committing to filters — caller handles the commit (e.g. travel time). */
  const handleDragEndNoCommit = useCallback((): [number, number] | null => {
    if (pendingDragRef.current) {
      pendingDragRef.current = null;
      setActiveFeature(null);
      setDragValue(null);
      dragValueRef.current = null;
      return null;
    }
    const dv = dragValueRef.current;
    setActiveFeature(null);
    setDragValue(null);
    dragActiveRef.current = null;
    dragValueRef.current = null;
    return dv;
  }, []);

  const handleSetFilters = useCallback((newFilters: FeatureFilters) => {
    // Enforce the demo cap on bulk sets too (e.g. AI filters, which bypass the
    // per-add path). Without this, an unlicensed user could end up with >5 filters,
    // which the server then rejects (400), breaking the map.
    let next = newFilters;
    const limit = filterLimitRef.current;
    if (limit != null && Object.keys(next).length > limit) {
      next = Object.fromEntries(Object.entries(next).slice(0, limit));
      onFilterLimitReachedRef.current?.();
    }
    setFilters(normalizeFilters(next));
    setActiveFeature(null);
    setDragValue(null);
    setPinnedFeature(null);
    pendingDragRef.current = null;
    dragActiveRef.current = null;
    dragValueRef.current = null;
  }, []);

  const handleTogglePin = useCallback((name: string) => {
    trackEvent('Filter Pin', { feature: name });
    setPinnedFeature((prev) => (prev === name ? null : name));
  }, []);

  const handleSetPin = useCallback((name: string) => {
    setPinnedFeature(name);
  }, []);

  const handleCancelPin = useCallback(() => {
    setPinnedFeature(null);
  }, []);

  return {
    filters,
    activeFeature,
    dragValue,
    pinnedFeature,
    enabledFeatures,
    viewFeature,
    viewSource,
    filterRange,
    handleAddFilter,
    handleFilterChange,
    handleRemoveFilter,
    handleSetFilters,
    handleDragStart,
    handleDragChange,
    handleDragEnd,
    handleDragEndNoCommit,
    handleTogglePin,
    handleSetPin,
    handleCancelPin,
  };
}
