import type { FeatureMeta, FeatureFilters, ViewState } from '../types';
import {
  MAX_TRAVEL_MINUTES,
  parseServerMode,
  resolveTransitVariant,
  type TravelTimeEntry,
  type TravelTimeInitial,
} from '../hooks/useTravelTime';
import { INITIAL_VIEW_STATE } from './consts';
import {
  SCHOOL_FILTER_NAME,
  createSchoolFilterKey,
  getSchoolFilterConfig,
  isSchoolFilterName,
  type SchoolPhase,
  type SchoolRating,
} from './school-filter';
import {
  SPECIFIC_CRIMES_FILTER_NAME,
  createSpecificCrimeFilterKey,
  getSpecificCrimeFeatureName,
  isSpecificCrimeFeatureName,
  isSpecificCrimeFilterName,
} from './crime-filter';
import {
  ELECTION_VOTE_SHARE_FILTER_NAME,
  createElectionVoteShareFilterKey,
  getElectionVoteShareFeatureName,
  isElectionVoteShareFeatureName,
  isElectionVoteShareFilterName,
} from './election-filter';
import {
  ETHNICITIES_FILTER_NAME,
  createEthnicityFilterKey,
  getEthnicityFeatureName,
  isEthnicityFeatureName,
  isEthnicityFilterName,
} from './ethnicity-filter';
import {
  POI_DISTANCE_FILTER_NAME,
  TRANSPORT_DISTANCE_FILTER_NAME,
  POI_COUNT_2KM_FILTER_NAME,
  POI_COUNT_5KM_FILTER_NAME,
  createPoiFilterKey,
  getPoiDistanceFeatureName,
  getPoiFilterName,
  isPoiDistanceFilterName,
  type PoiFilterName,
} from './poi-distance-filter';
import { dedupeTravelTimeEntries } from './travel-params';
import { isOverlayId, DEFAULT_OVERLAY_IDS, type OverlayId } from './overlays';
import { CRIME_TYPES, isCrimeTypeValue } from './crime-types';
import { isBasemapId, type BasemapId } from './basemaps';
import {
  DEFAULT_COLOR_OPACITY,
  colorOpacityToPercent,
  normalizeColorOpacity,
} from './color-opacity';

const POI_NONE_PARAM = '__none';
const CRIME_TYPES_NONE_PARAM = '__none';
const OVERLAY_NONE_PARAM = '__none';
const CRIME_OVERLAY_ID: OverlayId = 'crime-hotspots';

export interface UrlState {
  viewState: ViewState;
  /** True only when the URL carried explicit lat/lon/zoom (shared/dashboard link).
   *  False on a fresh visit, so the app may centre on the visitor's IP instead. */
  hasExplicitView: boolean;
  filters: FeatureFilters;
  poiCategories: Set<string>;
  overlays: Set<OverlayId>;
  crimeTypes?: Set<string>;
  basemap: BasemapId;
  colorOpacity: number;
  tab: 'properties' | 'area';
  travelTime?: TravelTimeInitial;
  postcode?: string;
  share?: string;
}

