import type { FeatureFilters, FeatureMeta } from '../types';

export const POI_DISTANCE_FILTER_NAME = 'Amenity distance';
export const TRANSPORT_DISTANCE_FILTER_NAME = 'Closest transport option';
export const POI_COUNT_2KM_FILTER_NAME = 'Amenities within 2km';
export const POI_COUNT_5KM_FILTER_NAME = 'Amenities within 5km';

export const POI_FILTER_NAMES = [
  TRANSPORT_DISTANCE_FILTER_NAME,
  POI_DISTANCE_FILTER_NAME,
  POI_COUNT_2KM_FILTER_NAME,
  POI_COUNT_5KM_FILTER_NAME,
] as const;

export type PoiFilterName = (typeof POI_FILTER_NAMES)[number];
type PoiMetric = 'distance' | 'count_2km' | 'count_5km';

export const POI_DISTANCE_FILTER_KEY_PREFIX = `${POI_DISTANCE_FILTER_NAME}:`;

const TRANSPORT_POI_CATEGORIES = new Set([
  'Airport',
  'Bus station',
  'Bus stop',
  'Ferry',
  'Rail station',
  'Taxi rank',
  'Tram & Metro stop',
  'Tube station',
]);

const DYNAMIC_DISTANCE_RE = /^Distance to nearest amenity \((.+)\) \(km\)$/;
const DYNAMIC_COUNT_RE = /^Number of amenities \((.+)\) within (2|5)km$/;

const POI_FILTER_CONFIGS: Record<
  PoiFilterName,
  {
    metric: PoiMetric;
    keyPrefix: string;
    description: string;
    detail: string;
    defaultMax: number;
    step: number;
    suffix: string;
  }
> = {
  [POI_DISTANCE_FILTER_NAME]: {
    metric: 'distance',
    keyPrefix: POI_DISTANCE_FILTER_KEY_PREFIX,
    description: 'Distance to nearby amenities',
    detail: 'Filter by distance to one nearby amenity type at a time.',
    defaultMax: 5,
    step: 0.1,
    suffix: ' km',
  },
  [TRANSPORT_DISTANCE_FILTER_NAME]: {
    metric: 'distance',
    keyPrefix: `${TRANSPORT_DISTANCE_FILTER_NAME}:`,
    description: 'Distance to nearby transport stops',
    detail: 'Filter by distance to one nearby public transport type at a time.',
    defaultMax: 5,
    step: 0.1,
    suffix: ' km',
  },
  [POI_COUNT_2KM_FILTER_NAME]: {
    metric: 'count_2km',
    keyPrefix: `${POI_COUNT_2KM_FILTER_NAME}:`,
    description: 'Number of nearby amenities within 2km',
    detail: 'Filter by the count of one amenity type within 2km.',
    defaultMax: 20,
    step: 1,
    suffix: '',
  },
  [POI_COUNT_5KM_FILTER_NAME]: {
    metric: 'count_5km',
    keyPrefix: `${POI_COUNT_5KM_FILTER_NAME}:`,
    description: 'Number of nearby amenities within 5km',
    detail: 'Filter by the count of one amenity type within 5km.',
    defaultMax: 50,
    step: 1,
    suffix: '',
  },
};

function getConfig(filterName: PoiFilterName) {
  return POI_FILTER_CONFIGS[filterName];
}

function isDynamicPoiDistanceFeatureName(name: string): boolean {
  return DYNAMIC_DISTANCE_RE.test(name);
}

function getPoiMetric(name: string): PoiMetric | null {
  if (DYNAMIC_DISTANCE_RE.test(name)) {
    return 'distance';
  }

  const countMatch = name.match(DYNAMIC_COUNT_RE);
  if (!countMatch) return null;
  return countMatch[2] === '2' ? 'count_2km' : 'count_5km';
}

export function isTransportPoiFeatureName(name: string): boolean {
  const category = getPoiFeatureCategory(name);
  return category ? TRANSPORT_POI_CATEGORIES.has(category) : false;
}