function parseFilters(params: URLSearchParams): FeatureFilters {
  const filterParams = params.getAll('filter');
  const schoolParams = params.getAll('school');
  const crimeParams = params.getAll('crime');
  const voteShareParams = params.getAll('voteShare');
  const ethnicityParams = params.getAll('ethnicity');
  const amenityDistanceParams = params.getAll('amenityDistance');
  const transportDistanceParams = params.getAll('transportDistance');
  const amenityCount2KmParams = params.getAll('amenityCount2km');
  const amenityCount5KmParams = params.getAll('amenityCount5km');
  if (
    filterParams.length === 0 &&
    schoolParams.length === 0 &&
    crimeParams.length === 0 &&
    voteShareParams.length === 0 &&
    ethnicityParams.length === 0 &&
    amenityDistanceParams.length === 0 &&
    transportDistanceParams.length === 0 &&
    amenityCount2KmParams.length === 0 &&
    amenityCount5KmParams.length === 0
  ) {
    return {};
  }

  const filters: FeatureFilters = {};
  for (const entry of filterParams) {
    const colonIdx = entry.indexOf(':');
    if (colonIdx === -1) continue;
    const name = entry.substring(0, colonIdx);
    const rest = entry.substring(colonIdx + 1);
    if (rest.includes(':')) {
      const [minStr, maxStr] = rest.split(':');
      const min = Number(minStr);
      const max = Number(maxStr);
      if (!isNaN(min) && !isNaN(max)) {
        filters[name] = [min, max];
      }
    } else if (rest.includes('|')) {
      filters[name] = rest.split('|');
    } else {
      filters[name] = [rest];
    }
  }

  schoolParams.forEach((entry, index) => {
    const parts = entry.split(':');
    // 4 parts is the current phase:rating:min:max form; 5 parts is the legacy
    // phase:rating:distance:min:max form, whose distance segment is ignored.
    if (parts.length !== 4 && parts.length !== 5) return;
    const phase = parts[0] as SchoolPhase;
    const rating = parts[1] as SchoolRating;
    const min = Number(parts[parts.length - 2]);
    const max = Number(parts[parts.length - 1]);
    if (
      (phase !== 'primary' && phase !== 'secondary') ||
      (rating !== 'good' && rating !== 'outstanding') ||
      isNaN(min) ||
      isNaN(max)
    ) {
      return;
    }
    filters[createSchoolFilterKey(phase, rating, index)] = [min, max];
  });

  crimeParams.forEach((entry, index) => {
    const parts = entry.split(':');
    if (parts.length < 3) return;
    const featureName = parts.slice(0, -2).join(':');
    const min = Number(parts[parts.length - 2]);
    const max = Number(parts[parts.length - 1]);
    if (!isSpecificCrimeFeatureName(featureName) || isNaN(min) || isNaN(max)) {
      return;
    }
    filters[createSpecificCrimeFilterKey(featureName, index)] = [min, max];
  });

  voteShareParams.forEach((entry, index) => {
    const parts = entry.split(':');
    if (parts.length < 3) return;
    const featureName = parts.slice(0, -2).join(':');
    const min = Number(parts[parts.length - 2]);
    const max = Number(parts[parts.length - 1]);
    if (!isElectionVoteShareFeatureName(featureName) || isNaN(min) || isNaN(max)) {
      return;
    }
    filters[createElectionVoteShareFilterKey(featureName, index)] = [min, max];
  });

  ethnicityParams.forEach((entry, index) => {
    const parts = entry.split(':');
    if (parts.length < 3) return;
    const featureName = parts.slice(0, -2).join(':');
    const min = Number(parts[parts.length - 2]);
    const max = Number(parts[parts.length - 1]);
    if (!isEthnicityFeatureName(featureName) || isNaN(min) || isNaN(max)) {
      return;
    }
    filters[createEthnicityFilterKey(featureName, index)] = [min, max];
  });

  const parsePoiParams = (entries: string[], filterName: PoiFilterName, startIndex: number) => {
    entries.forEach((entry, index) => {
      const parts = entry.split(':');
      if (parts.length < 3) return;
      const featureName = decodeURIComponent(parts.slice(0, -2).join(':'));
      const min = Number(parts[parts.length - 2]);
      const max = Number(parts[parts.length - 1]);
      const targetFilterName = getPoiFilterName(featureName);
      if (!targetFilterName || targetFilterName !== filterName || isNaN(min) || isNaN(max)) {
        return;
      }
      filters[createPoiFilterKey(targetFilterName, featureName, startIndex + index)] = [min, max];
    });
  };

  const parsePoiCountParams = (
    entries: string[],
    filterName: PoiFilterName,
    startIndex: number
  ) => {
    parsePoiParams(entries, filterName, startIndex);
  };
  parsePoiParams(amenityDistanceParams, POI_DISTANCE_FILTER_NAME, 0);
  parsePoiParams(
    transportDistanceParams,
    TRANSPORT_DISTANCE_FILTER_NAME,
    amenityDistanceParams.length
  );
  parsePoiCountParams(
    amenityCount2KmParams,
    POI_COUNT_2KM_FILTER_NAME,
    amenityDistanceParams.length + transportDistanceParams.length
  );
  parsePoiCountParams(
    amenityCount5KmParams,
    POI_COUNT_5KM_FILTER_NAME,
    amenityDistanceParams.length + transportDistanceParams.length + amenityCount2KmParams.length
  );

  return filters;
}

export function parseUrlState(): UrlState {
  const params = new URLSearchParams(window.location.search);
  const result: UrlState = {
    viewState: INITIAL_VIEW_STATE,
    hasExplicitView: false,
    filters: parseFilters(params),
    poiCategories: new Set(),
    overlays: new Set(DEFAULT_OVERLAY_IDS),
    basemap: 'standard',
    colorOpacity: DEFAULT_COLOR_OPACITY,
    tab: 'area',
  };

  // Share-link code: may grant bbox-scoped access when the backend record
  // contains an explicit server-created grant.
  const share = params.get('share');
  if (share && /^[a-z0-9]{1,20}$/i.test(share)) {
    result.share = share;
  }

  // View state: separate lat/lon/zoom params
  const lat = params.get('lat');
  const lon = params.get('lon');
  const zoom = params.get('zoom');
  if (lat && lon && zoom) {
    const latN = Number(lat);
    const lonN = Number(lon);
    const zoomN = Number(zoom);
    if (!isNaN(latN) && !isNaN(lonN) && !isNaN(zoomN)) {
      result.viewState = { latitude: latN, longitude: lonN, zoom: zoomN, pitch: 0 };
      result.hasExplicitView = true;
    }
  }

  // POI categories: repeated `poi` params
  const poiParams = params.getAll('poi');
  if (poiParams.length > 0) {
    if (poiParams.includes(POI_NONE_PARAM)) {
      result.poiCategories = new Set();
    } else {
      result.poiCategories = new Set(
        poiParams.filter((value) => value && value !== POI_NONE_PARAM)
      );
    }
  }

  // Overlays default to DEFAULT_OVERLAY_IDS on a fresh visit; explicit `overlay`
  // params override that, and the `__none` sentinel records "all off" so it
  // survives a reload instead of silently restoring the defaults.
  const overlayParams = params.getAll('overlay');
  if (overlayParams.length > 0) {
    result.overlays = overlayParams.includes(OVERLAY_NONE_PARAM)
      ? new Set()
      : new Set(overlayParams.filter(isOverlayId));
  }

  // Crime-type filter: repeated `crimeType` params, or the `__none` sentinel.
  // Absent means "all types" (handled downstream), so leave it undefined.
  const crimeTypeParams = params.getAll('crimeType');
  if (crimeTypeParams.length > 0) {
    result.crimeTypes = crimeTypeParams.includes(CRIME_TYPES_NONE_PARAM)
      ? new Set()
      : new Set(crimeTypeParams.filter(isCrimeTypeValue));
  }

  const basemap = params.get('basemap');
  if (basemap && isBasemapId(basemap)) {
    result.basemap = basemap;
  }

  const colorOpacity = params.get('colorOpacity');
  if (colorOpacity != null) {
    const colorOpacityPercent = Number(colorOpacity);
    if (!isNaN(colorOpacityPercent)) {
      result.colorOpacity = normalizeColorOpacity(colorOpacityPercent / 100);
    }
  }

  // Tab: full name
  const tab = params.get('tab');
  if (tab === 'properties' || tab === 'area') {
    result.tab = tab;
  }

  // Selected postcode. This is also accepted as the historical one-time
  // navigate-to-postcode param used by saved-property links.
  const pc = params.get('pc');
  if (pc) {
    result.postcode = pc;
  }

  // Travel time: repeated `tt` params
  // Format: serverMode:slug:label[:b][:min:max]
  // serverMode is one of: car | bicycle | walking | transit | transit-no-bus
  // | transit-no-change | transit-no-change-no-bus | transit-one-change
  // | transit-one-change-no-bus. Unknown modes cause the entry to be dropped
  // here (parseServerMode returns null) so we don't silently broaden the filter.
  const ttParams = params.getAll('tt');
  if (ttParams.length > 0) {
    const entries: TravelTimeEntry[] = [];
    for (const tt of ttParams) {
      const parts = tt.split(':');
      if (parts.length < 3) continue;
      const parsedMode = parseServerMode(parts[0]);
      if (!parsedMode) continue;
      const slug = parts[1];
      const label = decodeURIComponent(parts[2]);
      const useBest = parts.length >= 4 && parts[3] === 'b';
      const rangeOffset = useBest ? 1 : 0;
      let timeRange: [number, number] | null = null;
      if (parts.length >= 5 + rangeOffset) {
        const min = Number(parts[3 + rangeOffset]);
        const max = Number(parts[4 + rangeOffset]);
        if (!isNaN(min) && !isNaN(max)) {
          // Clamp loaded max-time to the data ceiling. Older shared URLs
          // may have max=120 from the previous slider range; no data exists
          // above MAX_TRAVEL_MINUTES so the result is identical.
          timeRange = [min, Math.min(max, MAX_TRAVEL_MINUTES)];
        }
      }
      entries.push({
        mode: parsedMode.mode,
        slug,
        label,
        timeRange,
        useBest,
        noChange: parsedMode.noChange,
        oneChange: parsedMode.oneChange,
        noBuses: parsedMode.noBuses,
      });
    }
    if (entries.length > 0) {
      result.travelTime = { entries: dedupeTravelTimeEntries(entries) };
    }
  }

  return result;
}