function getFilterNameForFeature(name: string): PoiFilterName | null {
  const metric = getPoiMetric(name);
  if (!metric) return null;

  const isTransport = isTransportPoiFeatureName(name);
  if (metric === 'distance' && isTransport) return TRANSPORT_DISTANCE_FILTER_NAME;
  if (metric === 'distance') return POI_DISTANCE_FILTER_NAME;
  if (isTransport) return null;
  if (metric === 'count_2km') return POI_COUNT_2KM_FILTER_NAME;
  if (metric === 'count_5km') return POI_COUNT_5KM_FILTER_NAME;
  return null;
}

function getFilterNameForMetric(metric: PoiMetric): PoiFilterName {
  if (metric === 'count_2km') return POI_COUNT_2KM_FILTER_NAME;
  if (metric === 'count_5km') return POI_COUNT_5KM_FILTER_NAME;
  return POI_DISTANCE_FILTER_NAME;
}

export function getPoiFeatureCategory(name: string): string | null {
  const distanceMatch = name.match(DYNAMIC_DISTANCE_RE);
  if (distanceMatch) return distanceMatch[1];

  const countMatch = name.match(DYNAMIC_COUNT_RE);
  if (countMatch) return countMatch[1];

  return null;
}

export function isPoiDistanceFeatureName(name: string): boolean {
  return isDynamicPoiDistanceFeatureName(name);
}

export function usesFixedPoiDistanceScale(feature?: FeatureMeta): boolean {
  return Boolean(feature?.absolute && isPoiDistanceFeatureName(feature.name));
}

export function isPoiFilterFeatureName(name: string): boolean {
  return getPoiMetric(name) != null;
}

export function getPoiFilterName(name: string): PoiFilterName | null {
  for (const filterName of POI_FILTER_NAMES) {
    if (name.startsWith(getConfig(filterName).keyPrefix)) return filterName;
  }
  return getFilterNameForFeature(name);
}

export function isPoiDistanceFilterName(name: string): boolean {
  return getPoiFilterName(name) != null;
}

export function createPoiFilterKey(
  filterName: PoiFilterName,
  featureName: string,
  id: number | string
): string {
  return `${getConfig(filterName).keyPrefix}${encodeURIComponent(featureName)}:${id}`;
}

export function createPoiDistanceFilterKey(featureName: string, id: number | string): string {
  return createPoiFilterKey(POI_DISTANCE_FILTER_NAME, featureName, id);
}

export function getPoiFilterKeyId(name: string): string | null {
  const filterName = getPoiFilterName(name);
  if (!filterName) return null;
  const prefix = getConfig(filterName).keyPrefix;
  if (!name.startsWith(prefix)) return null;
  const rest = name.substring(prefix.length);
  const lastColon = rest.lastIndexOf(':');
  return lastColon === -1 ? null : rest.substring(lastColon + 1);
}

export function getPoiDistanceFilterKeyId(name: string): string | null {
  return getPoiFilterKeyId(name);
}

export function parsePoiFilterKey(name: string): string | null {
  const filterName = getPoiFilterName(name);
  if (!filterName) return null;
  const prefix = getConfig(filterName).keyPrefix;
  if (!name.startsWith(prefix)) return null;
  const rest = name.substring(prefix.length);
  const lastColon = rest.lastIndexOf(':');
  if (lastColon === -1) return null;

  const decoded = decodeURIComponent(rest.substring(0, lastColon));
  return getFilterNameForFeature(decoded) === filterName ? decoded : null;
}

export function parsePoiDistanceFilterKey(name: string): string | null {
  return parsePoiFilterKey(name);
}

export function getPoiDistanceFeatureName(name: string): string | null {
  if (isPoiFilterFeatureName(name)) return name;
  return parsePoiFilterKey(name);
}

export function getActiveAmenityFilterFeatureNames(filters: FeatureFilters): Set<string> {
  const names = new Set<string>();

  for (const name of Object.keys(filters)) {
    const filterName = getPoiFilterName(name);
    if (!filterName || filterName === TRANSPORT_DISTANCE_FILTER_NAME) continue;

    const featureName = getPoiDistanceFeatureName(name);
    if (featureName) names.add(featureName);
  }

  return names;
}