export function stateToParams(
  viewState: { latitude: number; longitude: number; zoom: number } | null,
  filters: FeatureFilters,
  features: FeatureMeta[],
  selectedPOICategories: Set<string>,
  rightPaneTab: 'properties' | 'area',
  travelTimeEntries?: TravelTimeEntry[],
  share?: string,
  selectedOverlays?: Set<OverlayId>,
  basemap?: BasemapId,
  selectedCrimeTypes?: Set<string>,
  selectedPostcode?: string,
  colorOpacity?: number
): URLSearchParams {
  const params = new URLSearchParams();

  if (share) {
    params.set('share', share);
  }

  if (viewState) {
    params.set('lat', viewState.latitude.toFixed(4));
    params.set('lon', viewState.longitude.toFixed(4));
    params.set('zoom', viewState.zoom.toFixed(1));
  }

  for (const [name, value] of Object.entries(filters)) {
    const schoolConfig = getSchoolFilterConfig(name);
    if (schoolConfig && isSchoolFilterName(name)) {
      const [min, max] = value as [number, number];
      params.append('school', `${schoolConfig.phase}:${schoolConfig.rating}:${min}:${max}`);
      continue;
    }

    const specificCrimeFeatureName = getSpecificCrimeFeatureName(name);
    if (specificCrimeFeatureName && isSpecificCrimeFilterName(name)) {
      const [min, max] = value as [number, number];
      params.append('crime', `${specificCrimeFeatureName}:${min}:${max}`);
      continue;
    }

    const electionVoteShareFeatureName = getElectionVoteShareFeatureName(name);
    if (electionVoteShareFeatureName && isElectionVoteShareFilterName(name)) {
      const [min, max] = value as [number, number];
      params.append('voteShare', `${electionVoteShareFeatureName}:${min}:${max}`);
      continue;
    }

    const ethnicityFeatureName = getEthnicityFeatureName(name);
    if (ethnicityFeatureName && isEthnicityFilterName(name)) {
      const [min, max] = value as [number, number];
      params.append('ethnicity', `${ethnicityFeatureName}:${min}:${max}`);
      continue;
    }

    const amenityDistanceFeatureName = getPoiDistanceFeatureName(name);
    if (amenityDistanceFeatureName && isPoiDistanceFilterName(name)) {
      const [min, max] = value as [number, number];
      const filterName = getPoiFilterName(name);
      const paramName =
        filterName === POI_COUNT_2KM_FILTER_NAME
          ? 'amenityCount2km'
          : filterName === POI_COUNT_5KM_FILTER_NAME
            ? 'amenityCount5km'
            : filterName === TRANSPORT_DISTANCE_FILTER_NAME
              ? 'transportDistance'
              : 'amenityDistance';
      params.append(paramName, `${encodeURIComponent(amenityDistanceFeatureName)}:${min}:${max}`);
      continue;
    }

    const meta = features.find((f) => f.name === name);
    if (meta?.type === 'enum' || typeof value[0] === 'string') {
      params.append('filter', `${name}:${(value as string[]).join('|')}`);
    } else {
      const [min, max] = value as [number, number];
      params.append('filter', `${name}:${min}:${max}`);
    }
  }

  if (selectedPOICategories.size === 0) {
    params.append('poi', POI_NONE_PARAM);
  } else {
    for (const category of selectedPOICategories) {
      params.append('poi', category);
    }
  }

  if (rightPaneTab === 'properties') {
    params.set('tab', 'properties');
  }

  if (selectedPostcode) {
    params.set('pc', selectedPostcode);
  }

  if (selectedOverlays) {
    if (selectedOverlays.size === 0) {
      // Distinguish a deliberate "all overlays off" from a fresh visit, which
      // would otherwise re-apply the defaults on reload.
      params.append('overlay', OVERLAY_NONE_PARAM);
    } else {
      for (const overlay of selectedOverlays) {
        params.append('overlay', overlay);
      }
    }
  }

  // Crime-type selection only matters while the crime overlay is on. Emit it
  // only when it deviates from the default (all types selected) to keep URLs
  // clean; a fully-deselected state uses an explicit sentinel.
  if (selectedOverlays?.has(CRIME_OVERLAY_ID) && selectedCrimeTypes) {
    if (selectedCrimeTypes.size === 0) {
      params.append('crimeType', CRIME_TYPES_NONE_PARAM);
    } else if (selectedCrimeTypes.size < CRIME_TYPES.length) {
      for (const crimeType of selectedCrimeTypes) {
        params.append('crimeType', crimeType);
      }
    }
  }

  if (basemap && basemap !== 'standard') {
    params.set('basemap', basemap);
  }

  if (colorOpacity != null) {
    const normalizedColorOpacity = normalizeColorOpacity(colorOpacity);
    if (normalizedColorOpacity !== DEFAULT_COLOR_OPACITY) {
      params.set('colorOpacity', String(colorOpacityToPercent(normalizedColorOpacity)));
    }
  }

  // Travel time: repeated `tt` params
  if (travelTimeEntries) {
    for (const entry of dedupeTravelTimeEntries(travelTimeEntries)) {
      if (!entry.slug) continue;
      const serverMode = resolveTransitVariant(entry);
      let val = `${serverMode}:${entry.slug}:${encodeURIComponent(entry.label)}`;
      if (entry.useBest) val += ':b';
      if (entry.timeRange) {
        val += `:${entry.timeRange[0]}:${entry.timeRange[1]}`;
      }
      params.append('tt', val);
    }
  }

  return params;
}

export function summarizeParams(queryString: string): string {
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const i18n = require('../i18n').default as {
    t: (key: string, opts?: Record<string, unknown>) => string;
  };
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const { ts } = require('../i18n/server') as { ts: (v: string) => string };
  const params = new URLSearchParams(queryString);
  const parts: string[] = [];

  const filterParams = params.getAll('filter');
  const schoolParams = params.getAll('school');
  const crimeParams = params.getAll('crime');
  const voteShareParams = params.getAll('voteShare');
  const ethnicityParams = params.getAll('ethnicity');
  const amenityDistanceParams = params.getAll('amenityDistance');
  const transportDistanceParams = params.getAll('transportDistance');
  const amenityCount2KmParams = params.getAll('amenityCount2km');
  const amenityCount5KmParams = params.getAll('amenityCount5km');
  if (
    filterParams.length > 0 ||
    schoolParams.length > 0 ||
    crimeParams.length > 0 ||
    voteShareParams.length > 0 ||
    ethnicityParams.length > 0 ||
    amenityDistanceParams.length > 0 ||
    transportDistanceParams.length > 0 ||
    amenityCount2KmParams.length > 0 ||
    amenityCount5KmParams.length > 0
  ) {
    const filterNames = filterParams
      .map((entry) => {
        const colonIdx = entry.indexOf(':');
        const name = colonIdx > 0 ? entry.substring(0, colonIdx) : entry;
        if (isSpecificCrimeFeatureName(name)) return SPECIFIC_CRIMES_FILTER_NAME;
        if (isElectionVoteShareFeatureName(name)) return ELECTION_VOTE_SHARE_FILTER_NAME;
        if (isEthnicityFeatureName(name)) return ETHNICITIES_FILTER_NAME;
        const poiFilterName = getPoiFilterName(name);
        if (poiFilterName) return poiFilterName;
        return name;
      })
      .filter((n) => n);
    for (let i = 0; i < schoolParams.length; i++) filterNames.push(SCHOOL_FILTER_NAME);
    for (let i = 0; i < crimeParams.length; i++) {
      filterNames.push(SPECIFIC_CRIMES_FILTER_NAME);
    }
    for (let i = 0; i < voteShareParams.length; i++) {
      filterNames.push(ELECTION_VOTE_SHARE_FILTER_NAME);
    }
    for (let i = 0; i < ethnicityParams.length; i++) {
      filterNames.push(ETHNICITIES_FILTER_NAME);
    }
    for (let i = 0; i < amenityDistanceParams.length; i++) {
      filterNames.push(POI_DISTANCE_FILTER_NAME);
    }
    for (let i = 0; i < transportDistanceParams.length; i++) {
      filterNames.push(TRANSPORT_DISTANCE_FILTER_NAME);
    }
    for (let i = 0; i < amenityCount2KmParams.length; i++) {
      filterNames.push(POI_COUNT_2KM_FILTER_NAME);
    }
    for (let i = 0; i < amenityCount5KmParams.length; i++) {
      filterNames.push(POI_COUNT_5KM_FILTER_NAME);
    }
    if (filterNames.length > 0) {
      parts.push(
        filterNames.length <= 2
          ? filterNames.map((n) => ts(n)).join(', ')
          : i18n.t('format.nFilters', { count: filterNames.length })
      );
    }
  }

  const poiParams = params.getAll('poi');
  if (poiParams.length > 0) {
    const count = poiParams.filter((value) => value && value !== POI_NONE_PARAM).length;
    if (count > 0) {
      parts.push(
        count === 1
          ? i18n.t('format.poiCategory', { count })
          : i18n.t('format.poiCategories', { count })
      );
    }
  }

  const ttParams = params.getAll('tt');
  if (ttParams.length > 0) {
    const count = ttParams.filter(Boolean).length;
    if (count > 0) {
      parts.push(
        count === 1
          ? i18n.t('format.travelDestination', { count })
          : i18n.t('format.travelDestinations', { count })
      );
    }
  }

  return parts.length > 0 ? parts.join(' + ') : i18n.t('format.noFilters');
}