export function replacePoiFilterKeySelection(key: string, featureName: string): string {
  const filterName =
    getPoiFilterName(key) ??
    getFilterNameForFeature(featureName) ??
    getFilterNameForMetric(getPoiMetric(featureName)!);
  const id = getPoiFilterKeyId(key) ?? '0';
  return createPoiFilterKey(filterName, featureName, id);
}

export function replacePoiDistanceFilterKeySelection(key: string, featureName: string): string {
  return replacePoiFilterKeySelection(key, featureName);
}

export function getPoiFilterFeatureOptions(
  features: FeatureMeta[],
  filterName: PoiFilterName
): FeatureMeta[] {
  const metric = getConfig(filterName).metric;
  const dynamicOptions = features.filter((feature) => {
    const featureMetric = getPoiMetric(feature.name);
    if (featureMetric !== metric) return false;
    const isTransport = isTransportPoiFeatureName(feature.name);
    if (filterName === TRANSPORT_DISTANCE_FILTER_NAME) {
      return metric === 'distance' && isTransport;
    }
    if (isTransport) return false;
    return metric !== 'distance' || DYNAMIC_DISTANCE_RE.test(feature.name);
  });

  if (filterName === TRANSPORT_DISTANCE_FILTER_NAME) {
    return dynamicOptions;
  }
  return dynamicOptions;
}

export function getDefaultPoiFilterFeatureName(
  features: FeatureMeta[],
  filterName: PoiFilterName
): string | null {
  return getPoiFilterFeatureOptions(features, filterName)[0]?.name ?? null;
}

export function getDefaultPoiDistanceFeatureName(features: FeatureMeta[]): string | null {
  return getDefaultPoiFilterFeatureName(features, POI_DISTANCE_FILTER_NAME);
}

export function getPoiFilterMeta(features: FeatureMeta[], filterName: PoiFilterName): FeatureMeta {
  const sourceFeatureName = getDefaultPoiFilterFeatureName(features, filterName);
  const sourceFeature = sourceFeatureName
    ? features.find((feature) => feature.name === sourceFeatureName)
    : undefined;
  const config = getConfig(filterName);

  return {
    name: filterName,
    type: 'numeric',
    group: filterName === TRANSPORT_DISTANCE_FILTER_NAME ? 'Transport' : 'Amenities',
    min: sourceFeature?.min ?? 0,
    max: sourceFeature?.max ?? config.defaultMax,
    step: config.step,
    description: config.description,
    detail: config.detail,
    source: sourceFeature?.source ?? 'osm-pois',
    suffix: config.suffix,
    absolute: sourceFeature?.absolute,
  };
}

export function getPoiDistanceFilterMeta(features: FeatureMeta[]): FeatureMeta {
  return getPoiFilterMeta(features, POI_DISTANCE_FILTER_NAME);
}

export function normalizePoiDistanceFilters(filters: FeatureFilters): FeatureFilters {
  let changed = false;
  const next: FeatureFilters = {};

  for (const [name, value] of Object.entries(filters)) {
    if (isPoiFilterFeatureName(name)) {
      const filterName = getPoiFilterName(name);
      if (!filterName) {
        changed = true;
        continue;
      }
      next[createPoiFilterKey(filterName, name, Object.keys(next).length)] = value;
      changed = true;
      continue;
    }
    next[name] = value;
  }

  return changed ? next : filters;
}

export function clampPoiFilterRange(
  value: [number, number],
  feature?: FeatureMeta
): [number, number] {
  const min = usesFixedPoiDistanceScale(feature)
    ? (feature?.min ?? 0)
    : (feature?.histogram?.min ?? feature?.min ?? 0);
  const max = usesFixedPoiDistanceScale(feature)
    ? (feature?.max ?? 5)
    : (feature?.histogram?.max ?? feature?.max ?? Math.max(1, value[1]));
  return [Math.max(min, Math.min(value[0], max)), Math.max(min, Math.min(value[1], max))];
}

export function clampPoiDistanceRange(
  value: [number, number],
  feature?: FeatureMeta
): [number, number] {
  return clampPoiFilterRange(value, feature);
}
